From 4473d7f5dd4f45fb6f2af3f92befb49f666887d5 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 22 Jun 2020 16:18:10 -0400 Subject: [PATCH 001/113] [preservefiles] Move permissions classes to libcalamares --- .../permissions.cpp => libcalamares/utils/Permissions.cpp} | 0 .../permissions.h => libcalamares/utils/Permissions.h} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/{modules/preservefiles/permissions.cpp => libcalamares/utils/Permissions.cpp} (100%) rename src/{modules/preservefiles/permissions.h => libcalamares/utils/Permissions.h} (100%) diff --git a/src/modules/preservefiles/permissions.cpp b/src/libcalamares/utils/Permissions.cpp similarity index 100% rename from src/modules/preservefiles/permissions.cpp rename to src/libcalamares/utils/Permissions.cpp diff --git a/src/modules/preservefiles/permissions.h b/src/libcalamares/utils/Permissions.h similarity index 100% rename from src/modules/preservefiles/permissions.h rename to src/libcalamares/utils/Permissions.h From e24f812b2d29e2833668b2c4a553a9d9ef1e496b Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 22 Jun 2020 16:32:47 -0400 Subject: [PATCH 002/113] [libcalamares] Chase Permissions move - Fix include names in *preservefiles* - Tidy up include guards - Fix CMakeLists in *perservefiles* and *libcalamares* - Use SPDX license headers --- src/libcalamares/CMakeLists.txt | 1 + src/libcalamares/utils/Permissions.cpp | 67 ++++++-------- src/libcalamares/utils/Permissions.h | 41 ++++----- src/modules/preservefiles/CMakeLists.txt | 1 - src/modules/preservefiles/PreserveFiles.cpp | 99 ++++++++++++--------- src/modules/preservefiles/PreserveFiles.h | 34 +++---- 6 files changed, 115 insertions(+), 128 deletions(-) diff --git a/src/libcalamares/CMakeLists.txt b/src/libcalamares/CMakeLists.txt index 0516b5613..56bacb32a 100644 --- a/src/libcalamares/CMakeLists.txt +++ b/src/libcalamares/CMakeLists.txt @@ -76,6 +76,7 @@ set( libSources utils/Dirs.cpp utils/Entropy.cpp utils/Logger.cpp + utils/Permissions.cpp utils/PluginFactory.cpp utils/Retranslator.cpp utils/String.cpp diff --git a/src/libcalamares/utils/Permissions.cpp b/src/libcalamares/utils/Permissions.cpp index a3f8ac136..d9d3226e6 100644 --- a/src/libcalamares/utils/Permissions.cpp +++ b/src/libcalamares/utils/Permissions.cpp @@ -1,75 +1,66 @@ /* === This file is part of Calamares - === * - * Copyright (C) 2018 Scott Harvey - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * SPDX-FileCopyrightText: 2018 Scott Harvey + * SPDX-License-Identifier: GPL-3.0-or-later + * License-Filename: LICENSE * */ +#include "Permissions.h" + #include #include -#include "permissions.h" -Permissions::Permissions() : - m_username(), - m_group(), - m_valid(false), - m_value(0) +Permissions::Permissions() + : m_username() + , m_group() + , m_valid( false ) + , m_value( 0 ) { } -Permissions::Permissions(QString p) : Permissions() +Permissions::Permissions( QString p ) + : Permissions() { - parsePermissions(p); + parsePermissions( p ); } -void Permissions::parsePermissions(const QString& p) { +void +Permissions::parsePermissions( const QString& p ) +{ - QStringList segments = p.split(":"); + QStringList segments = p.split( ":" ); - if (segments.length() != 3) { + if ( segments.length() != 3 ) + { m_valid = false; return; } - if (segments[0].isEmpty() || segments[1].isEmpty()) { + if ( segments[ 0 ].isEmpty() || segments[ 1 ].isEmpty() ) + { m_valid = false; return; } bool ok; - int octal = segments[2].toInt(&ok, 8); - if (!ok || octal == 0) { + int octal = segments[ 2 ].toInt( &ok, 8 ); + if ( !ok || octal == 0 ) + { m_valid = false; return; - } else { + } + else + { m_value = octal; } // We have exactly three segments and the third is valid octal, // so we can declare the string valid and set the user and group names m_valid = true; - m_username = segments[0]; - m_group = segments[1]; + m_username = segments[ 0 ]; + m_group = segments[ 1 ]; return; - } - - - - - - diff --git a/src/libcalamares/utils/Permissions.h b/src/libcalamares/utils/Permissions.h index 4cb70a2c2..baa5da554 100644 --- a/src/libcalamares/utils/Permissions.h +++ b/src/libcalamares/utils/Permissions.h @@ -1,45 +1,35 @@ /* === This file is part of Calamares - === * - * Copyright (C) 2018 Scott Harvey - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * SPDX-FileCopyrightText: 2018 Scott Harvey + * SPDX-License-Identifier: GPL-3.0-or-later + * License-Filename: LICENSE * */ -#ifndef PERMISSIONS_H -#define PERMISSIONS_H +#ifndef LIBCALAMARES_PERMISSIONS_H +#define LIBCALAMARES_PERMISSIONS_H + +#include "DllMacro.h" #include /** - * @brief The Permissions class takes a QString @p in the form of + * @brief The Permissions class takes a QString @p in the form of * ::, checks it for validity, and makes the three * components available indivdually. */ -class Permissions +class DLLEXPORT Permissions { public: - /** @brief Constructor - * - * Splits the string @p at the colon (":") into separate elements for + * + * Splits the string @p at the colon (":") into separate elements for * , , and (permissions), where is returned as * an **octal** integer. */ - Permissions(QString p); - + Permissions( QString p ); + /** @brief Default constructor of an invalid Permissions. */ Permissions(); @@ -50,13 +40,12 @@ public: QString octal() const { return QString::number( m_value, 8 ); } private: - void parsePermissions(QString const &p); + void parsePermissions( QString const& p ); QString m_username; QString m_group; bool m_valid; int m_value; - }; -#endif // PERMISSIONS_H +#endif // LIBCALAMARES_PERMISSIONS_H diff --git a/src/modules/preservefiles/CMakeLists.txt b/src/modules/preservefiles/CMakeLists.txt index f6cd98008..571de31ca 100644 --- a/src/modules/preservefiles/CMakeLists.txt +++ b/src/modules/preservefiles/CMakeLists.txt @@ -4,7 +4,6 @@ calamares_add_plugin( preservefiles TYPE job EXPORT_MACRO PLUGINDLLEXPORT_PRO SOURCES - permissions.cpp PreserveFiles.cpp LINK_PRIVATE_LIBRARIES calamares diff --git a/src/modules/preservefiles/PreserveFiles.cpp b/src/modules/preservefiles/PreserveFiles.cpp index 175f8e4f8..3e34024e7 100644 --- a/src/modules/preservefiles/PreserveFiles.cpp +++ b/src/modules/preservefiles/PreserveFiles.cpp @@ -1,39 +1,28 @@ /* === This file is part of Calamares - === * - * Copyright 2018, Adriaan de Groot + * SPDX-FileCopyrightText: 2018 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later + * License-Filename: LICENSE * - * 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 . */ #include "PreserveFiles.h" -#include "permissions.h" - #include "CalamaresVersion.h" -#include "JobQueue.h" #include "GlobalStorage.h" - +#include "JobQueue.h" #include "utils/CalamaresUtilsSystem.h" #include "utils/CommandList.h" #include "utils/Logger.h" +#include "utils/Permissions.h" #include "utils/Units.h" #include using CalamaresUtils::operator""_MiB; -QString targetPrefix() +QString +targetPrefix() { if ( CalamaresUtils::System::instance()->doChroot() ) { @@ -42,9 +31,13 @@ QString targetPrefix() { QString r = gs->value( "rootMountPoint" ).toString(); if ( !r.isEmpty() ) + { return r; + } else + { cDebug() << "RootMountPoint is empty"; + } } else { @@ -55,16 +48,21 @@ QString targetPrefix() return QLatin1String( "/" ); } -QString atReplacements( QString s ) +QString +atReplacements( QString s ) { Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); QString root( "/" ); QString user; if ( gs && gs->contains( "rootMountPoint" ) ) + { root = gs->value( "rootMountPoint" ).toString(); + } if ( gs && gs->contains( "username" ) ) + { user = gs->value( "username" ).toString(); + } return s.replace( "@@ROOT@@", root ).replace( "@@USER@@", user ); } @@ -74,9 +72,7 @@ PreserveFiles::PreserveFiles( QObject* parent ) { } -PreserveFiles::~PreserveFiles() -{ -} +PreserveFiles::~PreserveFiles() {} QString PreserveFiles::prettyName() const @@ -107,8 +103,7 @@ copy_file( const QString& source, const QString& dest ) { b = sourcef.read( 1_MiB ); destf.write( b ); - } - while ( b.count() > 0 ); + } while ( b.count() > 0 ); sourcef.close(); destf.close(); @@ -116,14 +111,19 @@ copy_file( const QString& source, const QString& dest ) return true; } -Calamares::JobResult PreserveFiles::exec() +Calamares::JobResult +PreserveFiles::exec() { if ( m_items.isEmpty() ) + { return Calamares::JobResult::error( tr( "No files configured to save for later." ) ); + } QString prefix = targetPrefix(); if ( !prefix.endsWith( '/' ) ) + { prefix.append( '/' ); + } int count = 0; for ( const auto& it : m_items ) @@ -133,16 +133,24 @@ Calamares::JobResult PreserveFiles::exec() QString dest = prefix + bare_dest; if ( it.type == ItemType::Log ) + { source = Logger::logFile(); + } if ( it.type == ItemType::Config ) { if ( Calamares::JobQueue::instance()->globalStorage()->save( dest ) ) + { cWarning() << "Could not write config for" << dest; + } else + { ++count; + } } else if ( source.isEmpty() ) + { cWarning() << "Skipping unnamed source file for" << dest; + } else { if ( copy_file( source, dest ) ) @@ -153,17 +161,23 @@ Calamares::JobResult PreserveFiles::exec() int r; - r = s_p->targetEnvCall( QStringList{ "chown", it.perm.username(), bare_dest } ); + r = s_p->targetEnvCall( QStringList { "chown", it.perm.username(), bare_dest } ); if ( r ) + { cWarning() << "Could not chown target" << bare_dest; + } - r = s_p->targetEnvCall( QStringList{ "chgrp", it.perm.group(), bare_dest } ); + r = s_p->targetEnvCall( QStringList { "chgrp", it.perm.group(), bare_dest } ); if ( r ) + { cWarning() << "Could not chgrp target" << bare_dest; + } - r = s_p->targetEnvCall( QStringList{ "chmod", it.perm.octal(), bare_dest } ); + r = s_p->targetEnvCall( QStringList { "chmod", it.perm.octal(), bare_dest } ); if ( r ) + { cWarning() << "Could not chmod target" << bare_dest; + } } ++count; @@ -171,12 +185,13 @@ Calamares::JobResult PreserveFiles::exec() } } - return count == m_items.count() ? - Calamares::JobResult::ok() : - Calamares::JobResult::error( tr( "Not all of the configured files could be preserved." ) ); + return count == m_items.count() + ? Calamares::JobResult::ok() + : Calamares::JobResult::error( tr( "Not all of the configured files could be preserved." ) ); } -void PreserveFiles::setConfigurationMap(const QVariantMap& configurationMap) +void +PreserveFiles::setConfigurationMap( const QVariantMap& configurationMap ) { auto files = configurationMap[ "files" ]; if ( !files.isValid() ) @@ -193,7 +208,9 @@ void PreserveFiles::setConfigurationMap(const QVariantMap& configurationMap) QString defaultPermissions = configurationMap[ "perm" ].toString(); if ( defaultPermissions.isEmpty() ) + { defaultPermissions = QStringLiteral( "root:root:0400" ); + } QVariantList l = files.toList(); unsigned int c = 0; @@ -203,22 +220,23 @@ void PreserveFiles::setConfigurationMap(const QVariantMap& configurationMap) { QString filename = li.toString(); if ( !filename.isEmpty() ) - m_items.append( Item{ filename, filename, Permissions( defaultPermissions ), ItemType::Path } ); + m_items.append( Item { filename, filename, Permissions( defaultPermissions ), ItemType::Path } ); else + { cDebug() << "Empty filename for preservefiles, item" << c; + } } else if ( li.type() == QVariant::Map ) { const auto map = li.toMap(); QString dest = map[ "dest" ].toString(); QString from = map[ "from" ].toString(); - ItemType t = - ( from == "log" ) ? ItemType::Log : - ( from == "config" ) ? ItemType::Config : - ItemType::None; + ItemType t = ( from == "log" ) ? ItemType::Log : ( from == "config" ) ? ItemType::Config : ItemType::None; QString perm = map[ "perm" ].toString(); if ( perm.isEmpty() ) + { perm = defaultPermissions; + } if ( dest.isEmpty() ) { @@ -230,15 +248,16 @@ void PreserveFiles::setConfigurationMap(const QVariantMap& configurationMap) } else { - m_items.append( Item{ QString(), dest, Permissions( perm ), t } ); + m_items.append( Item { QString(), dest, Permissions( perm ), t } ); } } else + { cDebug() << "Invalid type for preservefiles, item" << c; + } ++c; } } -CALAMARES_PLUGIN_FACTORY_DEFINITION( PreserveFilesFactory, registerPlugin(); ) - +CALAMARES_PLUGIN_FACTORY_DEFINITION( PreserveFilesFactory, registerPlugin< PreserveFiles >(); ) diff --git a/src/modules/preservefiles/PreserveFiles.h b/src/modules/preservefiles/PreserveFiles.h index 587ac9bab..214ff0df8 100644 --- a/src/modules/preservefiles/PreserveFiles.h +++ b/src/modules/preservefiles/PreserveFiles.h @@ -1,35 +1,23 @@ /* === This file is part of Calamares - === * - * Copyright 2018, Adriaan de Groot + * SPDX-FileCopyrightText: 2018 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later + * License-Filename: LICENSE * - * 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 . */ #ifndef PRESERVEFILES_H #define PRESERVEFILES_H +#include "CppJob.h" +#include "DllMacro.h" +#include "utils/Permissions.h" +#include "utils/PluginFactory.h" + #include #include #include -#include "CppJob.h" -#include "DllMacro.h" - -#include "utils/PluginFactory.h" - -#include "permissions.h" - class PLUGINDLLEXPORT PreserveFiles : public Calamares::CppJob { Q_OBJECT @@ -40,7 +28,7 @@ class PLUGINDLLEXPORT PreserveFiles : public Calamares::CppJob Path, Log, Config - } ; + }; struct Item { @@ -48,7 +36,7 @@ class PLUGINDLLEXPORT PreserveFiles : public Calamares::CppJob QString dest; Permissions perm; ItemType type; - } ; + }; using ItemList = QList< Item >; @@ -68,4 +56,4 @@ private: CALAMARES_PLUGIN_FACTORY_DECLARATION( PreserveFilesFactory ) -#endif // PRESERVEFILES_H +#endif // PRESERVEFILES_H From e1c85340e40a09756a0d60d79179ea1f26e4a967 Mon Sep 17 00:00:00 2001 From: Calamares CI Date: Mon, 20 Jul 2020 12:05:55 +0200 Subject: [PATCH 003/113] i18n: [calamares] Automatic merge of Transifex translations FIXES #1455 --- lang/calamares_cs_CZ.ts | 8 ++++---- lang/calamares_pt_BR.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lang/calamares_cs_CZ.ts b/lang/calamares_cs_CZ.ts index e38f1ff69..e1a62eb13 100644 --- a/lang/calamares_cs_CZ.ts +++ b/lang/calamares_cs_CZ.ts @@ -781,7 +781,7 @@ Instalační program bude ukončen a všechny změny ztraceny. <h1>Welcome to the Calamares setup program for %1</h1> - + <h1>Vítejte v Calamares instalačním programu pro %1.</h1> @@ -796,7 +796,7 @@ Instalační program bude ukončen a všechny změny ztraceny. <h1>Welcome to the %1 installer</h1> - + <h1>Vítejte v instalátoru %1.</h1> @@ -3424,7 +3424,7 @@ Výstup: KDE user feedback - + Zpětná vazba uživatele KDE @@ -3445,7 +3445,7 @@ Výstup: Could not configure KDE user feedback correctly, Calamares error %1. - + Nepodařilo se správně nastavit zpětnou vazbu KDE uživatele, chyba Calamares %1. diff --git a/lang/calamares_pt_BR.ts b/lang/calamares_pt_BR.ts index f65ecd0ee..27d8803a3 100644 --- a/lang/calamares_pt_BR.ts +++ b/lang/calamares_pt_BR.ts @@ -3549,7 +3549,7 @@ Saída: Your username must start with a lowercase letter or underscore. - Seu nome de usuário deve começar com uma letra maiúscula ou com um sublinhado. + Seu nome de usuário deve começar com uma letra minúscula ou com um sublinhado. From 0d5db2dd062c13c542c02a0a5750e8e8393c6e84 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 20 Jul 2020 12:36:58 +0200 Subject: [PATCH 004/113] [localeq] Config-handling is a total bodge-job, disable --- src/modules/localeq/LocaleQmlViewStep.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/localeq/LocaleQmlViewStep.cpp b/src/modules/localeq/LocaleQmlViewStep.cpp index fd5e72734..4354cb7bd 100644 --- a/src/modules/localeq/LocaleQmlViewStep.cpp +++ b/src/modules/localeq/LocaleQmlViewStep.cpp @@ -65,7 +65,7 @@ LocaleQmlViewStep::fetchGeoIpTimezone() } } - m_config->setLocaleInfo(m_startingTimezone.first, m_startingTimezone.second, m_localeGenPath); + // m_config->setLocaleInfo(m_startingTimezone.first, m_startingTimezone.second, m_localeGenPath); } Calamares::RequirementsList LocaleQmlViewStep::checkRequirements() @@ -138,6 +138,7 @@ void LocaleQmlViewStep::onActivate() void LocaleQmlViewStep::onLeave() { +#if 0 if ( true ) { m_jobs = m_config->createJobs(); @@ -157,6 +158,7 @@ void LocaleQmlViewStep::onLeave() m_jobs.clear(); Calamares::JobQueue::instance()->globalStorage()->remove( "localeConf" ); } +#endif } void LocaleQmlViewStep::setConfigurationMap(const QVariantMap& configurationMap) From 8119c7e72a861df5d85394cd602963d86e059cc9 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 20 Jul 2020 12:37:27 +0200 Subject: [PATCH 005/113] [locale] Reset Config object The Config object wasn't being used at all in the locale module; reset it to empty and start using it in locale, so that configuration functionality can be added to it as-needed, and with the necessary refactoring built-in. --- src/modules/locale/Config.cpp | 306 +------------------------- src/modules/locale/Config.h | 56 +---- src/modules/locale/LocaleViewStep.cpp | 4 +- src/modules/locale/LocaleViewStep.h | 10 +- 4 files changed, 17 insertions(+), 359 deletions(-) diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index cde0a5e09..ae8595734 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -1,7 +1,8 @@ /* === This file is part of Calamares - === * - * Copyright 2019-2020, Adriaan de Groot - * Copyright 2020, Camilo Higuita + * SPDX-FileCopyrightText: 2020 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later + * License-Filename: LICENSE * * Calamares is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,313 +20,16 @@ #include "Config.h" -#include "LCLocaleDialog.h" -#include "SetTimezoneJob.h" -#include "timezonewidget/timezonewidget.h" - -#include "GlobalStorage.h" -#include "JobQueue.h" -#include "Settings.h" - -#include "locale/Label.h" -#include "locale/TimeZone.h" -#include "utils/CalamaresUtilsGui.h" #include "utils/Logger.h" -#include "utils/Retranslator.h" - -#include -#include -#include Config::Config( QObject* parent ) : QObject( parent ) - , m_regionList( CalamaresUtils::Locale::TZRegion::fromZoneTab() ) - , m_regionModel( new CalamaresUtils::Locale::CStringListModel( m_regionList ) ) - , m_zonesModel( new CalamaresUtils::Locale::CStringListModel() ) - , m_blockTzWidgetSet( false ) { - connect( m_regionModel, &CalamaresUtils::Locale::CStringListModel::currentIndexChanged, [&]() { - m_zonesModel->setList( static_cast< const CalamaresUtils::Locale::TZRegion* >( - m_regionModel->item( m_regionModel->currentIndex() ) ) - ->zones() ); - updateLocaleLabels(); - } ); - - connect( - m_zonesModel, &CalamaresUtils::Locale::CStringListModel::currentIndexChanged, [&]() { updateLocaleLabels(); } ); } -Config::~Config() -{ - qDeleteAll( m_regionList ); -} - -CalamaresUtils::Locale::CStringListModel* -Config::zonesModel() const -{ - return m_zonesModel; -} - -CalamaresUtils::Locale::CStringListModel* -Config::regionModel() const -{ - return m_regionModel; -} +Config::~Config() {} void -Config::setLocaleInfo( const QString& initialRegion, const QString& initialZone, const QString& localeGenPath ) +Config::setConfigurationMap( const QVariantMap& ) { - using namespace CalamaresUtils::Locale; - - cDebug() << "REGION MODEL SIZE" << initialRegion << initialZone; - auto* region = m_regionList.find< TZRegion >( initialRegion ); - if ( region && region->zones().find< TZZone >( initialZone ) ) - { - m_regionModel->setCurrentIndex( m_regionModel->indexOf( initialRegion ) ); - m_zonesModel->setList( region->zones() ); - m_zonesModel->setCurrentIndex( m_zonesModel->indexOf( initialZone ) ); - } - else - { - m_regionModel->setCurrentIndex( m_regionModel->indexOf( "America" ) ); - m_zonesModel->setList( - static_cast< const TZRegion* >( m_regionModel->item( m_regionModel->currentIndex() ) )->zones() ); - m_zonesModel->setCurrentIndex( m_zonesModel->indexOf( "New_York" ) ); - } - - // Some distros come with a meaningfully commented and easy to parse locale.gen, - // and others ship a separate file /usr/share/i18n/SUPPORTED with a clean list of - // supported locales. We first try that one, and if it doesn't exist, we fall back - // to parsing the lines from locale.gen - m_localeGenLines.clear(); - QFile supported( "/usr/share/i18n/SUPPORTED" ); - QByteArray ba; - - if ( supported.exists() && supported.open( QIODevice::ReadOnly | QIODevice::Text ) ) - { - ba = supported.readAll(); - supported.close(); - - const auto lines = ba.split( '\n' ); - for ( const QByteArray& line : lines ) - { - m_localeGenLines.append( QString::fromLatin1( line.simplified() ) ); - } - } - else - { - QFile localeGen( localeGenPath ); - if ( localeGen.open( QIODevice::ReadOnly | QIODevice::Text ) ) - { - ba = localeGen.readAll(); - localeGen.close(); - } - else - { - cWarning() << "Cannot open file" << localeGenPath - << ". Assuming the supported languages are already built into " - "the locale archive."; - QProcess localeA; - localeA.start( "locale", QStringList() << "-a" ); - localeA.waitForFinished(); - ba = localeA.readAllStandardOutput(); - } - const auto lines = ba.split( '\n' ); - for ( const QByteArray& line : lines ) - { - if ( line.startsWith( "## " ) || line.startsWith( "# " ) || line.simplified() == "#" ) - { - continue; - } - - QString lineString = QString::fromLatin1( line.simplified() ); - if ( lineString.startsWith( "#" ) ) - { - lineString.remove( '#' ); - } - lineString = lineString.simplified(); - - if ( lineString.isEmpty() ) - { - continue; - } - - m_localeGenLines.append( lineString ); - } - } - - if ( m_localeGenLines.isEmpty() ) - { - cWarning() << "cannot acquire a list of available locales." - << "The locale and localecfg modules will be broken as long as this " - "system does not provide" - << "\n\t " - << "* a well-formed" << supported.fileName() << "\n\tOR" - << "* a well-formed" - << ( localeGenPath.isEmpty() ? QLatin1String( "/etc/locale.gen" ) : localeGenPath ) << "\n\tOR" - << "* a complete pre-compiled locale-gen database which allows complete locale -a output."; - return; // something went wrong and there's nothing we can do about it. - } - - // Assuming we have a list of supported locales, we usually only want UTF-8 ones - // because it's not 1995. - for ( auto it = m_localeGenLines.begin(); it != m_localeGenLines.end(); ) - { - if ( !it->contains( "UTF-8", Qt::CaseInsensitive ) && !it->contains( "utf8", Qt::CaseInsensitive ) ) - { - it = m_localeGenLines.erase( it ); - } - else - { - ++it; - } - } - - // We strip " UTF-8" from "en_US.UTF-8 UTF-8" because it's redundant redundant. - for ( auto it = m_localeGenLines.begin(); it != m_localeGenLines.end(); ++it ) - { - if ( it->endsWith( " UTF-8" ) ) - { - it->chop( 6 ); - } - *it = it->simplified(); - } - updateGlobalStorage(); - updateLocaleLabels(); -} - -void -Config::updateGlobalLocale() -{ - auto* gs = Calamares::JobQueue::instance()->globalStorage(); - const QString bcp47 = m_selectedLocaleConfiguration.toBcp47(); - gs->insert( "locale", bcp47 ); -} - -void -Config::updateGlobalStorage() -{ - auto* gs = Calamares::JobQueue::instance()->globalStorage(); - - const auto* location = currentLocation(); - bool locationChanged = ( location->region() != gs->value( "locationRegion" ) ) - || ( location->zone() != gs->value( "locationZone" ) ); -#ifdef DEBUG_TIMEZONES - if ( locationChanged ) - { - cDebug() << "Location changed" << gs->value( "locationRegion" ) << ',' << gs->value( "locationZone" ) << "to" - << location->region() << ',' << location->zone(); - } -#endif - gs->insert( "locationRegion", location->region() ); - gs->insert( "locationZone", location->zone() ); - - updateGlobalLocale(); - - // If we're in chroot mode (normal install mode), then we immediately set the - // timezone on the live system. When debugging timezones, don't bother. -#ifndef DEBUG_TIMEZONES - if ( locationChanged && Calamares::Settings::instance()->doChroot() ) - { - QProcess::execute( "timedatectl", // depends on systemd - { "set-timezone", location->region() + '/' + location->zone() } ); - } -#endif - - // Preserve those settings that have been made explicit. - auto newLocale = guessLocaleConfiguration(); - if ( !m_selectedLocaleConfiguration.isEmpty() && m_selectedLocaleConfiguration.explicit_lang ) - { - newLocale.setLanguage( m_selectedLocaleConfiguration.language() ); - } - if ( !m_selectedLocaleConfiguration.isEmpty() && m_selectedLocaleConfiguration.explicit_lc ) - { - newLocale.lc_numeric = m_selectedLocaleConfiguration.lc_numeric; - newLocale.lc_time = m_selectedLocaleConfiguration.lc_time; - newLocale.lc_monetary = m_selectedLocaleConfiguration.lc_monetary; - newLocale.lc_paper = m_selectedLocaleConfiguration.lc_paper; - newLocale.lc_name = m_selectedLocaleConfiguration.lc_name; - newLocale.lc_address = m_selectedLocaleConfiguration.lc_address; - newLocale.lc_telephone = m_selectedLocaleConfiguration.lc_telephone; - newLocale.lc_measurement = m_selectedLocaleConfiguration.lc_measurement; - newLocale.lc_identification = m_selectedLocaleConfiguration.lc_identification; - } - newLocale.explicit_lang = m_selectedLocaleConfiguration.explicit_lang; - newLocale.explicit_lc = m_selectedLocaleConfiguration.explicit_lc; - - m_selectedLocaleConfiguration = newLocale; - updateLocaleLabels(); -} - -void -Config::updateLocaleLabels() -{ - LocaleConfiguration lc - = m_selectedLocaleConfiguration.isEmpty() ? guessLocaleConfiguration() : m_selectedLocaleConfiguration; - auto labels = prettyLocaleStatus( lc ); - emit prettyStatusChanged(); -} - - -std::pair< QString, QString > -Config::prettyLocaleStatus( const LocaleConfiguration& lc ) const -{ - using CalamaresUtils::Locale::Label; - - Label lang( lc.language(), Label::LabelFormat::AlwaysWithCountry ); - Label num( lc.lc_numeric, Label::LabelFormat::AlwaysWithCountry ); - - return std::make_pair< QString, QString >( - tr( "The system language will be set to %1." ).arg( lang.label() ), - tr( "The numbers and dates locale will be set to %1." ).arg( num.label() ) ); -} - -Calamares::JobList -Config::createJobs() -{ - QList< Calamares::job_ptr > list; - const CalamaresUtils::Locale::TZZone* location = currentLocation(); - - Calamares::Job* j = new SetTimezoneJob( location->region(), location->zone() ); - list.append( Calamares::job_ptr( j ) ); - - return list; -} - -LocaleConfiguration -Config::guessLocaleConfiguration() const -{ - return LocaleConfiguration::fromLanguageAndLocation( - QLocale().name(), m_localeGenLines, currentLocation() ? currentLocation()->country() : "" ); -} - -QMap< QString, QString > -Config::localesMap() -{ - return m_selectedLocaleConfiguration.isEmpty() ? guessLocaleConfiguration().toMap() - : m_selectedLocaleConfiguration.toMap(); -} - -QString -Config::prettyStatus() const -{ - QString status; - status += tr( "Set timezone to %1/%2.
" ) - .arg( m_regionModel->item( m_regionModel->currentIndex() )->tr() ) - .arg( m_zonesModel->item( m_zonesModel->currentIndex() )->tr() ); - - LocaleConfiguration lc - = m_selectedLocaleConfiguration.isEmpty() ? guessLocaleConfiguration() : m_selectedLocaleConfiguration; - auto labels = prettyLocaleStatus( lc ); - status += labels.first + "
"; - status += labels.second + "
"; - - return status; -} - - -const CalamaresUtils::Locale::TZZone* -Config::currentLocation() const -{ - return static_cast< const CalamaresUtils::Locale::TZZone* >( m_zonesModel->item( m_zonesModel->currentIndex() ) ); } diff --git a/src/modules/locale/Config.h b/src/modules/locale/Config.h index cfbed7bae..fcfc22a98 100644 --- a/src/modules/locale/Config.h +++ b/src/modules/locale/Config.h @@ -1,7 +1,8 @@ /* === This file is part of Calamares - === * - * Copyright 2019-2020, Adriaan de Groot - * Copyright 2020, Camilo Higuita + * SPDX-FileCopyrightText: 2020 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later + * License-Filename: LICENSE * * Calamares is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,66 +21,17 @@ #ifndef LOCALE_CONFIG_H #define LOCALE_CONFIG_H -#include "LocaleConfiguration.h" - -#include "Job.h" -#include "locale/TimeZone.h" - -#include #include -#include - class Config : public QObject { Q_OBJECT - Q_PROPERTY( CalamaresUtils::Locale::CStringListModel* zonesModel READ zonesModel CONSTANT FINAL ) - Q_PROPERTY( CalamaresUtils::Locale::CStringListModel* regionModel READ regionModel CONSTANT FINAL ) - Q_PROPERTY( QString prettyStatus READ prettyStatus NOTIFY prettyStatusChanged FINAL ) public: Config( QObject* parent = nullptr ); ~Config(); - CalamaresUtils::Locale::CStringListModel* regionModel() const; - CalamaresUtils::Locale::CStringListModel* zonesModel() const; - void setLocaleInfo( const QString& initialRegion, const QString& initialZone, const QString& localeGenPath ); - - Calamares::JobList createJobs(); - QMap< QString, QString > localesMap(); - QString prettyStatus() const; - -private: - CalamaresUtils::Locale::CStringPairList m_regionList; - CalamaresUtils::Locale::CStringListModel* m_regionModel; - CalamaresUtils::Locale::CStringListModel* m_zonesModel; - - LocaleConfiguration m_selectedLocaleConfiguration; - - QStringList m_localeGenLines; - int m_currentRegion = -1; - - bool m_blockTzWidgetSet; - - LocaleConfiguration guessLocaleConfiguration() const; - - // For the given locale config, return two strings describing - // the settings for language and numbers. - std::pair< QString, QString > prettyLocaleStatus( const LocaleConfiguration& ) const; - - /** @brief Update the GS *locale* key with the selected system language. - * - * This uses whatever is set in m_selectedLocaleConfiguration as the language, - * and writes it to GS *locale* key (as a string, in BCP47 format). - */ - void updateGlobalLocale(); - void updateGlobalStorage(); - void updateLocaleLabels(); - - const CalamaresUtils::Locale::TZZone* currentLocation() const; - -signals: - void prettyStatusChanged(); + void setConfigurationMap( const QVariantMap& ); }; diff --git a/src/modules/locale/LocaleViewStep.cpp b/src/modules/locale/LocaleViewStep.cpp index 3a8c37673..880f42a7d 100644 --- a/src/modules/locale/LocaleViewStep.cpp +++ b/src/modules/locale/LocaleViewStep.cpp @@ -34,7 +34,6 @@ #include #include -#include CALAMARES_PLUGIN_FACTORY_DEFINITION( LocaleViewStepFactory, registerPlugin< LocaleViewStep >(); ) @@ -45,6 +44,7 @@ LocaleViewStep::LocaleViewStep( QObject* parent ) , m_actualWidget( nullptr ) , m_nextEnabled( false ) , m_geoip( nullptr ) + , m_config( std::make_unique< Config >() ) { QBoxLayout* mainLayout = new QHBoxLayout; m_widget->setLayout( mainLayout ); @@ -221,6 +221,8 @@ LocaleViewStep::setConfigurationMap( const QVariantMap& configurationMap ) cWarning() << "GeoIP Style" << style << "is not recognized."; } } + + m_config->setConfigurationMap( configurationMap ); } Calamares::RequirementsList diff --git a/src/modules/locale/LocaleViewStep.h b/src/modules/locale/LocaleViewStep.h index 841aba97f..4e6c3d262 100644 --- a/src/modules/locale/LocaleViewStep.h +++ b/src/modules/locale/LocaleViewStep.h @@ -20,16 +20,14 @@ #ifndef LOCALEVIEWSTEP_H #define LOCALEVIEWSTEP_H +#include "Config.h" + +#include "DllMacro.h" #include "geoip/Handler.h" #include "geoip/Interface.h" #include "utils/PluginFactory.h" #include "viewpages/ViewStep.h" -#include "DllMacro.h" - -#include -#include - #include class LocalePage; @@ -79,6 +77,8 @@ private: Calamares::JobList m_jobs; std::unique_ptr< CalamaresUtils::GeoIP::Handler > m_geoip; + + std::unique_ptr< Config > m_config; }; CALAMARES_PLUGIN_FACTORY_DECLARATION( LocaleViewStepFactory ) From b6b5c449968c72c76ba60819f1f66054f46739b8 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 20 Jul 2020 12:55:07 +0200 Subject: [PATCH 006/113] [locale] Load supported locales in Config --- src/modules/locale/Config.cpp | 126 +++++++++++++++++++++++++++++++++- src/modules/locale/Config.h | 4 ++ 2 files changed, 129 insertions(+), 1 deletion(-) diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index ae8595734..d886f1eec 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -21,6 +21,124 @@ #include "Config.h" #include "utils/Logger.h" +#include "utils/Variant.h" + +#include +#include + +/** @brief Load supported locale keys + * + * If i18n/SUPPORTED exists, read the lines from that and return those + * as supported locales; otherwise, try the file at @p localeGenPath + * and get lines from that. Failing both, try the output of `locale -a`. + * + * This gives us a list of locale identifiers (e.g. en_US.UTF-8), which + * are not particularly human-readable. + * + * Only UTF-8 locales are returned (even if the system claims to support + * other, non-UTF-8, locales). + */ +static QStringList +loadLocales( const QString& localeGenPath ) +{ + QStringList localeGenLines; + + // Some distros come with a meaningfully commented and easy to parse locale.gen, + // and others ship a separate file /usr/share/i18n/SUPPORTED with a clean list of + // supported locales. We first try that one, and if it doesn't exist, we fall back + // to parsing the lines from locale.gen + localeGenLines.clear(); + QFile supported( "/usr/share/i18n/SUPPORTED" ); + QByteArray ba; + + if ( supported.exists() && supported.open( QIODevice::ReadOnly | QIODevice::Text ) ) + { + ba = supported.readAll(); + supported.close(); + + const auto lines = ba.split( '\n' ); + for ( const QByteArray& line : lines ) + { + localeGenLines.append( QString::fromLatin1( line.simplified() ) ); + } + } + else + { + QFile localeGen( localeGenPath ); + if ( localeGen.open( QIODevice::ReadOnly | QIODevice::Text ) ) + { + ba = localeGen.readAll(); + localeGen.close(); + } + else + { + cWarning() << "Cannot open file" << localeGenPath + << ". Assuming the supported languages are already built into " + "the locale archive."; + QProcess localeA; + localeA.start( "locale", QStringList() << "-a" ); + localeA.waitForFinished(); + ba = localeA.readAllStandardOutput(); + } + const auto lines = ba.split( '\n' ); + for ( const QByteArray& line : lines ) + { + if ( line.startsWith( "## " ) || line.startsWith( "# " ) || line.simplified() == "#" ) + { + continue; + } + + QString lineString = QString::fromLatin1( line.simplified() ); + if ( lineString.startsWith( "#" ) ) + { + lineString.remove( '#' ); + } + lineString = lineString.simplified(); + + if ( lineString.isEmpty() ) + { + continue; + } + + localeGenLines.append( lineString ); + } + } + + if ( localeGenLines.isEmpty() ) + { + cWarning() << "cannot acquire a list of available locales." + << "The locale and localecfg modules will be broken as long as this " + "system does not provide" + << "\n\t " + << "* a well-formed" << supported.fileName() << "\n\tOR" + << "* a well-formed" + << ( localeGenPath.isEmpty() ? QLatin1String( "/etc/locale.gen" ) : localeGenPath ) << "\n\tOR" + << "* a complete pre-compiled locale-gen database which allows complete locale -a output."; + return localeGenLines; // something went wrong and there's nothing we can do about it. + } + + // Assuming we have a list of supported locales, we usually only want UTF-8 ones + // because it's not 1995. + auto notUtf8 = []( const QString& s ) { + return !s.contains( "UTF-8", Qt::CaseInsensitive ) && !s.contains( "utf8", Qt::CaseInsensitive ); + }; + auto it = std::remove_if( localeGenLines.begin(), localeGenLines.end(), notUtf8 ); + localeGenLines.erase( it, localeGenLines.end() ); + + // We strip " UTF-8" from "en_US.UTF-8 UTF-8" because it's redundant redundant. + // Also simplify whitespace. + auto unredundant = []( QString& s ) { + if ( s.endsWith( " UTF-8" ) ) + { + s.chop( 6 ); + } + s = s.simplified(); + }; + std::for_each( localeGenLines.begin(), localeGenLines.end(), unredundant ); + + return localeGenLines; +} + Config::Config( QObject* parent ) : QObject( parent ) @@ -30,6 +148,12 @@ Config::Config( QObject* parent ) Config::~Config() {} void -Config::setConfigurationMap( const QVariantMap& ) +Config::setConfigurationMap( const QVariantMap& configurationMap ) { + QString localeGenPath = CalamaresUtils::getString( configurationMap, "localeGenPath" ); + if ( localeGenPath.isEmpty() ) + { + localeGenPath = QStringLiteral( "/etc/locale.gen" ); + } + m_localeGenLines = loadLocales( localeGenPath ); } diff --git a/src/modules/locale/Config.h b/src/modules/locale/Config.h index fcfc22a98..53e4d5561 100644 --- a/src/modules/locale/Config.h +++ b/src/modules/locale/Config.h @@ -32,6 +32,10 @@ public: ~Config(); void setConfigurationMap( const QVariantMap& ); + +private: + /// A list of supported locale identifiers (e.g. "en_US.UTF-8") + QStringList m_localeGenLines; }; From 338635146f792f448ab59448fc72d01a078634b5 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 20 Jul 2020 12:58:35 +0200 Subject: [PATCH 007/113] [locale] Hand the Config object also to the page --- src/modules/locale/LocalePage.cpp | 5 +++-- src/modules/locale/LocalePage.h | 7 ++++++- src/modules/locale/LocaleViewStep.cpp | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/modules/locale/LocalePage.cpp b/src/modules/locale/LocalePage.cpp index 53d97ea02..faeae7edc 100644 --- a/src/modules/locale/LocalePage.cpp +++ b/src/modules/locale/LocalePage.cpp @@ -19,6 +19,7 @@ #include "LocalePage.h" +#include "Config.h" #include "SetTimezoneJob.h" #include "timezonewidget/timezonewidget.h" @@ -26,7 +27,6 @@ #include "JobQueue.h" #include "LCLocaleDialog.h" #include "Settings.h" - #include "locale/Label.h" #include "locale/TimeZone.h" #include "utils/CalamaresUtilsGui.h" @@ -40,8 +40,9 @@ #include #include -LocalePage::LocalePage( QWidget* parent ) +LocalePage::LocalePage( Config* config, QWidget* parent ) : QWidget( parent ) + , m_config( config ) , m_regionList( CalamaresUtils::Locale::TZRegion::fromZoneTab() ) , m_regionModel( std::make_unique< CalamaresUtils::Locale::CStringListModel >( m_regionList ) ) , m_blockTzWidgetSet( false ) diff --git a/src/modules/locale/LocalePage.h b/src/modules/locale/LocalePage.h index f31d81fb6..1e2052223 100644 --- a/src/modules/locale/LocalePage.h +++ b/src/modules/locale/LocalePage.h @@ -32,13 +32,15 @@ class QComboBox; class QLabel; class QPushButton; + +class Config; class TimeZoneWidget; class LocalePage : public QWidget { Q_OBJECT public: - explicit LocalePage( QWidget* parent = nullptr ); + explicit LocalePage( class Config* config, QWidget* parent = nullptr ); virtual ~LocalePage(); void init( const QString& initialRegion, const QString& initialZone, const QString& localeGenPath ); @@ -54,6 +56,9 @@ public: private: LocaleConfiguration guessLocaleConfiguration() const; + /// @brief Non-owning pointer to the ViewStep's config + Config* m_config; + // For the given locale config, return two strings describing // the settings for language and numbers. std::pair< QString, QString > prettyLocaleStatus( const LocaleConfiguration& ) const; diff --git a/src/modules/locale/LocaleViewStep.cpp b/src/modules/locale/LocaleViewStep.cpp index 880f42a7d..173532bf8 100644 --- a/src/modules/locale/LocaleViewStep.cpp +++ b/src/modules/locale/LocaleViewStep.cpp @@ -68,7 +68,7 @@ LocaleViewStep::setUpPage() { if ( !m_actualWidget ) { - m_actualWidget = new LocalePage(); + m_actualWidget = new LocalePage( m_config.get() ); } m_actualWidget->init( m_startingTimezone.first, m_startingTimezone.second, m_localeGenPath ); m_widget->layout()->addWidget( m_actualWidget ); From 25ba1bb767ae2ae32d53caba820917cf02d20e4f Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 20 Jul 2020 13:22:38 +0200 Subject: [PATCH 008/113] [locale] Remove localeGenLines from page - the Config object took over loading of the string list - expose the list as a property - drop loading code from the page. --- src/modules/locale/Config.h | 4 ++ src/modules/locale/LocalePage.cpp | 100 ++------------------------ src/modules/locale/LocalePage.h | 4 +- src/modules/locale/LocaleViewStep.cpp | 2 +- 4 files changed, 10 insertions(+), 100 deletions(-) diff --git a/src/modules/locale/Config.h b/src/modules/locale/Config.h index 53e4d5561..34aa8b92f 100644 --- a/src/modules/locale/Config.h +++ b/src/modules/locale/Config.h @@ -26,6 +26,7 @@ class Config : public QObject { Q_OBJECT + Q_PROPERTY( const QStringList& supportedLocales READ supportedLocales CONSTANT FINAL ) public: Config( QObject* parent = nullptr ); @@ -33,6 +34,9 @@ public: void setConfigurationMap( const QVariantMap& ); +public Q_SLOTS: + const QStringList& supportedLocales() const { return m_localeGenLines; } + private: /// A list of supported locale identifiers (e.g. "en_US.UTF-8") QStringList m_localeGenLines; diff --git a/src/modules/locale/LocalePage.cpp b/src/modules/locale/LocalePage.cpp index faeae7edc..fa04d1058 100644 --- a/src/modules/locale/LocalePage.cpp +++ b/src/modules/locale/LocalePage.cpp @@ -138,7 +138,7 @@ LocalePage::updateLocaleLabels() } void -LocalePage::init( const QString& initialRegion, const QString& initialZone, const QString& localeGenPath ) +LocalePage::init( const QString& initialRegion, const QString& initialZone ) { using namespace CalamaresUtils::Locale; @@ -155,98 +155,6 @@ LocalePage::init( const QString& initialRegion, const QString& initialZone, cons m_tzWidget->setCurrentLocation( "America", "New_York" ); } - // Some distros come with a meaningfully commented and easy to parse locale.gen, - // and others ship a separate file /usr/share/i18n/SUPPORTED with a clean list of - // supported locales. We first try that one, and if it doesn't exist, we fall back - // to parsing the lines from locale.gen - m_localeGenLines.clear(); - QFile supported( "/usr/share/i18n/SUPPORTED" ); - QByteArray ba; - - if ( supported.exists() && supported.open( QIODevice::ReadOnly | QIODevice::Text ) ) - { - ba = supported.readAll(); - supported.close(); - - const auto lines = ba.split( '\n' ); - for ( const QByteArray& line : lines ) - { - m_localeGenLines.append( QString::fromLatin1( line.simplified() ) ); - } - } - else - { - QFile localeGen( localeGenPath ); - if ( localeGen.open( QIODevice::ReadOnly | QIODevice::Text ) ) - { - ba = localeGen.readAll(); - localeGen.close(); - } - else - { - cWarning() << "Cannot open file" << localeGenPath - << ". Assuming the supported languages are already built into " - "the locale archive."; - QProcess localeA; - localeA.start( "locale", QStringList() << "-a" ); - localeA.waitForFinished(); - ba = localeA.readAllStandardOutput(); - } - const auto lines = ba.split( '\n' ); - for ( const QByteArray& line : lines ) - { - if ( line.startsWith( "## " ) || line.startsWith( "# " ) || line.simplified() == "#" ) - { - continue; - } - - QString lineString = QString::fromLatin1( line.simplified() ); - if ( lineString.startsWith( "#" ) ) - { - lineString.remove( '#' ); - } - lineString = lineString.simplified(); - - if ( lineString.isEmpty() ) - { - continue; - } - - m_localeGenLines.append( lineString ); - } - } - - if ( m_localeGenLines.isEmpty() ) - { - cWarning() << "cannot acquire a list of available locales." - << "The locale and localecfg modules will be broken as long as this " - "system does not provide" - << "\n\t " - << "* a well-formed" << supported.fileName() << "\n\tOR" - << "* a well-formed" - << ( localeGenPath.isEmpty() ? QLatin1String( "/etc/locale.gen" ) : localeGenPath ) << "\n\tOR" - << "* a complete pre-compiled locale-gen database which allows complete locale -a output."; - return; // something went wrong and there's nothing we can do about it. - } - - // Assuming we have a list of supported locales, we usually only want UTF-8 ones - // because it's not 1995. - auto notUtf8 = []( const QString& s ) { - return !s.contains( "UTF-8", Qt::CaseInsensitive ) && !s.contains( "utf8", Qt::CaseInsensitive ); - }; - auto it = std::remove_if( m_localeGenLines.begin(), m_localeGenLines.end(), notUtf8 ); - m_localeGenLines.erase( it, m_localeGenLines.end() ); - - // We strip " UTF-8" from "en_US.UTF-8 UTF-8" because it's redundant redundant. - // Also simplify whitespace. - auto unredundant = []( QString& s ) { - if ( s.endsWith( " UTF-8" ) ) - { - s.chop( 6 ); - } - s = s.simplified(); - }; - std::for_each( m_localeGenLines.begin(), m_localeGenLines.end(), unredundant ); updateGlobalStorage(); } @@ -319,7 +227,7 @@ LocaleConfiguration LocalePage::guessLocaleConfiguration() const { return LocaleConfiguration::fromLanguageAndLocation( - QLocale().name(), m_localeGenLines, m_tzWidget->currentLocation()->country() ); + QLocale().name(), m_config->supportedLocales(), m_tzWidget->currentLocation()->country() ); } @@ -446,7 +354,7 @@ LocalePage::changeLocale() LCLocaleDialog* dlg = new LCLocaleDialog( m_selectedLocaleConfiguration.isEmpty() ? guessLocaleConfiguration().language() : m_selectedLocaleConfiguration.language(), - m_localeGenLines, + m_config->supportedLocales(), this ); dlg->exec(); if ( dlg->result() == QDialog::Accepted && !dlg->selectedLCLocale().isEmpty() ) @@ -467,7 +375,7 @@ LocalePage::changeFormats() LCLocaleDialog* dlg = new LCLocaleDialog( m_selectedLocaleConfiguration.isEmpty() ? guessLocaleConfiguration().lc_numeric : m_selectedLocaleConfiguration.lc_numeric, - m_localeGenLines, + m_config->supportedLocales(), this ); dlg->exec(); if ( dlg->result() == QDialog::Accepted && !dlg->selectedLCLocale().isEmpty() ) diff --git a/src/modules/locale/LocalePage.h b/src/modules/locale/LocalePage.h index 1e2052223..ea41f5233 100644 --- a/src/modules/locale/LocalePage.h +++ b/src/modules/locale/LocalePage.h @@ -43,7 +43,7 @@ public: explicit LocalePage( class Config* config, QWidget* parent = nullptr ); virtual ~LocalePage(); - void init( const QString& initialRegion, const QString& initialZone, const QString& localeGenPath ); + void init( const QString& initialRegion, const QString& initialZone ); QString prettyStatus() const; @@ -95,8 +95,6 @@ private: LocaleConfiguration m_selectedLocaleConfiguration; - QStringList m_localeGenLines; - bool m_blockTzWidgetSet; }; diff --git a/src/modules/locale/LocaleViewStep.cpp b/src/modules/locale/LocaleViewStep.cpp index 173532bf8..e35b5a112 100644 --- a/src/modules/locale/LocaleViewStep.cpp +++ b/src/modules/locale/LocaleViewStep.cpp @@ -70,7 +70,7 @@ LocaleViewStep::setUpPage() { m_actualWidget = new LocalePage( m_config.get() ); } - m_actualWidget->init( m_startingTimezone.first, m_startingTimezone.second, m_localeGenPath ); + m_actualWidget->init( m_startingTimezone.first, m_startingTimezone.second ); m_widget->layout()->addWidget( m_actualWidget ); ensureSize( m_actualWidget->sizeHint() ); From 931ce20f304075e737bfd44632752a0c852ccf18 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 20 Jul 2020 13:38:20 +0200 Subject: [PATCH 009/113] [locale] Reduce API surface - getLocationPosition doesn't need to be a method, since it calls out to a static function of TimeZoneImageList anyway. --- src/modules/locale/timezonewidget/timezonewidget.cpp | 7 +++++++ src/modules/locale/timezonewidget/timezonewidget.h | 5 ----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/modules/locale/timezonewidget/timezonewidget.cpp b/src/modules/locale/timezonewidget/timezonewidget.cpp index b81e0414c..05ee8f54d 100644 --- a/src/modules/locale/timezonewidget/timezonewidget.cpp +++ b/src/modules/locale/timezonewidget/timezonewidget.cpp @@ -34,6 +34,13 @@ #define ZONE_NAME QStringLiteral( "zone" ) #endif +static QPoint +getLocationPosition( const CalamaresUtils::Locale::TZZone* l ) +{ + return TimeZoneImageList::getLocationPosition( l->longitude(), l->latitude() ); +} + + TimeZoneWidget::TimeZoneWidget( QWidget* parent ) : QWidget( parent ) , timeZoneImages( TimeZoneImageList::fromQRC() ) diff --git a/src/modules/locale/timezonewidget/timezonewidget.h b/src/modules/locale/timezonewidget/timezonewidget.h index afebbfd7b..08a0491ee 100644 --- a/src/modules/locale/timezonewidget/timezonewidget.h +++ b/src/modules/locale/timezonewidget/timezonewidget.h @@ -53,11 +53,6 @@ private: TimeZoneImageList timeZoneImages; const TZZone* m_currentLocation = nullptr; // Not owned by me - QPoint getLocationPosition( const TZZone* l ) - { - return timeZoneImages.getLocationPosition( l->longitude(), l->latitude() ); - } - void paintEvent( QPaintEvent* event ); void mousePressEvent( QMouseEvent* event ); }; From 439f828d9b31a63841761a0b982bd9be6f73a910 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 20 Jul 2020 13:47:23 +0200 Subject: [PATCH 010/113] [locale] Document TZ widget --- .../locale/timezonewidget/timezonewidget.h | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/modules/locale/timezonewidget/timezonewidget.h b/src/modules/locale/timezonewidget/timezonewidget.h index 08a0491ee..7ef29d72d 100644 --- a/src/modules/locale/timezonewidget/timezonewidget.h +++ b/src/modules/locale/timezonewidget/timezonewidget.h @@ -31,6 +31,22 @@ #include #include +/** @brief The TimeZoneWidget shows a map and reports where clicks happen + * + * This widget shows a map (unspecified whether it's a whole world map + * or can show regionsvia some kind of internal state). Mouse clicks are + * translated into timezone locations (e.g. the zone for America/New_York). + * + * The current location can be changed programmatically, by name + * or through a pointer to a location. If a pointer is used, take care + * that the pointer is to a zone in the same model as used by the + * widget. + * + * When a location is chosen -- by mouse click or programmatically -- + * the locationChanged() signal is emitted with the new location. + * + * NOTE: the widget currently uses the globally cached TZRegion::fromZoneTab() + */ class TimeZoneWidget : public QWidget { Q_OBJECT @@ -39,10 +55,19 @@ public: explicit TimeZoneWidget( QWidget* parent = nullptr ); + /** @brief Sets a location by name + * + * @p region should be "America" or the like, while @p zone + * names a zone within that region. + */ void setCurrentLocation( QString region, QString zone ); + /** @brief Sets a location by pointer + * + * Pointer should be within the same model as the widget uses. + */ void setCurrentLocation( const TZZone* location ); - const TZZone* currentLocation() { return m_currentLocation; } + const TZZone* currentLocation() { return m_currentLocation; } signals: void locationChanged( const TZZone* location ); From 51b7ec875f7c952ab0bce1e616784006b709dd8f Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 20 Jul 2020 13:55:00 +0200 Subject: [PATCH 011/113] [locale] Don't need own copy of zones list --- src/modules/locale/LocalePage.cpp | 8 +++----- src/modules/locale/LocalePage.h | 1 - 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/modules/locale/LocalePage.cpp b/src/modules/locale/LocalePage.cpp index fa04d1058..9903b2693 100644 --- a/src/modules/locale/LocalePage.cpp +++ b/src/modules/locale/LocalePage.cpp @@ -43,8 +43,7 @@ LocalePage::LocalePage( Config* config, QWidget* parent ) : QWidget( parent ) , m_config( config ) - , m_regionList( CalamaresUtils::Locale::TZRegion::fromZoneTab() ) - , m_regionModel( std::make_unique< CalamaresUtils::Locale::CStringListModel >( m_regionList ) ) + , m_regionModel( std::make_unique< CalamaresUtils::Locale::CStringListModel >( CalamaresUtils::Locale::TZRegion::fromZoneTab() ) ) , m_blockTzWidgetSet( false ) { QBoxLayout* mainLayout = new QVBoxLayout; @@ -118,7 +117,6 @@ LocalePage::LocalePage( Config* config, QWidget* parent ) LocalePage::~LocalePage() { - qDeleteAll( m_regionList ); } @@ -145,7 +143,7 @@ LocalePage::init( const QString& initialRegion, const QString& initialZone ) m_regionCombo->setModel( m_regionModel.get() ); m_regionCombo->currentIndexChanged( m_regionCombo->currentIndex() ); - auto* region = m_regionList.find< TZRegion >( initialRegion ); + auto* region = CalamaresUtils::Locale::TZRegion::fromZoneTab().find< TZRegion >( initialRegion ); if ( region && region->zones().find< TZZone >( initialZone ) ) { m_tzWidget->setCurrentLocation( initialRegion, initialZone ); @@ -297,7 +295,7 @@ LocalePage::regionChanged( int currentIndex ) Q_UNUSED( currentIndex ) QString selectedRegion = m_regionCombo->currentData().toString(); - TZRegion* region = m_regionList.find< TZRegion >( selectedRegion ); + TZRegion* region = CalamaresUtils::Locale::TZRegion::fromZoneTab().find< TZRegion >( selectedRegion ); if ( !region ) { return; diff --git a/src/modules/locale/LocalePage.h b/src/modules/locale/LocalePage.h index ea41f5233..5d40f7fc8 100644 --- a/src/modules/locale/LocalePage.h +++ b/src/modules/locale/LocalePage.h @@ -79,7 +79,6 @@ private: void changeFormats(); // Dynamically, QList< TZRegion* > - CalamaresUtils::Locale::CStringPairList m_regionList; std::unique_ptr< CalamaresUtils::Locale::CStringListModel > m_regionModel; TimeZoneWidget* m_tzWidget; From e8282f27a3af0827d3993ba702612e5a500c0fc8 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 20 Jul 2020 14:06:21 +0200 Subject: [PATCH 012/113] Docs: update RELEASE.md with some GPG-info and remove old steps --- ci/RELEASE.md | 112 +++++++++++++++++++++++--------------------------- 1 file changed, 51 insertions(+), 61 deletions(-) diff --git a/ci/RELEASE.md b/ci/RELEASE.md index 524a67a65..7fa19c26a 100644 --- a/ci/RELEASE.md +++ b/ci/RELEASE.md @@ -1,21 +1,13 @@ # Calamares Release Process > Calamares releases are now rolling when-they-are-ready releases. -> Releases are made from *master* and tagged there. When, in future, +> Releases are made from *calamares* and tagged there. When, in future, > LTS releases resume, these steps may be edited again. > > Most things are automated through the release script [RELEASE.sh](RELEASE.sh) -## (0) A week in advance +## (1) Preparation -* Run [Coverity scan][coverity], fix what's relevant. The Coverity scan runs - automatically once a week on master. The badge is displayed on the - project front page and in the wiki. -* Build with clang -Weverything, fix what's relevant. - ``` - rm -rf build ; mkdir build ; cd build - CC=clang CXX=clang++ cmake .. && make - ``` * Make sure all tests pass. ``` make @@ -25,16 +17,6 @@ an additional environment variable to be set for some tests, which will destroy an attached disk. This is not always desirable. There are some sample config-files that are empty and which fail the config-tests. -* Notify [translators][transifex]. In the dashboard there is an *Announcements* - link that you can use to send a translation announcement. Note that regular - use of `txpush.sh` will notify translators as well of any changes. - -[coverity]: https://scan.coverity.com/projects/calamares-calamares?tab=overview -[transifex]: https://www.transifex.com/calamares/calamares/dashboard/ - - -## (1) Preparation - * Pull latest translations from Transifex. We only push / pull translations from master, so longer-lived branches (e.g. 3.1.x) don't get translation updates. This is to keep the translation workflow simple. The script @@ -47,13 +29,8 @@ fairly complete translations, or use `ci/txstats.py` for an automated suggestion. If there are changes, commit them. * Push the changes. -* Drop the RC variable to 0 in `CMakeLists.txt`, *CALAMARES_VERSION_RC*. -* Check `README.md` and the - [Coding Guide](https://github.com/calamares/calamares/wiki/Develop-Code), - make sure it's all still - relevant. Run `ci/calamaresstyle` to check the C++ code style. - Run pycodestyle on recently-modified Python modules, fix what makes sense. * Check defaults in `settings.conf` and other configuration files. +* Drop the RC variable to 0 in `CMakeLists.txt`, *CALAMARES_VERSION_RC*. * Edit `CHANGES` and set the date of the release. * Commit both. This is usually done with commit-message *Changes: pre-release housekeeping*. @@ -73,44 +50,16 @@ On success, it prints out a suitable signature- and SHA256 blurb for use in the release announcement. -### (2.1) Buld and Test +## (3) Release -* Build with gcc. If available, build again with Clang and double-check - any warnings Clang produces. -* Run the tests; `make test` in the build directory should have no - failures (or if there are, know why they are there). +Follow the instructions printed by the release script. -### (2.2) Tag - -* `git tag -s v1.1.0` Make sure the signing key is known in GitHub, so that the - tag is shown as a verified tag. Do not sign -rc tags. - You can use `make show-version` in the build directory to get the right - version number -- this will fail if you didn't follow step (1). - -### (2.3) Tarball - -* Create tarball: `git-archive-all -v calamares-1.1-rc1.tar.gz` or without - the helper script, - ``` - V=calamares-3.1.5 - git archive -o $V.tar.gz --prefix $V/ master - ``` - Double check that the tarball matches the version number. -* Test tarball (e.g. unpack somewhere else and run the tests from step 0). - - -## (3) Housekeeping - -* Generate MD5 and SHA256 checksums. -* Upload tarball. -* Announce on mailing list, notify packagers. -* Write release article. -* Publish tarball. -* Update download page. +* Push the tags. +* Create a new release on GitHub. +* Upload tarball and signature. * Publish release article on `calamares.io`. -* Publicize on social networks. -* Close associated milestone on GitHub if this is the actual release. -* Publish blog post. +* Close associated milestone on GitHub if it's entirely done. +* Update topic on #calamares IRC channel. ## (4) Post-Release @@ -133,3 +82,44 @@ This release contains contributions from (alphabetically by first name): ## Modules ## - No module changes yet ``` + +# Related Material + +> This section isn't directly related to any specific release, +> but bears on all releases. + +## GPG Key Maintainence + +Calamares uses GPG Keys for signing the tarballs and some commits +(tags, mostly). Calamares uses the **maintainer's** personal GPG +key for this. This section details some GPG activities that the +maintainer should do with those keys. + +- Signing sub-key. It's convenient to use a signing sub-key specifically + for the signing of Calamares. To do so, add a key to the private key. + It's recommended to use key expiry, and to update signing keys periodically. + - Run `gpg -K` to find the key ID of your personal GPG secret key. + - Run `gpg --edit-key ` to edit that personal GPG key. + - In gpg edit-mode, use `addkey`, then pick a key type that is *sign-only* + (e.g. type 4, *RSA (sign only)*), then pick a keysize (3072 seems ok + as of 2020) and set a key expiry time, (e.g. in 18 months time). + - After generation, the secret key information is printed again, now + including the new signing subkey: + ``` +ssb rsa3072/0xCFDDC96F12B1915C + created: 2020-07-11 expires: 2022-01-02 usage: S +``` +- Update the `RELEASE.sh` script with a new signing sub-key ID when a new + one is generated. Also announce the change of signing sub-key (e.g. on + the Calmares site or as part of a release announcement). + - Send the updated key to keyservers with `gpg --send-keys ` + - Optional: sanitize the keyring for use in development machines. + Export the current subkeys of the master key and keep **only** those + secret keys around. There is documentation + [here](https://blog.tinned-software.net/create-gnupg-key-with-sub-keys-to-sign-encrypt-authenticate/) + but be careful. + - Export the public key material with `gpg --export --armor `, + possibly also setting an output file. + - Upload that public key to the relevant GitHub profile. + - Upload that public key to the Calamares site. + From 88d1d255f6a61080ce45d86116c4b662bc78eca5 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 20 Jul 2020 16:13:08 +0200 Subject: [PATCH 013/113] [locale] Add regions & zones models to Config - The models are constant pointers, even if their contents aren't. - Make the top-level (region) model point to the global TZ list. --- src/modules/locale/Config.cpp | 3 +++ src/modules/locale/Config.h | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index d886f1eec..52378b0b4 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -142,6 +142,9 @@ loadLocales( const QString& localeGenPath ) Config::Config( QObject* parent ) : QObject( parent ) + , m_regionModel( std::make_unique< CalamaresUtils::Locale::CStringListModel >( + CalamaresUtils::Locale::TZRegion::fromZoneTab() ) ) + , m_zonesModel( std::make_unique< CalamaresUtils::Locale::CStringListModel >() ) { } diff --git a/src/modules/locale/Config.h b/src/modules/locale/Config.h index 34aa8b92f..a98ecc73d 100644 --- a/src/modules/locale/Config.h +++ b/src/modules/locale/Config.h @@ -21,12 +21,18 @@ #ifndef LOCALE_CONFIG_H #define LOCALE_CONFIG_H +#include "locale/TimeZone.h" + #include +#include + class Config : public QObject { Q_OBJECT Q_PROPERTY( const QStringList& supportedLocales READ supportedLocales CONSTANT FINAL ) + Q_PROPERTY( CalamaresUtils::Locale::CStringListModel* zonesModel READ zonesModel CONSTANT FINAL ) + Q_PROPERTY( CalamaresUtils::Locale::CStringListModel* regionModel READ regionModel CONSTANT FINAL ) public: Config( QObject* parent = nullptr ); @@ -36,10 +42,17 @@ public: public Q_SLOTS: const QStringList& supportedLocales() const { return m_localeGenLines; } + CalamaresUtils::Locale::CStringListModel* regionModel() const { return m_regionModel.get(); } + CalamaresUtils::Locale::CStringListModel* zonesModel() const { return m_zonesModel.get(); } private: /// A list of supported locale identifiers (e.g. "en_US.UTF-8") QStringList m_localeGenLines; + + /// The regions (America, Asia, Europe ..) + std::unique_ptr< CalamaresUtils::Locale::CStringListModel > m_regionModel; + /// The zones for the current region (e.g. America/New_York) + std::unique_ptr< CalamaresUtils::Locale::CStringListModel > m_zonesModel; }; From 4d5ff6d5c4d049ddf94921ffe49a48886ff79a98 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 20 Jul 2020 16:27:15 +0200 Subject: [PATCH 014/113] [locale] Make the Page use the region model from Config --- src/modules/locale/LocalePage.cpp | 3 +-- src/modules/locale/LocalePage.h | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/modules/locale/LocalePage.cpp b/src/modules/locale/LocalePage.cpp index 9903b2693..2d0832758 100644 --- a/src/modules/locale/LocalePage.cpp +++ b/src/modules/locale/LocalePage.cpp @@ -43,7 +43,6 @@ LocalePage::LocalePage( Config* config, QWidget* parent ) : QWidget( parent ) , m_config( config ) - , m_regionModel( std::make_unique< CalamaresUtils::Locale::CStringListModel >( CalamaresUtils::Locale::TZRegion::fromZoneTab() ) ) , m_blockTzWidgetSet( false ) { QBoxLayout* mainLayout = new QVBoxLayout; @@ -140,7 +139,7 @@ LocalePage::init( const QString& initialRegion, const QString& initialZone ) { using namespace CalamaresUtils::Locale; - m_regionCombo->setModel( m_regionModel.get() ); + m_regionCombo->setModel( m_config->regionModel() ); m_regionCombo->currentIndexChanged( m_regionCombo->currentIndex() ); auto* region = CalamaresUtils::Locale::TZRegion::fromZoneTab().find< TZRegion >( initialRegion ); diff --git a/src/modules/locale/LocalePage.h b/src/modules/locale/LocalePage.h index 5d40f7fc8..6c7fed1d6 100644 --- a/src/modules/locale/LocalePage.h +++ b/src/modules/locale/LocalePage.h @@ -78,9 +78,6 @@ private: void changeLocale(); void changeFormats(); - // Dynamically, QList< TZRegion* > - std::unique_ptr< CalamaresUtils::Locale::CStringListModel > m_regionModel; - TimeZoneWidget* m_tzWidget; QComboBox* m_regionCombo; QComboBox* m_zoneCombo; From f0cac7d6692c472931f4224de95540d402077478 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 20 Jul 2020 16:54:44 +0200 Subject: [PATCH 015/113] [locale] Hook tz widget up to the Config's data --- src/modules/locale/Config.cpp | 16 ++++++++++++++-- src/modules/locale/Config.h | 3 +++ src/modules/locale/LocalePage.cpp | 2 +- .../locale/timezonewidget/timezonewidget.cpp | 8 ++++---- .../locale/timezonewidget/timezonewidget.h | 4 +++- 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index 52378b0b4..39ae8732b 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -139,17 +139,29 @@ loadLocales( const QString& localeGenPath ) return localeGenLines; } +static inline const CalamaresUtils::Locale::CStringPairList& +timezoneData() +{ + return CalamaresUtils::Locale::TZRegion::fromZoneTab(); +} + Config::Config( QObject* parent ) : QObject( parent ) - , m_regionModel( std::make_unique< CalamaresUtils::Locale::CStringListModel >( - CalamaresUtils::Locale::TZRegion::fromZoneTab() ) ) + , m_regionModel( std::make_unique< CalamaresUtils::Locale::CStringListModel >( ::timezoneData() ) ) , m_zonesModel( std::make_unique< CalamaresUtils::Locale::CStringListModel >() ) { } Config::~Config() {} +const CalamaresUtils::Locale::CStringPairList& +Config::timezoneData() const +{ + return ::timezoneData(); +} + + void Config::setConfigurationMap( const QVariantMap& configurationMap ) { diff --git a/src/modules/locale/Config.h b/src/modules/locale/Config.h index a98ecc73d..4f8c9bc64 100644 --- a/src/modules/locale/Config.h +++ b/src/modules/locale/Config.h @@ -33,6 +33,7 @@ class Config : public QObject Q_PROPERTY( const QStringList& supportedLocales READ supportedLocales CONSTANT FINAL ) Q_PROPERTY( CalamaresUtils::Locale::CStringListModel* zonesModel READ zonesModel CONSTANT FINAL ) Q_PROPERTY( CalamaresUtils::Locale::CStringListModel* regionModel READ regionModel CONSTANT FINAL ) + Q_PROPERTY( const CalamaresUtils::Locale::CStringPairList& timezoneData READ timezoneData CONSTANT FINAL ) public: Config( QObject* parent = nullptr ); @@ -44,6 +45,8 @@ public Q_SLOTS: const QStringList& supportedLocales() const { return m_localeGenLines; } CalamaresUtils::Locale::CStringListModel* regionModel() const { return m_regionModel.get(); } CalamaresUtils::Locale::CStringListModel* zonesModel() const { return m_zonesModel.get(); } + // Underlying data for the models + const CalamaresUtils::Locale::CStringPairList& timezoneData() const; private: /// A list of supported locale identifiers (e.g. "en_US.UTF-8") diff --git a/src/modules/locale/LocalePage.cpp b/src/modules/locale/LocalePage.cpp index 2d0832758..2a4212fca 100644 --- a/src/modules/locale/LocalePage.cpp +++ b/src/modules/locale/LocalePage.cpp @@ -48,7 +48,7 @@ LocalePage::LocalePage( Config* config, QWidget* parent ) QBoxLayout* mainLayout = new QVBoxLayout; QBoxLayout* tzwLayout = new QHBoxLayout; - m_tzWidget = new TimeZoneWidget( this ); + m_tzWidget = new TimeZoneWidget( config->timezoneData(), this ); tzwLayout->addStretch(); tzwLayout->addWidget( m_tzWidget ); tzwLayout->addStretch(); diff --git a/src/modules/locale/timezonewidget/timezonewidget.cpp b/src/modules/locale/timezonewidget/timezonewidget.cpp index 05ee8f54d..149ad6590 100644 --- a/src/modules/locale/timezonewidget/timezonewidget.cpp +++ b/src/modules/locale/timezonewidget/timezonewidget.cpp @@ -41,9 +41,10 @@ getLocationPosition( const CalamaresUtils::Locale::TZZone* l ) } -TimeZoneWidget::TimeZoneWidget( QWidget* parent ) +TimeZoneWidget::TimeZoneWidget( const CalamaresUtils::Locale::CStringPairList& zones, QWidget* parent ) : QWidget( parent ) , timeZoneImages( TimeZoneImageList::fromQRC() ) + , m_zonesData( zones ) { setMouseTracking( false ); setCursor( Qt::PointingHandCursor ); @@ -67,8 +68,7 @@ void TimeZoneWidget::setCurrentLocation( QString regionName, QString zoneName ) { using namespace CalamaresUtils::Locale; - const auto& regions = TZRegion::fromZoneTab(); - auto* region = regions.find< TZRegion >( regionName ); + auto* region = m_zonesData.find< TZRegion >( regionName ); if ( !region ) { return; @@ -201,7 +201,7 @@ TimeZoneWidget::mousePressEvent( QMouseEvent* event ) using namespace CalamaresUtils::Locale; const TZZone* closest = nullptr; - for ( const auto* region_p : TZRegion::fromZoneTab() ) + for ( const auto* region_p : m_zonesData ) { const auto* region = dynamic_cast< const TZRegion* >( region_p ); if ( region ) diff --git a/src/modules/locale/timezonewidget/timezonewidget.h b/src/modules/locale/timezonewidget/timezonewidget.h index 7ef29d72d..c070b4cd4 100644 --- a/src/modules/locale/timezonewidget/timezonewidget.h +++ b/src/modules/locale/timezonewidget/timezonewidget.h @@ -53,7 +53,7 @@ class TimeZoneWidget : public QWidget public: using TZZone = CalamaresUtils::Locale::TZZone; - explicit TimeZoneWidget( QWidget* parent = nullptr ); + explicit TimeZoneWidget( const CalamaresUtils::Locale::CStringPairList& zones, QWidget* parent = nullptr ); /** @brief Sets a location by name * @@ -76,6 +76,8 @@ private: QFont font; QImage background, pin, currentZoneImage; TimeZoneImageList timeZoneImages; + + const CalamaresUtils::Locale::CStringPairList& m_zonesData; const TZZone* m_currentLocation = nullptr; // Not owned by me void paintEvent( QPaintEvent* event ); From 8c21b5985396af73dc51f1c8b486f3553cdde409 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 20 Jul 2020 18:13:33 +0200 Subject: [PATCH 016/113] [locale] Remove unused localegen (moved to Config earlier) --- src/modules/locale/LocaleViewStep.cpp | 6 ------ src/modules/locale/LocaleViewStep.h | 5 ++--- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/modules/locale/LocaleViewStep.cpp b/src/modules/locale/LocaleViewStep.cpp index e35b5a112..d71523da1 100644 --- a/src/modules/locale/LocaleViewStep.cpp +++ b/src/modules/locale/LocaleViewStep.cpp @@ -201,12 +201,6 @@ LocaleViewStep::setConfigurationMap( const QVariantMap& configurationMap ) = CalamaresUtils::GeoIP::RegionZonePair( QStringLiteral( "America" ), QStringLiteral( "New_York" ) ); } - m_localeGenPath = CalamaresUtils::getString( configurationMap, "localeGenPath" ); - if ( m_localeGenPath.isEmpty() ) - { - m_localeGenPath = QStringLiteral( "/etc/locale.gen" ); - } - bool ok = false; QVariantMap geoip = CalamaresUtils::getSubMap( configurationMap, "geoip", ok ); if ( ok ) diff --git a/src/modules/locale/LocaleViewStep.h b/src/modules/locale/LocaleViewStep.h index 4e6c3d262..24764b172 100644 --- a/src/modules/locale/LocaleViewStep.h +++ b/src/modules/locale/LocaleViewStep.h @@ -72,10 +72,9 @@ private: bool m_nextEnabled; QString m_prettyStatus; - CalamaresUtils::GeoIP::RegionZonePair m_startingTimezone; - QString m_localeGenPath; - Calamares::JobList m_jobs; + + CalamaresUtils::GeoIP::RegionZonePair m_startingTimezone; std::unique_ptr< CalamaresUtils::GeoIP::Handler > m_geoip; std::unique_ptr< Config > m_config; From 5a6a9a0d45d95ebef47019a6632effeead0cd67b Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 20 Jul 2020 22:21:29 +0200 Subject: [PATCH 017/113] [locale] Move job-creation to Config - since Config knows what settings there are, it should create the jobs to run later -- not the Page. - this doesn't work yet, because the Config does **not** know what the selected timezone is yet. --- src/modules/locale/Config.cpp | 17 +++++++++++++++++ src/modules/locale/Config.h | 2 ++ src/modules/locale/LocalePage.cpp | 17 +---------------- src/modules/locale/LocalePage.h | 2 -- src/modules/locale/LocaleViewStep.cpp | 2 +- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index 39ae8732b..2e0eb8173 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -20,6 +20,8 @@ #include "Config.h" +#include "SetTimezoneJob.h" + #include "utils/Logger.h" #include "utils/Variant.h" @@ -172,3 +174,18 @@ Config::setConfigurationMap( const QVariantMap& configurationMap ) } m_localeGenLines = loadLocales( localeGenPath ); } + +Calamares::JobList +Config::createJobs() +{ + Calamares::JobList list; + const CalamaresUtils::Locale::TZZone* location = nullptr; // TODO: m_tzWidget->currentLocation(); + + if ( location ) + { + Calamares::Job* j = new SetTimezoneJob( location->region(), location->zone() ); + list.append( Calamares::job_ptr( j ) ); + } + + return list; +} diff --git a/src/modules/locale/Config.h b/src/modules/locale/Config.h index 4f8c9bc64..8e1f29cf7 100644 --- a/src/modules/locale/Config.h +++ b/src/modules/locale/Config.h @@ -21,6 +21,7 @@ #ifndef LOCALE_CONFIG_H #define LOCALE_CONFIG_H +#include "Job.h" #include "locale/TimeZone.h" #include @@ -40,6 +41,7 @@ public: ~Config(); void setConfigurationMap( const QVariantMap& ); + Calamares::JobList createJobs(); public Q_SLOTS: const QStringList& supportedLocales() const { return m_localeGenLines; } diff --git a/src/modules/locale/LocalePage.cpp b/src/modules/locale/LocalePage.cpp index 2a4212fca..b3fa8e8e9 100644 --- a/src/modules/locale/LocalePage.cpp +++ b/src/modules/locale/LocalePage.cpp @@ -114,9 +114,7 @@ LocalePage::LocalePage( Config* config, QWidget* parent ) } -LocalePage::~LocalePage() -{ -} +LocalePage::~LocalePage() {} void @@ -185,19 +183,6 @@ LocalePage::prettyStatus() const } -Calamares::JobList -LocalePage::createJobs() -{ - QList< Calamares::job_ptr > list; - const CalamaresUtils::Locale::TZZone* location = m_tzWidget->currentLocation(); - - Calamares::Job* j = new SetTimezoneJob( location->region(), location->zone() ); - list.append( Calamares::job_ptr( j ) ); - - return list; -} - - QMap< QString, QString > LocalePage::localesMap() { diff --git a/src/modules/locale/LocalePage.h b/src/modules/locale/LocalePage.h index 6c7fed1d6..c8312309e 100644 --- a/src/modules/locale/LocalePage.h +++ b/src/modules/locale/LocalePage.h @@ -47,8 +47,6 @@ public: QString prettyStatus() const; - Calamares::JobList createJobs(); - QMap< QString, QString > localesMap(); void onActivate(); diff --git a/src/modules/locale/LocaleViewStep.cpp b/src/modules/locale/LocaleViewStep.cpp index d71523da1..81162d843 100644 --- a/src/modules/locale/LocaleViewStep.cpp +++ b/src/modules/locale/LocaleViewStep.cpp @@ -166,7 +166,7 @@ LocaleViewStep::onLeave() { if ( m_actualWidget ) { - m_jobs = m_actualWidget->createJobs(); + m_jobs = m_config->createJobs(); m_prettyStatus = m_actualWidget->prettyStatus(); auto map = m_actualWidget->localesMap(); From 726f88218525b7fa91dca8410b372d2d15be99b4 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 20 Jul 2020 23:06:12 +0200 Subject: [PATCH 018/113] [locale] Move current-location to Config --- src/modules/locale/Config.cpp | 25 ++++++++++++++++++ src/modules/locale/Config.h | 22 ++++++++++++++++ src/modules/locale/LocalePage.cpp | 26 ++++++++----------- .../locale/timezonewidget/timezonewidget.cpp | 20 +++----------- .../locale/timezonewidget/timezonewidget.h | 10 ++----- 5 files changed, 63 insertions(+), 40 deletions(-) diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index 2e0eb8173..5a97339a8 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -163,6 +163,31 @@ Config::timezoneData() const return ::timezoneData(); } +void +Config::setCurrentLocation( const QString& regionName, const QString& zoneName ) +{ + using namespace CalamaresUtils::Locale; + auto* region = timezoneData().find< TZRegion >( regionName ); + auto* zone = region ? region->zones().find< TZZone >( zoneName ) : nullptr; + if ( zone ) + { + setCurrentLocation( zone ); + } + else + { + setCurrentLocation( QStringLiteral("America"), QStringLiteral("New_York") ); + } +} + +void +Config::setCurrentLocation( const CalamaresUtils::Locale::TZZone* location ) +{ + if ( location != m_currentLocation ) + { + m_currentLocation = location; + emit currentLocationChanged( m_currentLocation ); + } +} void Config::setConfigurationMap( const QVariantMap& configurationMap ) diff --git a/src/modules/locale/Config.h b/src/modules/locale/Config.h index 8e1f29cf7..44e7c9142 100644 --- a/src/modules/locale/Config.h +++ b/src/modules/locale/Config.h @@ -35,6 +35,7 @@ class Config : public QObject Q_PROPERTY( CalamaresUtils::Locale::CStringListModel* zonesModel READ zonesModel CONSTANT FINAL ) Q_PROPERTY( CalamaresUtils::Locale::CStringListModel* regionModel READ regionModel CONSTANT FINAL ) Q_PROPERTY( const CalamaresUtils::Locale::CStringPairList& timezoneData READ timezoneData CONSTANT FINAL ) + Q_PROPERTY( const CalamaresUtils::Locale::TZZone* currentLocation READ currentLocation WRITE setCurrentLocation NOTIFY currentLocationChanged ) public: Config( QObject* parent = nullptr ); @@ -50,6 +51,23 @@ public Q_SLOTS: // Underlying data for the models const CalamaresUtils::Locale::CStringPairList& timezoneData() const; + /** @brief Sets a location by name + * + * @p region should be "America" or the like, while @p zone + * names a zone within that region. + */ + void setCurrentLocation( const QString& region, const QString& zone ); + /** @brief Sets a location by pointer + * + * Pointer should be within the same model as the widget uses. + */ + void setCurrentLocation( const CalamaresUtils::Locale::TZZone* location ); + + const CalamaresUtils::Locale::TZZone* currentLocation() const { return m_currentLocation; } + +signals: + void currentLocationChanged( const CalamaresUtils::Locale::TZZone* location ); + private: /// A list of supported locale identifiers (e.g. "en_US.UTF-8") QStringList m_localeGenLines; @@ -58,6 +76,10 @@ private: std::unique_ptr< CalamaresUtils::Locale::CStringListModel > m_regionModel; /// The zones for the current region (e.g. America/New_York) std::unique_ptr< CalamaresUtils::Locale::CStringListModel > m_zonesModel; + + /// The location, points into the timezone data + const CalamaresUtils::Locale::TZZone* m_currentLocation = nullptr; + }; diff --git a/src/modules/locale/LocalePage.cpp b/src/modules/locale/LocalePage.cpp index b3fa8e8e9..d4d24afef 100644 --- a/src/modules/locale/LocalePage.cpp +++ b/src/modules/locale/LocalePage.cpp @@ -104,9 +104,13 @@ LocalePage::LocalePage( Config* config, QWidget* parent ) setMinimumWidth( m_tzWidget->width() ); setLayout( mainLayout ); + connect( config, &Config::currentLocationChanged, m_tzWidget, &TimeZoneWidget::setCurrentLocation ); + connect( config, &Config::currentLocationChanged, this, &LocalePage::locationChanged ); + connect( m_tzWidget, &TimeZoneWidget::locationChanged, config, QOverload< const CalamaresUtils::Locale::TZZone* >::of( &Config::setCurrentLocation ) ); + connect( m_regionCombo, QOverload< int >::of( &QComboBox::currentIndexChanged ), this, &LocalePage::regionChanged ); connect( m_zoneCombo, QOverload< int >::of( &QComboBox::currentIndexChanged ), this, &LocalePage::zoneChanged ); - connect( m_tzWidget, &TimeZoneWidget::locationChanged, this, &LocalePage::locationChanged ); + connect( m_localeChangeButton, &QPushButton::clicked, this, &LocalePage::changeLocale ); connect( m_formatsChangeButton, &QPushButton::clicked, this, &LocalePage::changeFormats ); @@ -140,16 +144,7 @@ LocalePage::init( const QString& initialRegion, const QString& initialZone ) m_regionCombo->setModel( m_config->regionModel() ); m_regionCombo->currentIndexChanged( m_regionCombo->currentIndex() ); - auto* region = CalamaresUtils::Locale::TZRegion::fromZoneTab().find< TZRegion >( initialRegion ); - if ( region && region->zones().find< TZZone >( initialZone ) ) - { - m_tzWidget->setCurrentLocation( initialRegion, initialZone ); - } - else - { - m_tzWidget->setCurrentLocation( "America", "New_York" ); - } - + m_config->setCurrentLocation( initialRegion, initialZone ); updateGlobalStorage(); } @@ -209,7 +204,7 @@ LocaleConfiguration LocalePage::guessLocaleConfiguration() const { return LocaleConfiguration::fromLanguageAndLocation( - QLocale().name(), m_config->supportedLocales(), m_tzWidget->currentLocation()->country() ); + QLocale().name(), m_config->supportedLocales(), m_config->currentLocation()->country() ); } @@ -227,7 +222,7 @@ LocalePage::updateGlobalStorage() { auto* gs = Calamares::JobQueue::instance()->globalStorage(); - const auto* location = m_tzWidget->currentLocation(); + const auto* location = m_config->currentLocation(); bool locationChanged = ( location->region() != gs->value( "locationRegion" ) ) || ( location->zone() != gs->value( "locationZone" ) ); @@ -296,9 +291,10 @@ LocalePage::zoneChanged( int currentIndex ) { Q_UNUSED( currentIndex ) if ( !m_blockTzWidgetSet ) - m_tzWidget->setCurrentLocation( m_regionCombo->currentData().toString(), + { + m_config->setCurrentLocation( m_regionCombo->currentData().toString(), m_zoneCombo->currentData().toString() ); - + } updateGlobalStorage(); } diff --git a/src/modules/locale/timezonewidget/timezonewidget.cpp b/src/modules/locale/timezonewidget/timezonewidget.cpp index 149ad6590..df1142e17 100644 --- a/src/modules/locale/timezonewidget/timezonewidget.cpp +++ b/src/modules/locale/timezonewidget/timezonewidget.cpp @@ -65,26 +65,13 @@ TimeZoneWidget::TimeZoneWidget( const CalamaresUtils::Locale::CStringPairList& z void -TimeZoneWidget::setCurrentLocation( QString regionName, QString zoneName ) +TimeZoneWidget::setCurrentLocation( const CalamaresUtils::Locale::TZZone* location ) { - using namespace CalamaresUtils::Locale; - auto* region = m_zonesData.find< TZRegion >( regionName ); - if ( !region ) + if ( location == m_currentLocation ) { return; } - auto* zone = region->zones().find< TZZone >( zoneName ); - if ( zone ) - { - setCurrentLocation( zone ); - } -} - - -void -TimeZoneWidget::setCurrentLocation( const CalamaresUtils::Locale::TZZone* location ) -{ m_currentLocation = location; // Set zone @@ -100,7 +87,6 @@ TimeZoneWidget::setCurrentLocation( const CalamaresUtils::Locale::TZZone* locati // Repaint widget repaint(); - emit locationChanged( m_currentLocation ); } @@ -229,6 +215,6 @@ TimeZoneWidget::mousePressEvent( QMouseEvent* event ) // Set zone image and repaint widget setCurrentLocation( closest ); // Emit signal - emit locationChanged( m_currentLocation ); + emit locationChanged( closest ); } } diff --git a/src/modules/locale/timezonewidget/timezonewidget.h b/src/modules/locale/timezonewidget/timezonewidget.h index c070b4cd4..6bb94c0dd 100644 --- a/src/modules/locale/timezonewidget/timezonewidget.h +++ b/src/modules/locale/timezonewidget/timezonewidget.h @@ -55,21 +55,15 @@ public: explicit TimeZoneWidget( const CalamaresUtils::Locale::CStringPairList& zones, QWidget* parent = nullptr ); - /** @brief Sets a location by name - * - * @p region should be "America" or the like, while @p zone - * names a zone within that region. - */ - void setCurrentLocation( QString region, QString zone ); +public Q_SLOTS: /** @brief Sets a location by pointer * * Pointer should be within the same model as the widget uses. */ void setCurrentLocation( const TZZone* location ); - const TZZone* currentLocation() { return m_currentLocation; } - signals: + /** @brief The location has changed by mouse click */ void locationChanged( const TZZone* location ); private: From 98f912f80a59a0e3b61a902d0771e9f05c66833d Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 21 Jul 2020 00:11:16 +0200 Subject: [PATCH 019/113] [locale] Drop LocalePage:;init - setting the initial location is something the Config-object should do - setting up the combo-boxes can be done in the constructor --- src/modules/locale/LocalePage.cpp | 17 ++++------------- src/modules/locale/LocalePage.h | 2 -- src/modules/locale/LocaleViewStep.cpp | 2 +- 3 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/modules/locale/LocalePage.cpp b/src/modules/locale/LocalePage.cpp index d4d24afef..f0a2418ac 100644 --- a/src/modules/locale/LocalePage.cpp +++ b/src/modules/locale/LocalePage.cpp @@ -115,6 +115,10 @@ LocalePage::LocalePage( Config* config, QWidget* parent ) connect( m_formatsChangeButton, &QPushButton::clicked, this, &LocalePage::changeFormats ); CALAMARES_RETRANSLATE_SLOT( &LocalePage::updateLocaleLabels ) + + m_regionCombo->setModel( m_config->regionModel() ); + m_regionCombo->currentIndexChanged( m_regionCombo->currentIndex() ); + updateGlobalStorage(); } @@ -136,19 +140,6 @@ LocalePage::updateLocaleLabels() m_formatsLabel->setText( labels.second ); } -void -LocalePage::init( const QString& initialRegion, const QString& initialZone ) -{ - using namespace CalamaresUtils::Locale; - - m_regionCombo->setModel( m_config->regionModel() ); - m_regionCombo->currentIndexChanged( m_regionCombo->currentIndex() ); - - m_config->setCurrentLocation( initialRegion, initialZone ); - - updateGlobalStorage(); -} - std::pair< QString, QString > LocalePage::prettyLocaleStatus( const LocaleConfiguration& lc ) const { diff --git a/src/modules/locale/LocalePage.h b/src/modules/locale/LocalePage.h index c8312309e..3d4b1d03f 100644 --- a/src/modules/locale/LocalePage.h +++ b/src/modules/locale/LocalePage.h @@ -43,8 +43,6 @@ public: explicit LocalePage( class Config* config, QWidget* parent = nullptr ); virtual ~LocalePage(); - void init( const QString& initialRegion, const QString& initialZone ); - QString prettyStatus() const; QMap< QString, QString > localesMap(); diff --git a/src/modules/locale/LocaleViewStep.cpp b/src/modules/locale/LocaleViewStep.cpp index 81162d843..e17c7b004 100644 --- a/src/modules/locale/LocaleViewStep.cpp +++ b/src/modules/locale/LocaleViewStep.cpp @@ -70,7 +70,7 @@ LocaleViewStep::setUpPage() { m_actualWidget = new LocalePage( m_config.get() ); } - m_actualWidget->init( m_startingTimezone.first, m_startingTimezone.second ); + m_config->setCurrentLocation( m_startingTimezone.first, m_startingTimezone.second ); m_widget->layout()->addWidget( m_actualWidget ); ensureSize( m_actualWidget->sizeHint() ); From 0645a46b42da350ed25d08d65ade6bdb110583db Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 21 Jul 2020 00:21:16 +0200 Subject: [PATCH 020/113] [libcalamares] Expand RAII conveniences --- src/libcalamares/utils/RAII.h | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/libcalamares/utils/RAII.h b/src/libcalamares/utils/RAII.h index dae85e84a..28e57ff9e 100644 --- a/src/libcalamares/utils/RAII.h +++ b/src/libcalamares/utils/RAII.h @@ -1,5 +1,5 @@ /* === This file is part of Calamares - === - * + * * SPDX-FileCopyrightText: 2020 Adriaan de Groot * * Calamares is free software: you can redistribute it and/or modify @@ -24,6 +24,7 @@ #define UTILS_RAII_H #include +#include #include @@ -44,4 +45,21 @@ struct cqDeleter } }; +/// @brief Sets a bool to @p value and resets to !value on destruction +template < bool value > +struct cBoolSetter +{ + bool& m_b; + + cBoolSetter( bool& b ) + : m_b( b ) + { + m_b = value; + } + ~cBoolSetter() { m_b = !value; } +}; + +/// @brief Blocks signals on a QObject until destruction +using cSignalBlocker = QSignalBlocker; + #endif From 81520bbbf9c60e21329dd3d38cb10804b3486bd3 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 21 Jul 2020 00:21:42 +0200 Subject: [PATCH 021/113] [locale] Chase RAII conveniences - several early-return paths would leave the TZ widget blocked - use the zones data from config --- src/modules/locale/LocalePage.cpp | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/modules/locale/LocalePage.cpp b/src/modules/locale/LocalePage.cpp index f0a2418ac..abb4b63bc 100644 --- a/src/modules/locale/LocalePage.cpp +++ b/src/modules/locale/LocalePage.cpp @@ -31,6 +31,7 @@ #include "locale/TimeZone.h" #include "utils/CalamaresUtilsGui.h" #include "utils/Logger.h" +#include "utils/RAII.h" #include "utils/Retranslator.h" #include @@ -106,7 +107,10 @@ LocalePage::LocalePage( Config* config, QWidget* parent ) connect( config, &Config::currentLocationChanged, m_tzWidget, &TimeZoneWidget::setCurrentLocation ); connect( config, &Config::currentLocationChanged, this, &LocalePage::locationChanged ); - connect( m_tzWidget, &TimeZoneWidget::locationChanged, config, QOverload< const CalamaresUtils::Locale::TZZone* >::of( &Config::setCurrentLocation ) ); + connect( m_tzWidget, + &TimeZoneWidget::locationChanged, + config, + QOverload< const CalamaresUtils::Locale::TZZone* >::of( &Config::setCurrentLocation ) ); connect( m_regionCombo, QOverload< int >::of( &QComboBox::currentIndexChanged ), this, &LocalePage::regionChanged ); connect( m_zoneCombo, QOverload< int >::of( &QComboBox::currentIndexChanged ), this, &LocalePage::zoneChanged ); @@ -265,15 +269,17 @@ LocalePage::regionChanged( int currentIndex ) Q_UNUSED( currentIndex ) QString selectedRegion = m_regionCombo->currentData().toString(); - TZRegion* region = CalamaresUtils::Locale::TZRegion::fromZoneTab().find< TZRegion >( selectedRegion ); + TZRegion* region = m_config->timezoneData().find< TZRegion >( selectedRegion ); if ( !region ) { return; } - m_zoneCombo->blockSignals( true ); - m_zoneCombo->setModel( new CStringListModel( region->zones() ) ); - m_zoneCombo->blockSignals( false ); + { + cSignalBlocker b( m_zoneCombo ); + m_zoneCombo->setModel( new CStringListModel( region->zones() ) ); + } + m_zoneCombo->currentIndexChanged( m_zoneCombo->currentIndex() ); } @@ -283,8 +289,7 @@ LocalePage::zoneChanged( int currentIndex ) Q_UNUSED( currentIndex ) if ( !m_blockTzWidgetSet ) { - m_config->setCurrentLocation( m_regionCombo->currentData().toString(), - m_zoneCombo->currentData().toString() ); + m_config->setCurrentLocation( m_regionCombo->currentData().toString(), m_zoneCombo->currentData().toString() ); } updateGlobalStorage(); } @@ -292,7 +297,7 @@ LocalePage::zoneChanged( int currentIndex ) void LocalePage::locationChanged( const CalamaresUtils::Locale::TZZone* location ) { - m_blockTzWidgetSet = true; + cBoolSetter< true > b( m_blockTzWidgetSet ); // Set region index int index = m_regionCombo->findData( location->region() ); @@ -312,8 +317,6 @@ LocalePage::locationChanged( const CalamaresUtils::Locale::TZZone* location ) m_zoneCombo->setCurrentIndex( index ); - m_blockTzWidgetSet = false; - updateGlobalStorage(); } From a307217d83d9fcd4e1aa418331b70e954e896135 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 21 Jul 2020 11:08:17 +0200 Subject: [PATCH 022/113] [locale] Tidy LocaleConfiguration - expand API documentation - minor coding-style adjustments --- src/modules/locale/LocaleConfiguration.cpp | 4 +-- src/modules/locale/LocaleConfiguration.h | 37 ++++++++++++++++------ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/modules/locale/LocaleConfiguration.cpp b/src/modules/locale/LocaleConfiguration.cpp index f1810fb83..055907228 100644 --- a/src/modules/locale/LocaleConfiguration.cpp +++ b/src/modules/locale/LocaleConfiguration.cpp @@ -37,7 +37,7 @@ LocaleConfiguration::LocaleConfiguration( const QString& localeName, const QStri lc_numeric = lc_time = lc_monetary = lc_paper = lc_name = lc_address = lc_telephone = lc_measurement = lc_identification = formatsName; - (void)setLanguage( localeName ); + setLanguage( localeName ); } @@ -83,7 +83,7 @@ LocaleConfiguration::fromLanguageAndLocation( const QString& languageLocale, if ( language == "pt" || language == "zh" ) { QString proposedLocale = QString( "%1_%2" ).arg( language ).arg( countryCode ); - foreach ( QString line, linesForLanguage ) + for ( const QString& line : linesForLanguage ) { if ( line.contains( proposedLocale ) ) { diff --git a/src/modules/locale/LocaleConfiguration.h b/src/modules/locale/LocaleConfiguration.h index 4f4fc6b21..2e02bc02a 100644 --- a/src/modules/locale/LocaleConfiguration.h +++ b/src/modules/locale/LocaleConfiguration.h @@ -26,36 +26,53 @@ class LocaleConfiguration { -public: - /// @brief Create an empty locale, with nothing set - explicit LocaleConfiguration(); - /// @brief Create a locale with everything set to the given @p localeName +public: // TODO: private (but need to be public for tests) + /** @brief Create a locale with everything set to the given @p localeName + * + * Consumers should use fromLanguageAndLocation() instead. + */ explicit LocaleConfiguration( const QString& localeName /* "en_US.UTF-8" */ ) : LocaleConfiguration( localeName, localeName ) { } - /// @brief Create a locale with language and formats separate + /** @brief Create a locale with language and formats separate + * + * Consumers should use fromLanguageAndLocation() instead. + */ explicit LocaleConfiguration( const QString& localeName, const QString& formatsName ); + /// @brief Create an empty locale, with nothing set + explicit LocaleConfiguration(); + + /** @brief Create a "sensible" locale configuration for @p language and @p countryCode + * + * This method applies some heuristics to pick a good locale (from the list + * @p availableLocales), along with a good language (for instance, in + * large countries with many languages, picking a generally used one). + */ static LocaleConfiguration fromLanguageAndLocation( const QString& language, const QStringList& availableLocales, const QString& countryCode ); + /// Is this an empty (default-constructed and not modified) configuration? bool isEmpty() const; - /** @brief sets lang and the BCP47 representation + /** @brief sets language to @p localeName * - * Note that the documentation how this works is in packages.conf + * The language may be regionalized, e.g. "nl_BE". Both the language + * (with region) and BCP47 representation (without region, lowercase) + * are updated. The BCP47 representation is used by the packages module. + * See also `packages.conf` for a discussion of how this is used. */ void setLanguage( const QString& localeName ); + /// Current language (including region) QString language() const { return m_lang; } - - // Note that the documentation how this works is in packages.conf + /// Current language (lowercase, BCP47 format, no region) QString toBcp47() const { return m_languageLocaleBcp47; } QMap< QString, QString > toMap() const; // These become all uppercase in locale.conf, but we keep them lowercase here to - // avoid confusion with locale.h. + // avoid confusion with , which defines (e.g.) LC_NUMERIC macro. QString lc_numeric, lc_time, lc_monetary, lc_paper, lc_name, lc_address, lc_telephone, lc_measurement, lc_identification; From 66eacce654402371439babdce599165b2a3acf24 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 21 Jul 2020 13:16:52 +0200 Subject: [PATCH 023/113] [locale] Move localeconfiguration to Config object - the language and LC settings migrate from page to config - add API for explicitly setting language (which is then preserved when clicking new locations) --- src/modules/locale/Config.cpp | 61 ++++++++++++++++++++++- src/modules/locale/Config.h | 33 ++++++++++++- src/modules/locale/LocalePage.cpp | 82 +++++-------------------------- src/modules/locale/LocalePage.h | 3 -- 4 files changed, 104 insertions(+), 75 deletions(-) diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index 5a97339a8..c19569d43 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -175,7 +175,8 @@ Config::setCurrentLocation( const QString& regionName, const QString& zoneName ) } else { - setCurrentLocation( QStringLiteral("America"), QStringLiteral("New_York") ); + // Recursive, but America/New_York always exists. + setCurrentLocation( QStringLiteral( "America" ), QStringLiteral( "New_York" ) ); } } @@ -185,10 +186,66 @@ Config::setCurrentLocation( const CalamaresUtils::Locale::TZZone* location ) if ( location != m_currentLocation ) { m_currentLocation = location; + // Overwrite those settings that have not been made explicit. + auto newLocale = automaticLocaleConfiguration(); + if ( !m_selectedLocaleConfiguration.explicit_lang ) + { + m_selectedLocaleConfiguration.setLanguage( newLocale.language() ); + } + if ( !m_selectedLocaleConfiguration.explicit_lc ) + { + m_selectedLocaleConfiguration.lc_numeric = newLocale.lc_numeric; + m_selectedLocaleConfiguration.lc_time = newLocale.lc_time; + m_selectedLocaleConfiguration.lc_monetary = newLocale.lc_monetary; + m_selectedLocaleConfiguration.lc_paper = newLocale.lc_paper; + m_selectedLocaleConfiguration.lc_name = newLocale.lc_name; + m_selectedLocaleConfiguration.lc_address = newLocale.lc_address; + m_selectedLocaleConfiguration.lc_telephone = newLocale.lc_telephone; + m_selectedLocaleConfiguration.lc_measurement = newLocale.lc_measurement; + m_selectedLocaleConfiguration.lc_identification = newLocale.lc_identification; + } emit currentLocationChanged( m_currentLocation ); } } + +LocaleConfiguration +Config::automaticLocaleConfiguration() const +{ + return LocaleConfiguration::fromLanguageAndLocation( + QLocale().name(), supportedLocales(), currentLocation()->country() ); +} + +LocaleConfiguration +Config::localeConfiguration() const +{ + return m_selectedLocaleConfiguration.isEmpty() ? automaticLocaleConfiguration() : m_selectedLocaleConfiguration; +} + +void +Config::setLanguageExplicitly( const QString& language ) +{ + m_selectedLocaleConfiguration.setLanguage( language ); + m_selectedLocaleConfiguration.explicit_lang = true; +} + +void +Config::setLCLocaleExplicitly( const QString& locale ) +{ + // TODO: improve the granularity of this setting. + m_selectedLocaleConfiguration.lc_numeric = locale; + m_selectedLocaleConfiguration.lc_time = locale; + m_selectedLocaleConfiguration.lc_monetary = locale; + m_selectedLocaleConfiguration.lc_paper = locale; + m_selectedLocaleConfiguration.lc_name = locale; + m_selectedLocaleConfiguration.lc_address = locale; + m_selectedLocaleConfiguration.lc_telephone = locale; + m_selectedLocaleConfiguration.lc_measurement = locale; + m_selectedLocaleConfiguration.lc_identification = locale; + m_selectedLocaleConfiguration.explicit_lc = true; +} + + void Config::setConfigurationMap( const QVariantMap& configurationMap ) { @@ -204,7 +261,7 @@ Calamares::JobList Config::createJobs() { Calamares::JobList list; - const CalamaresUtils::Locale::TZZone* location = nullptr; // TODO: m_tzWidget->currentLocation(); + const CalamaresUtils::Locale::TZZone* location = currentLocation(); if ( location ) { diff --git a/src/modules/locale/Config.h b/src/modules/locale/Config.h index 44e7c9142..ff9125c13 100644 --- a/src/modules/locale/Config.h +++ b/src/modules/locale/Config.h @@ -21,6 +21,8 @@ #ifndef LOCALE_CONFIG_H #define LOCALE_CONFIG_H +#include "LocaleConfiguration.h" + #include "Job.h" #include "locale/TimeZone.h" @@ -35,7 +37,8 @@ class Config : public QObject Q_PROPERTY( CalamaresUtils::Locale::CStringListModel* zonesModel READ zonesModel CONSTANT FINAL ) Q_PROPERTY( CalamaresUtils::Locale::CStringListModel* regionModel READ regionModel CONSTANT FINAL ) Q_PROPERTY( const CalamaresUtils::Locale::CStringPairList& timezoneData READ timezoneData CONSTANT FINAL ) - Q_PROPERTY( const CalamaresUtils::Locale::TZZone* currentLocation READ currentLocation WRITE setCurrentLocation NOTIFY currentLocationChanged ) + Q_PROPERTY( const CalamaresUtils::Locale::TZZone* currentLocation READ currentLocation WRITE setCurrentLocation + NOTIFY currentLocationChanged ) public: Config( QObject* parent = nullptr ); @@ -60,11 +63,29 @@ public Q_SLOTS: /** @brief Sets a location by pointer * * Pointer should be within the same model as the widget uses. + * This can update the locale configuration -- the automatic one + * follows the current location, and otherwise only explicitly-set + * values will ignore changes to the location. */ void setCurrentLocation( const CalamaresUtils::Locale::TZZone* location ); + /** @brief The currently selected location (timezone) + * + * The location is a pointer into the date that timezoneData() returns. + * It is possible to return nullptr, if no location has been picked yet. + */ const CalamaresUtils::Locale::TZZone* currentLocation() const { return m_currentLocation; } + /// locale configuration (LC_* and LANG) based solely on the current location. + LocaleConfiguration automaticLocaleConfiguration() const; + /// locale configuration that takes explicit settings into account + LocaleConfiguration localeConfiguration() const; + + /// Set a language by user-choice, overriding future location changes + void setLanguageExplicitly( const QString& language ); + /// Set LC (formats) by user-choice, overriding future location changes + void setLCLocaleExplicitly( const QString& locale ); + signals: void currentLocationChanged( const CalamaresUtils::Locale::TZZone* location ); @@ -80,6 +101,16 @@ private: /// The location, points into the timezone data const CalamaresUtils::Locale::TZZone* m_currentLocation = nullptr; + /** @brief Specific locale configurations + * + * "Automatic" locale configuration based on the location (e.g. + * Europe/Amsterdam means Dutch language and Dutch locale) leave + * this empty; if the user explicitly sets something, then + * this configuration is non-empty and takes precedence over the + * automatic location settings (so a user in Amsterdam can still + * pick Ukranian settings, for instance). + */ + LocaleConfiguration m_selectedLocaleConfiguration; }; diff --git a/src/modules/locale/LocalePage.cpp b/src/modules/locale/LocalePage.cpp index abb4b63bc..d09d38540 100644 --- a/src/modules/locale/LocalePage.cpp +++ b/src/modules/locale/LocalePage.cpp @@ -137,8 +137,7 @@ LocalePage::updateLocaleLabels() m_localeChangeButton->setText( tr( "&Change..." ) ); m_formatsChangeButton->setText( tr( "&Change..." ) ); - LocaleConfiguration lc - = m_selectedLocaleConfiguration.isEmpty() ? guessLocaleConfiguration() : m_selectedLocaleConfiguration; + LocaleConfiguration lc = m_config->localeConfiguration(); auto labels = prettyLocaleStatus( lc ); m_localeLabel->setText( labels.first ); m_formatsLabel->setText( labels.second ); @@ -163,8 +162,7 @@ LocalePage::prettyStatus() const QString status; status += tr( "Set timezone to %1/%2.
" ).arg( m_regionCombo->currentText() ).arg( m_zoneCombo->currentText() ); - LocaleConfiguration lc - = m_selectedLocaleConfiguration.isEmpty() ? guessLocaleConfiguration() : m_selectedLocaleConfiguration; + LocaleConfiguration lc = m_config->localeConfiguration(); auto labels = prettyLocaleStatus( lc ); status += labels.first + "
"; status += labels.second + "
"; @@ -176,8 +174,7 @@ LocalePage::prettyStatus() const QMap< QString, QString > LocalePage::localesMap() { - return m_selectedLocaleConfiguration.isEmpty() ? guessLocaleConfiguration().toMap() - : m_selectedLocaleConfiguration.toMap(); + return m_config->localeConfiguration().toMap(); } @@ -185,21 +182,8 @@ void LocalePage::onActivate() { m_regionCombo->setFocus(); - if ( m_selectedLocaleConfiguration.isEmpty() || !m_selectedLocaleConfiguration.explicit_lang ) - { - auto newLocale = guessLocaleConfiguration(); - m_selectedLocaleConfiguration.setLanguage( newLocale.language() ); - updateGlobalLocale(); - updateLocaleLabels(); - } -} - - -LocaleConfiguration -LocalePage::guessLocaleConfiguration() const -{ - return LocaleConfiguration::fromLanguageAndLocation( - QLocale().name(), m_config->supportedLocales(), m_config->currentLocation()->country() ); + updateGlobalLocale(); + updateLocaleLabels(); } @@ -207,7 +191,7 @@ void LocalePage::updateGlobalLocale() { auto* gs = Calamares::JobQueue::instance()->globalStorage(); - const QString bcp47 = m_selectedLocaleConfiguration.toBcp47(); + const QString bcp47 = m_config->localeConfiguration().toBcp47(); gs->insert( "locale", bcp47 ); } @@ -236,28 +220,6 @@ LocalePage::updateGlobalStorage() } #endif - // Preserve those settings that have been made explicit. - auto newLocale = guessLocaleConfiguration(); - if ( !m_selectedLocaleConfiguration.isEmpty() && m_selectedLocaleConfiguration.explicit_lang ) - { - newLocale.setLanguage( m_selectedLocaleConfiguration.language() ); - } - if ( !m_selectedLocaleConfiguration.isEmpty() && m_selectedLocaleConfiguration.explicit_lc ) - { - newLocale.lc_numeric = m_selectedLocaleConfiguration.lc_numeric; - newLocale.lc_time = m_selectedLocaleConfiguration.lc_time; - newLocale.lc_monetary = m_selectedLocaleConfiguration.lc_monetary; - newLocale.lc_paper = m_selectedLocaleConfiguration.lc_paper; - newLocale.lc_name = m_selectedLocaleConfiguration.lc_name; - newLocale.lc_address = m_selectedLocaleConfiguration.lc_address; - newLocale.lc_telephone = m_selectedLocaleConfiguration.lc_telephone; - newLocale.lc_measurement = m_selectedLocaleConfiguration.lc_measurement; - newLocale.lc_identification = m_selectedLocaleConfiguration.lc_identification; - } - newLocale.explicit_lang = m_selectedLocaleConfiguration.explicit_lang; - newLocale.explicit_lc = m_selectedLocaleConfiguration.explicit_lc; - - m_selectedLocaleConfiguration = newLocale; updateLocaleLabels(); } @@ -324,17 +286,13 @@ void LocalePage::changeLocale() { LCLocaleDialog* dlg - = new LCLocaleDialog( m_selectedLocaleConfiguration.isEmpty() ? guessLocaleConfiguration().language() - : m_selectedLocaleConfiguration.language(), - m_config->supportedLocales(), - this ); + = new LCLocaleDialog( m_config->localeConfiguration().language(), m_config->supportedLocales(), this ); dlg->exec(); if ( dlg->result() == QDialog::Accepted && !dlg->selectedLCLocale().isEmpty() ) { - m_selectedLocaleConfiguration.setLanguage( dlg->selectedLCLocale() ); - m_selectedLocaleConfiguration.explicit_lang = true; - this->updateGlobalLocale(); - this->updateLocaleLabels(); + m_config->setLanguageExplicitly( dlg->selectedLCLocale() ); + updateGlobalLocale(); + updateLocaleLabels(); } dlg->deleteLater(); @@ -345,26 +303,12 @@ void LocalePage::changeFormats() { LCLocaleDialog* dlg - = new LCLocaleDialog( m_selectedLocaleConfiguration.isEmpty() ? guessLocaleConfiguration().lc_numeric - : m_selectedLocaleConfiguration.lc_numeric, - m_config->supportedLocales(), - this ); + = new LCLocaleDialog( m_config->localeConfiguration().lc_numeric, m_config->supportedLocales(), this ); dlg->exec(); if ( dlg->result() == QDialog::Accepted && !dlg->selectedLCLocale().isEmpty() ) { - // TODO: improve the granularity of this setting. - m_selectedLocaleConfiguration.lc_numeric = dlg->selectedLCLocale(); - m_selectedLocaleConfiguration.lc_time = dlg->selectedLCLocale(); - m_selectedLocaleConfiguration.lc_monetary = dlg->selectedLCLocale(); - m_selectedLocaleConfiguration.lc_paper = dlg->selectedLCLocale(); - m_selectedLocaleConfiguration.lc_name = dlg->selectedLCLocale(); - m_selectedLocaleConfiguration.lc_address = dlg->selectedLCLocale(); - m_selectedLocaleConfiguration.lc_telephone = dlg->selectedLCLocale(); - m_selectedLocaleConfiguration.lc_measurement = dlg->selectedLCLocale(); - m_selectedLocaleConfiguration.lc_identification = dlg->selectedLCLocale(); - m_selectedLocaleConfiguration.explicit_lc = true; - - this->updateLocaleLabels(); + m_config->setLCLocaleExplicitly( dlg->selectedLCLocale() ); + updateLocaleLabels(); } dlg->deleteLater(); diff --git a/src/modules/locale/LocalePage.h b/src/modules/locale/LocalePage.h index 3d4b1d03f..9690c4d97 100644 --- a/src/modules/locale/LocalePage.h +++ b/src/modules/locale/LocalePage.h @@ -50,8 +50,6 @@ public: void onActivate(); private: - LocaleConfiguration guessLocaleConfiguration() const; - /// @brief Non-owning pointer to the ViewStep's config Config* m_config; @@ -85,7 +83,6 @@ private: QLabel* m_formatsLabel; QPushButton* m_formatsChangeButton; - LocaleConfiguration m_selectedLocaleConfiguration; bool m_blockTzWidgetSet; }; From abc98cfa796f4c783cc0d1088000f9b98b44de5f Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 21 Jul 2020 14:57:09 +0200 Subject: [PATCH 024/113] [locale] Simplify allocation, guard against crashes if the dialog is deleted. --- src/modules/locale/LocalePage.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/modules/locale/LocalePage.cpp b/src/modules/locale/LocalePage.cpp index d09d38540..98383b429 100644 --- a/src/modules/locale/LocalePage.cpp +++ b/src/modules/locale/LocalePage.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include @@ -285,31 +286,31 @@ LocalePage::locationChanged( const CalamaresUtils::Locale::TZZone* location ) void LocalePage::changeLocale() { - LCLocaleDialog* dlg - = new LCLocaleDialog( m_config->localeConfiguration().language(), m_config->supportedLocales(), this ); + QPointer< LCLocaleDialog > dlg( + new LCLocaleDialog( m_config->localeConfiguration().language(), m_config->supportedLocales(), this ) ); dlg->exec(); - if ( dlg->result() == QDialog::Accepted && !dlg->selectedLCLocale().isEmpty() ) + if ( dlg && dlg->result() == QDialog::Accepted && !dlg->selectedLCLocale().isEmpty() ) { m_config->setLanguageExplicitly( dlg->selectedLCLocale() ); updateGlobalLocale(); updateLocaleLabels(); } - dlg->deleteLater(); + delete dlg; } void LocalePage::changeFormats() { - LCLocaleDialog* dlg - = new LCLocaleDialog( m_config->localeConfiguration().lc_numeric, m_config->supportedLocales(), this ); + QPointer< LCLocaleDialog > dlg( + new LCLocaleDialog( m_config->localeConfiguration().lc_numeric, m_config->supportedLocales(), this ) ); dlg->exec(); - if ( dlg->result() == QDialog::Accepted && !dlg->selectedLCLocale().isEmpty() ) + if ( dlg && dlg->result() == QDialog::Accepted && !dlg->selectedLCLocale().isEmpty() ) { m_config->setLCLocaleExplicitly( dlg->selectedLCLocale() ); updateLocaleLabels(); } - dlg->deleteLater(); + delete dlg; } From 855b21a7dbcf67a4f391886fa3c8ea36d93ee0bd Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 21 Jul 2020 15:35:38 +0200 Subject: [PATCH 025/113] [locale] Remove redundant method - configuration information lives in the Config object --- src/modules/locale/LocalePage.cpp | 8 -------- src/modules/locale/LocalePage.h | 2 -- src/modules/locale/LocaleViewStep.cpp | 2 +- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/modules/locale/LocalePage.cpp b/src/modules/locale/LocalePage.cpp index 98383b429..58ea4a81c 100644 --- a/src/modules/locale/LocalePage.cpp +++ b/src/modules/locale/LocalePage.cpp @@ -171,14 +171,6 @@ LocalePage::prettyStatus() const return status; } - -QMap< QString, QString > -LocalePage::localesMap() -{ - return m_config->localeConfiguration().toMap(); -} - - void LocalePage::onActivate() { diff --git a/src/modules/locale/LocalePage.h b/src/modules/locale/LocalePage.h index 9690c4d97..3a4dd93a9 100644 --- a/src/modules/locale/LocalePage.h +++ b/src/modules/locale/LocalePage.h @@ -45,8 +45,6 @@ public: QString prettyStatus() const; - QMap< QString, QString > localesMap(); - void onActivate(); private: diff --git a/src/modules/locale/LocaleViewStep.cpp b/src/modules/locale/LocaleViewStep.cpp index e17c7b004..c7ed8766b 100644 --- a/src/modules/locale/LocaleViewStep.cpp +++ b/src/modules/locale/LocaleViewStep.cpp @@ -169,7 +169,7 @@ LocaleViewStep::onLeave() m_jobs = m_config->createJobs(); m_prettyStatus = m_actualWidget->prettyStatus(); - auto map = m_actualWidget->localesMap(); + auto map = m_config->localeConfiguration().toMap(); QVariantMap vm; for ( auto it = map.constBegin(); it != map.constEnd(); ++it ) { From ef08ff6ac09cb3f10937a6001f2dd08d6c198980 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 21 Jul 2020 15:51:49 +0200 Subject: [PATCH 026/113] [locale] Move status strings from Page to Config - the config knows the status and how to describe it, fetch the strings from there. --- src/modules/locale/Config.cpp | 28 +++++++++++++++++++++++++++ src/modules/locale/Config.h | 13 +++++++++++++ src/modules/locale/LocalePage.cpp | 28 +-------------------------- src/modules/locale/LocalePage.h | 6 ------ src/modules/locale/LocaleViewStep.cpp | 2 +- 5 files changed, 43 insertions(+), 34 deletions(-) diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index c19569d43..8893d387a 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -22,6 +22,7 @@ #include "SetTimezoneJob.h" +#include "locale/Label.h" #include "utils/Logger.h" #include "utils/Variant.h" @@ -245,6 +246,33 @@ Config::setLCLocaleExplicitly( const QString& locale ) m_selectedLocaleConfiguration.explicit_lc = true; } +std::pair< QString, QString > +Config::prettyLocaleStatus() const +{ + using CalamaresUtils::Locale::Label; + + Label lang( m_selectedLocaleConfiguration.language(), Label::LabelFormat::AlwaysWithCountry ); + Label num( m_selectedLocaleConfiguration.lc_numeric, Label::LabelFormat::AlwaysWithCountry ); + + return std::make_pair< QString, QString >( + tr( "The system language will be set to %1." ).arg( lang.label() ), + tr( "The numbers and dates locale will be set to %1." ).arg( num.label() ) ); +} + +QString +Config::prettyStatus() const +{ + QString br( QStringLiteral("
")); + QString status; + status += tr( "Set timezone to %1/%2." ).arg( m_currentLocation->region(), m_currentLocation->zone() ) + br; + + auto labels = prettyLocaleStatus(); + status += labels.first + br; + status += labels.second + br; + + return status; +} + void Config::setConfigurationMap( const QVariantMap& configurationMap ) diff --git a/src/modules/locale/Config.h b/src/modules/locale/Config.h index ff9125c13..91ed8f1be 100644 --- a/src/modules/locale/Config.h +++ b/src/modules/locale/Config.h @@ -47,6 +47,19 @@ public: void setConfigurationMap( const QVariantMap& ); Calamares::JobList createJobs(); + /** @brief Human-readable status for language and LC + * + * For the current locale config, return two strings describing + * the settings for language and numbers. + */ + std::pair< QString, QString > prettyLocaleStatus() const; + /** @brief Human-readable zone, language and LC status + * + * Concatenates all three strings with
+ */ + QString prettyStatus() const; + + public Q_SLOTS: const QStringList& supportedLocales() const { return m_localeGenLines; } CalamaresUtils::Locale::CStringListModel* regionModel() const { return m_regionModel.get(); } diff --git a/src/modules/locale/LocalePage.cpp b/src/modules/locale/LocalePage.cpp index 58ea4a81c..fb3433d23 100644 --- a/src/modules/locale/LocalePage.cpp +++ b/src/modules/locale/LocalePage.cpp @@ -138,38 +138,12 @@ LocalePage::updateLocaleLabels() m_localeChangeButton->setText( tr( "&Change..." ) ); m_formatsChangeButton->setText( tr( "&Change..." ) ); - LocaleConfiguration lc = m_config->localeConfiguration(); - auto labels = prettyLocaleStatus( lc ); + auto labels = m_config->prettyLocaleStatus(); m_localeLabel->setText( labels.first ); m_formatsLabel->setText( labels.second ); } -std::pair< QString, QString > -LocalePage::prettyLocaleStatus( const LocaleConfiguration& lc ) const -{ - using CalamaresUtils::Locale::Label; - Label lang( lc.language(), Label::LabelFormat::AlwaysWithCountry ); - Label num( lc.lc_numeric, Label::LabelFormat::AlwaysWithCountry ); - - return std::make_pair< QString, QString >( - tr( "The system language will be set to %1." ).arg( lang.label() ), - tr( "The numbers and dates locale will be set to %1." ).arg( num.label() ) ); -} - -QString -LocalePage::prettyStatus() const -{ - QString status; - status += tr( "Set timezone to %1/%2.
" ).arg( m_regionCombo->currentText() ).arg( m_zoneCombo->currentText() ); - - LocaleConfiguration lc = m_config->localeConfiguration(); - auto labels = prettyLocaleStatus( lc ); - status += labels.first + "
"; - status += labels.second + "
"; - - return status; -} void LocalePage::onActivate() diff --git a/src/modules/locale/LocalePage.h b/src/modules/locale/LocalePage.h index 3a4dd93a9..b40aeb465 100644 --- a/src/modules/locale/LocalePage.h +++ b/src/modules/locale/LocalePage.h @@ -43,18 +43,12 @@ public: explicit LocalePage( class Config* config, QWidget* parent = nullptr ); virtual ~LocalePage(); - QString prettyStatus() const; - void onActivate(); private: /// @brief Non-owning pointer to the ViewStep's config Config* m_config; - // For the given locale config, return two strings describing - // the settings for language and numbers. - std::pair< QString, QString > prettyLocaleStatus( const LocaleConfiguration& ) const; - /** @brief Update the GS *locale* key with the selected system language. * * This uses whatever is set in m_selectedLocaleConfiguration as the language, diff --git a/src/modules/locale/LocaleViewStep.cpp b/src/modules/locale/LocaleViewStep.cpp index c7ed8766b..00752ebb6 100644 --- a/src/modules/locale/LocaleViewStep.cpp +++ b/src/modules/locale/LocaleViewStep.cpp @@ -167,7 +167,7 @@ LocaleViewStep::onLeave() if ( m_actualWidget ) { m_jobs = m_config->createJobs(); - m_prettyStatus = m_actualWidget->prettyStatus(); + m_prettyStatus = m_config->prettyStatus(); auto map = m_config->localeConfiguration().toMap(); QVariantMap vm; From f7c2e4a3e77221acf9ebe016acbb7da44ff87cf1 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 21 Jul 2020 16:27:21 +0200 Subject: [PATCH 027/113] [locale] Sanitize Config signals and slots - remove the weirdly-structured prettyStatus and similar: the Config object has human-readable status strings (three, for location, language, and LC-formats) which can be normal properties with signals. - Implement prettyStatus in the view step by querying the Config. --- src/modules/locale/Config.cpp | 49 +++++++++++-------- src/modules/locale/Config.h | 68 ++++++++++++++------------- src/modules/locale/LocalePage.cpp | 9 ++-- src/modules/locale/LocaleViewStep.cpp | 4 +- src/modules/locale/LocaleViewStep.h | 1 - 5 files changed, 70 insertions(+), 61 deletions(-) diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index 8893d387a..cdd4e767f 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -192,6 +192,7 @@ Config::setCurrentLocation( const CalamaresUtils::Locale::TZZone* location ) if ( !m_selectedLocaleConfiguration.explicit_lang ) { m_selectedLocaleConfiguration.setLanguage( newLocale.language() ); + emit currentLanguageStatusChanged( currentLanguageStatus() ); } if ( !m_selectedLocaleConfiguration.explicit_lc ) { @@ -204,6 +205,8 @@ Config::setCurrentLocation( const CalamaresUtils::Locale::TZZone* location ) m_selectedLocaleConfiguration.lc_telephone = newLocale.lc_telephone; m_selectedLocaleConfiguration.lc_measurement = newLocale.lc_measurement; m_selectedLocaleConfiguration.lc_identification = newLocale.lc_identification; + + emit currentLCStatusChanged( currentLCStatus() ); } emit currentLocationChanged( m_currentLocation ); } @@ -228,6 +231,8 @@ Config::setLanguageExplicitly( const QString& language ) { m_selectedLocaleConfiguration.setLanguage( language ); m_selectedLocaleConfiguration.explicit_lang = true; + + emit currentLanguageStatusChanged( currentLanguageStatus() ); } void @@ -244,33 +249,37 @@ Config::setLCLocaleExplicitly( const QString& locale ) m_selectedLocaleConfiguration.lc_measurement = locale; m_selectedLocaleConfiguration.lc_identification = locale; m_selectedLocaleConfiguration.explicit_lc = true; -} -std::pair< QString, QString > -Config::prettyLocaleStatus() const -{ - using CalamaresUtils::Locale::Label; - - Label lang( m_selectedLocaleConfiguration.language(), Label::LabelFormat::AlwaysWithCountry ); - Label num( m_selectedLocaleConfiguration.lc_numeric, Label::LabelFormat::AlwaysWithCountry ); - - return std::make_pair< QString, QString >( - tr( "The system language will be set to %1." ).arg( lang.label() ), - tr( "The numbers and dates locale will be set to %1." ).arg( num.label() ) ); + emit currentLCStatusChanged( currentLCStatus() ); } QString -Config::prettyStatus() const +Config::currentLocationStatus() const { - QString br( QStringLiteral("
")); - QString status; - status += tr( "Set timezone to %1/%2." ).arg( m_currentLocation->region(), m_currentLocation->zone() ) + br; + return tr( "Set timezone to %1/%2." ).arg( m_currentLocation->region(), m_currentLocation->zone() ); +} - auto labels = prettyLocaleStatus(); - status += labels.first + br; - status += labels.second + br; +static inline QString +localeLabel( const QString& s ) +{ + using CalamaresUtils::Locale::Label; - return status; + Label lang( s, Label::LabelFormat::AlwaysWithCountry ); + return lang.label(); +} + +QString +Config::currentLanguageStatus() const +{ + return tr( "The system language will be set to %1." ) + .arg( localeLabel( m_selectedLocaleConfiguration.language() ) ); +} + +QString +Config::currentLCStatus() const +{ + return tr( "The numbers and dates locale will be set to %1." ) + .arg( localeLabel( m_selectedLocaleConfiguration.lc_numeric ) ); } diff --git a/src/modules/locale/Config.h b/src/modules/locale/Config.h index 91ed8f1be..c4a512100 100644 --- a/src/modules/locale/Config.h +++ b/src/modules/locale/Config.h @@ -36,10 +36,14 @@ class Config : public QObject Q_PROPERTY( const QStringList& supportedLocales READ supportedLocales CONSTANT FINAL ) Q_PROPERTY( CalamaresUtils::Locale::CStringListModel* zonesModel READ zonesModel CONSTANT FINAL ) Q_PROPERTY( CalamaresUtils::Locale::CStringListModel* regionModel READ regionModel CONSTANT FINAL ) - Q_PROPERTY( const CalamaresUtils::Locale::CStringPairList& timezoneData READ timezoneData CONSTANT FINAL ) + Q_PROPERTY( const CalamaresUtils::Locale::TZZone* currentLocation READ currentLocation WRITE setCurrentLocation NOTIFY currentLocationChanged ) + Q_PROPERTY( QString currentLocationStatus READ currentLocationStatus NOTIFY currentLanguageStatusChanged ) + Q_PROPERTY( QString currentLanguageStatus READ currentLanguageStatus NOTIFY currentLanguageStatusChanged ) + Q_PROPERTY( QString currentLCStatus READ currentLCStatus NOTIFY currentLCStatusChanged ) + public: Config( QObject* parent = nullptr ); ~Config(); @@ -47,25 +51,37 @@ public: void setConfigurationMap( const QVariantMap& ); Calamares::JobList createJobs(); - /** @brief Human-readable status for language and LC - * - * For the current locale config, return two strings describing - * the settings for language and numbers. - */ - std::pair< QString, QString > prettyLocaleStatus() const; - /** @brief Human-readable zone, language and LC status - * - * Concatenates all three strings with
- */ - QString prettyStatus() const; + // Underlying data for the models + const CalamaresUtils::Locale::CStringPairList& timezoneData() const; + /** @brief The currently selected location (timezone) + * + * The location is a pointer into the date that timezoneData() returns. + * It is possible to return nullptr, if no location has been picked yet. + */ + const CalamaresUtils::Locale::TZZone* currentLocation() const { return m_currentLocation; } + + /// locale configuration (LC_* and LANG) based solely on the current location. + LocaleConfiguration automaticLocaleConfiguration() const; + /// locale configuration that takes explicit settings into account + LocaleConfiguration localeConfiguration() const; + + /// The human-readable description of what timezone is used + QString currentLocationStatus() const; + /// The human-readable description of what language is used + QString currentLanguageStatus() const; + /// The human-readable description of what locale (LC_*) is used + QString currentLCStatus() const; -public Q_SLOTS: const QStringList& supportedLocales() const { return m_localeGenLines; } CalamaresUtils::Locale::CStringListModel* regionModel() const { return m_regionModel.get(); } CalamaresUtils::Locale::CStringListModel* zonesModel() const { return m_zonesModel.get(); } - // Underlying data for the models - const CalamaresUtils::Locale::CStringPairList& timezoneData() const; + +public Q_SLOTS: + /// Set a language by user-choice, overriding future location changes + void setLanguageExplicitly( const QString& language ); + /// Set LC (formats) by user-choice, overriding future location changes + void setLCLocaleExplicitly( const QString& locale ); /** @brief Sets a location by name * @@ -82,25 +98,11 @@ public Q_SLOTS: */ void setCurrentLocation( const CalamaresUtils::Locale::TZZone* location ); - /** @brief The currently selected location (timezone) - * - * The location is a pointer into the date that timezoneData() returns. - * It is possible to return nullptr, if no location has been picked yet. - */ - const CalamaresUtils::Locale::TZZone* currentLocation() const { return m_currentLocation; } - - /// locale configuration (LC_* and LANG) based solely on the current location. - LocaleConfiguration automaticLocaleConfiguration() const; - /// locale configuration that takes explicit settings into account - LocaleConfiguration localeConfiguration() const; - - /// Set a language by user-choice, overriding future location changes - void setLanguageExplicitly( const QString& language ); - /// Set LC (formats) by user-choice, overriding future location changes - void setLCLocaleExplicitly( const QString& locale ); - signals: - void currentLocationChanged( const CalamaresUtils::Locale::TZZone* location ); + void currentLocationChanged( const CalamaresUtils::Locale::TZZone* location ) const; + void currentLocationStatusChanged( const QString& ) const; + void currentLanguageStatusChanged( const QString& ) const; + void currentLCStatusChanged( const QString& ) const; private: /// A list of supported locale identifiers (e.g. "en_US.UTF-8") diff --git a/src/modules/locale/LocalePage.cpp b/src/modules/locale/LocalePage.cpp index fb3433d23..c312f39f6 100644 --- a/src/modules/locale/LocalePage.cpp +++ b/src/modules/locale/LocalePage.cpp @@ -106,6 +106,8 @@ LocalePage::LocalePage( Config* config, QWidget* parent ) setMinimumWidth( m_tzWidget->width() ); setLayout( mainLayout ); + connect( config, &Config::currentLCStatusChanged, m_formatsLabel, &QLabel::setText ); + connect( config, &Config::currentLanguageStatusChanged, m_localeLabel, &QLabel::setText ); connect( config, &Config::currentLocationChanged, m_tzWidget, &TimeZoneWidget::setCurrentLocation ); connect( config, &Config::currentLocationChanged, this, &LocalePage::locationChanged ); connect( m_tzWidget, @@ -137,14 +139,11 @@ LocalePage::updateLocaleLabels() m_zoneLabel->setText( tr( "Zone:" ) ); m_localeChangeButton->setText( tr( "&Change..." ) ); m_formatsChangeButton->setText( tr( "&Change..." ) ); - - auto labels = m_config->prettyLocaleStatus(); - m_localeLabel->setText( labels.first ); - m_formatsLabel->setText( labels.second ); + m_localeLabel->setText( m_config->currentLanguageStatus() ); + m_formatsLabel->setText( m_config->currentLCStatus() ); } - void LocalePage::onActivate() { diff --git a/src/modules/locale/LocaleViewStep.cpp b/src/modules/locale/LocaleViewStep.cpp index 00752ebb6..31f8eb8bd 100644 --- a/src/modules/locale/LocaleViewStep.cpp +++ b/src/modules/locale/LocaleViewStep.cpp @@ -104,7 +104,8 @@ LocaleViewStep::prettyName() const QString LocaleViewStep::prettyStatus() const { - return m_prettyStatus; + QStringList l { m_config->currentLocationStatus(), m_config->currentLanguageStatus(), m_config->currentLCStatus() }; + return l.join( QStringLiteral( "
" ) ); } @@ -167,7 +168,6 @@ LocaleViewStep::onLeave() if ( m_actualWidget ) { m_jobs = m_config->createJobs(); - m_prettyStatus = m_config->prettyStatus(); auto map = m_config->localeConfiguration().toMap(); QVariantMap vm; diff --git a/src/modules/locale/LocaleViewStep.h b/src/modules/locale/LocaleViewStep.h index 24764b172..cb1902f2e 100644 --- a/src/modules/locale/LocaleViewStep.h +++ b/src/modules/locale/LocaleViewStep.h @@ -70,7 +70,6 @@ private: LocalePage* m_actualWidget; bool m_nextEnabled; - QString m_prettyStatus; Calamares::JobList m_jobs; From 1de2210d29e4403d57fadf538c8a6ca82df3a5c8 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 21 Jul 2020 17:37:09 +0200 Subject: [PATCH 028/113] [locale] Move the GS updating to the Config object - since all locale changes need to be entered into GS anyway, this is something the Config object can do because it is the source of truth for locale settings. - drop all the GS settings from the Page. --- src/modules/locale/Config.cpp | 33 ++++++++++++++++++++++++ src/modules/locale/LocalePage.cpp | 42 ------------------------------- src/modules/locale/LocalePage.h | 7 ------ 3 files changed, 33 insertions(+), 49 deletions(-) diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index cdd4e767f..5fa8b3c8b 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -22,6 +22,9 @@ #include "SetTimezoneJob.h" +#include "GlobalStorage.h" +#include "JobQueue.h" +#include "Settings.h" #include "locale/Label.h" #include "utils/Logger.h" #include "utils/Variant.h" @@ -154,6 +157,36 @@ Config::Config( QObject* parent ) , m_regionModel( std::make_unique< CalamaresUtils::Locale::CStringListModel >( ::timezoneData() ) ) , m_zonesModel( std::make_unique< CalamaresUtils::Locale::CStringListModel >() ) { + // Slightly unusual: connect to our *own* signals. Wherever the language + // or the location is changed, these signals are emitted, so hook up to + // them to update global storage accordingly. This simplifies code: + // we don't need to call an update-GS method, or introduce an intermediate + // update-thing-and-GS method. And everywhere where we **do** change + // language or location, we already emit the signal. + connect( this, &Config::currentLanguageStatusChanged, [&]() { + auto* gs = Calamares::JobQueue::instance()->globalStorage(); + gs->insert( "locale", m_selectedLocaleConfiguration.toBcp47() ); + } ); + + connect( this, &Config::currentLocationChanged, [&]() { + auto* gs = Calamares::JobQueue::instance()->globalStorage(); + const auto* location = currentLocation(); + bool locationChanged = ( location->region() != gs->value( "locationRegion" ) ) + || ( location->zone() != gs->value( "locationZone" ) ); + + gs->insert( "locationRegion", location->region() ); + gs->insert( "locationZone", location->zone() ); + + // If we're in chroot mode (normal install mode), then we immediately set the + // timezone on the live system. When debugging timezones, don't bother. +#ifndef DEBUG_TIMEZONES + if ( locationChanged && Calamares::Settings::instance()->doChroot() ) + { + QProcess::execute( "timedatectl", // depends on systemd + { "set-timezone", location->region() + '/' + location->zone() } ); + } +#endif + } ); } Config::~Config() {} diff --git a/src/modules/locale/LocalePage.cpp b/src/modules/locale/LocalePage.cpp index c312f39f6..0ea56a072 100644 --- a/src/modules/locale/LocalePage.cpp +++ b/src/modules/locale/LocalePage.cpp @@ -125,7 +125,6 @@ LocalePage::LocalePage( Config* config, QWidget* parent ) m_regionCombo->setModel( m_config->regionModel() ); m_regionCombo->currentIndexChanged( m_regionCombo->currentIndex() ); - updateGlobalStorage(); } @@ -148,47 +147,10 @@ void LocalePage::onActivate() { m_regionCombo->setFocus(); - updateGlobalLocale(); updateLocaleLabels(); } -void -LocalePage::updateGlobalLocale() -{ - auto* gs = Calamares::JobQueue::instance()->globalStorage(); - const QString bcp47 = m_config->localeConfiguration().toBcp47(); - gs->insert( "locale", bcp47 ); -} - - -void -LocalePage::updateGlobalStorage() -{ - auto* gs = Calamares::JobQueue::instance()->globalStorage(); - - const auto* location = m_config->currentLocation(); - bool locationChanged = ( location->region() != gs->value( "locationRegion" ) ) - || ( location->zone() != gs->value( "locationZone" ) ); - - gs->insert( "locationRegion", location->region() ); - gs->insert( "locationZone", location->zone() ); - - updateGlobalLocale(); - - // If we're in chroot mode (normal install mode), then we immediately set the - // timezone on the live system. When debugging timezones, don't bother. -#ifndef DEBUG_TIMEZONES - if ( locationChanged && Calamares::Settings::instance()->doChroot() ) - { - QProcess::execute( "timedatectl", // depends on systemd - { "set-timezone", location->region() + '/' + location->zone() } ); - } -#endif - - updateLocaleLabels(); -} - void LocalePage::regionChanged( int currentIndex ) { @@ -219,7 +181,6 @@ LocalePage::zoneChanged( int currentIndex ) { m_config->setCurrentLocation( m_regionCombo->currentData().toString(), m_zoneCombo->currentData().toString() ); } - updateGlobalStorage(); } void @@ -244,8 +205,6 @@ LocalePage::locationChanged( const CalamaresUtils::Locale::TZZone* location ) } m_zoneCombo->setCurrentIndex( index ); - - updateGlobalStorage(); } void @@ -257,7 +216,6 @@ LocalePage::changeLocale() if ( dlg && dlg->result() == QDialog::Accepted && !dlg->selectedLCLocale().isEmpty() ) { m_config->setLanguageExplicitly( dlg->selectedLCLocale() ); - updateGlobalLocale(); updateLocaleLabels(); } diff --git a/src/modules/locale/LocalePage.h b/src/modules/locale/LocalePage.h index b40aeb465..bf41f8f69 100644 --- a/src/modules/locale/LocalePage.h +++ b/src/modules/locale/LocalePage.h @@ -49,13 +49,6 @@ private: /// @brief Non-owning pointer to the ViewStep's config Config* m_config; - /** @brief Update the GS *locale* key with the selected system language. - * - * This uses whatever is set in m_selectedLocaleConfiguration as the language, - * and writes it to GS *locale* key (as a string, in BCP47 format). - */ - void updateGlobalLocale(); - void updateGlobalStorage(); void updateLocaleLabels(); void regionChanged( int currentIndex ); From 995ebd5c834d564a2b0f128f5ad1212ed562a515 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 21 Jul 2020 17:44:44 +0200 Subject: [PATCH 029/113] [locale] Remove unused #includes --- src/modules/locale/LocalePage.cpp | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/modules/locale/LocalePage.cpp b/src/modules/locale/LocalePage.cpp index 0ea56a072..3999599c9 100644 --- a/src/modules/locale/LocalePage.cpp +++ b/src/modules/locale/LocalePage.cpp @@ -20,15 +20,9 @@ #include "LocalePage.h" #include "Config.h" -#include "SetTimezoneJob.h" +#include "LCLocaleDialog.h" #include "timezonewidget/timezonewidget.h" -#include "GlobalStorage.h" -#include "JobQueue.h" -#include "LCLocaleDialog.h" -#include "Settings.h" -#include "locale/Label.h" -#include "locale/TimeZone.h" #include "utils/CalamaresUtilsGui.h" #include "utils/Logger.h" #include "utils/RAII.h" @@ -36,10 +30,8 @@ #include #include -#include #include #include -#include #include LocalePage::LocalePage( Config* config, QWidget* parent ) From f6419d5de123077a5961de9b481fefba6b7b551f Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 22 Jul 2020 00:06:56 +0200 Subject: [PATCH 030/113] [locale] New setting *adjustLiveTimezone* - allow finer-grained control over whether-or-not to adjust the timezone in the live system. - handle some special cases at the point of loading-configuration. - document the setting in locale.conf - correct some documentation bugs - adjust the YAML schema for locale.conf so it's legal YAML syntax **and** validates the current file. --- src/modules/locale/Config.cpp | 23 ++++++++++++++---- src/modules/locale/Config.h | 7 ++++++ src/modules/locale/locale.conf | 17 ++++++++++--- src/modules/locale/locale.schema.yaml | 35 ++++++++++++++++++++++++--- 4 files changed, 69 insertions(+), 13 deletions(-) diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index 5fa8b3c8b..4a2923081 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -177,15 +177,11 @@ Config::Config( QObject* parent ) gs->insert( "locationRegion", location->region() ); gs->insert( "locationZone", location->zone() ); - // If we're in chroot mode (normal install mode), then we immediately set the - // timezone on the live system. When debugging timezones, don't bother. -#ifndef DEBUG_TIMEZONES - if ( locationChanged && Calamares::Settings::instance()->doChroot() ) + if ( locationChanged && m_adjustLiveTimezone ) { QProcess::execute( "timedatectl", // depends on systemd { "set-timezone", location->region() + '/' + location->zone() } ); } -#endif } ); } @@ -325,6 +321,23 @@ Config::setConfigurationMap( const QVariantMap& configurationMap ) localeGenPath = QStringLiteral( "/etc/locale.gen" ); } m_localeGenLines = loadLocales( localeGenPath ); + + m_adjustLiveTimezone + = CalamaresUtils::getBool( configurationMap, "adjustLiveTimezone", Calamares::Settings::instance()->doChroot() ); +#ifdef DEBUG_TIMEZONES + if ( m_adjustLiveTimezone ) + { + cDebug() << "Turning off live-timezone adjustments because debugging is on."; + m_adjustLiveTimezone = false; + } +#endif +#ifdef __FreeBSD__ + if ( m_adjustLiveTimezone ) + { + cDebug() << "Turning off live-timezone adjustments on FreeBSD."; + m_adjustLiveTimezone = false; + } +#endif } Calamares::JobList diff --git a/src/modules/locale/Config.h b/src/modules/locale/Config.h index c4a512100..c8710f4a9 100644 --- a/src/modules/locale/Config.h +++ b/src/modules/locale/Config.h @@ -126,6 +126,13 @@ private: * pick Ukranian settings, for instance). */ LocaleConfiguration m_selectedLocaleConfiguration; + + /** @brief Should we adjust the "live" timezone when the location changes? + * + * In the Widgets UI, clicking around on the world map adjusts the + * timezone, and the live system can be made to follow that. + */ + bool m_adjustLiveTimezone; }; diff --git a/src/modules/locale/locale.conf b/src/modules/locale/locale.conf index dc68a050f..572326f0b 100644 --- a/src/modules/locale/locale.conf +++ b/src/modules/locale/locale.conf @@ -1,5 +1,5 @@ --- -# This settings are used to set your default system time zone. +# These settings are used to set your default system time zone. # Time zones are usually located under /usr/share/zoneinfo and # provided by the 'tzdata' package of your Distribution. # @@ -14,17 +14,26 @@ region: "America" zone: "New_York" +# Should changing the system location (e.g. clicking around on the timezone +# map) immediately reflect the changed timezone in the live system? +# By default, installers (with a target system) do, and setup (e.g. OEM +# configuration) does not, but you can switch it on here (or off, if +# you think it's annoying in the installer). +# +# Note that not all systems support live adjustment. +# +# adjustLiveTimezone: true # System locales are detected in the following order: # # - /usr/share/i18n/SUPPORTED # - localeGenPath (defaults to /etc/locale.gen if not set) -# - 'locale -a' output +# - `locale -a` output # -# Enable only when your Distribution is using an +# Enable only when your Distribution is using a # custom path for locale.gen # -#localeGenPath: "PATH_TO/locale.gen" +#localeGenPath: "/etc/locale.gen" # GeoIP based Language settings: Leave commented out to disable GeoIP. # diff --git a/src/modules/locale/locale.schema.yaml b/src/modules/locale/locale.schema.yaml index 41c3ad487..6eadb5c85 100644 --- a/src/modules/locale/locale.schema.yaml +++ b/src/modules/locale/locale.schema.yaml @@ -4,7 +4,34 @@ $id: https://calamares.io/schemas/locale additionalProperties: false type: object properties: - "region": { type: str } - "zone": { type: str } - "localeGenPath": { type: string, required: true } - "geoipUrl": { type: str } + region: { type: string, + enum: [ + Africa, + America, + Antarctica, + Arctic, + Asia, + Atlantic, + Australia, + Europe, + Indian, + Pacific + ] + } + zone: { type: string } + + adjustLiveTimezone: { type: boolean, default: true } + + localeGenPath: { type: string } + + # TODO: refactor, this is reused in welcome + geoip: + additionalProperties: false + type: object + properties: + style: { type: string, enum: [ none, fixed, xml, json ] } + url: { type: string } + selector: { type: string } + required: [ style, url, selector ] + +required: [ region, zone ] From 0c9480aa3f0fe7f17704354134f573fa2fd31ddf Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 22 Jul 2020 00:23:50 +0200 Subject: [PATCH 031/113] [locale] Move more business logic to Config - writing *localeConf* settings to GS can be done always when the formats are set, rather than special-cased. The code that handles the "special case" of no widget existing for the ViewStep overlooks the other crashes that happen then. - Since Config knows what jobs to create, just ask it rather than keeping a copy. --- src/modules/locale/Config.cpp | 12 +++++++++++- src/modules/locale/LocaleViewStep.cpp | 20 +------------------- src/modules/locale/LocaleViewStep.h | 2 -- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index 4a2923081..cb0fe7734 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -170,18 +170,28 @@ Config::Config( QObject* parent ) connect( this, &Config::currentLocationChanged, [&]() { auto* gs = Calamares::JobQueue::instance()->globalStorage(); + + // Update the GS region and zone (and possibly the live timezone) const auto* location = currentLocation(); bool locationChanged = ( location->region() != gs->value( "locationRegion" ) ) || ( location->zone() != gs->value( "locationZone" ) ); gs->insert( "locationRegion", location->region() ); gs->insert( "locationZone", location->zone() ); - if ( locationChanged && m_adjustLiveTimezone ) { QProcess::execute( "timedatectl", // depends on systemd { "set-timezone", location->region() + '/' + location->zone() } ); } + + // Update GS localeConf (the LC_ variables) + auto map = localeConfiguration().toMap(); + QVariantMap vm; + for ( auto it = map.constBegin(); it != map.constEnd(); ++it ) + { + vm.insert( it.key(), it.value() ); + } + gs->insert( "localeConf", vm ); } ); } diff --git a/src/modules/locale/LocaleViewStep.cpp b/src/modules/locale/LocaleViewStep.cpp index 31f8eb8bd..5baf68b43 100644 --- a/src/modules/locale/LocaleViewStep.cpp +++ b/src/modules/locale/LocaleViewStep.cpp @@ -147,7 +147,7 @@ LocaleViewStep::isAtEnd() const Calamares::JobList LocaleViewStep::jobs() const { - return m_jobs; + return m_config->createJobs(); } @@ -165,24 +165,6 @@ LocaleViewStep::onActivate() void LocaleViewStep::onLeave() { - if ( m_actualWidget ) - { - m_jobs = m_config->createJobs(); - - auto map = m_config->localeConfiguration().toMap(); - QVariantMap vm; - for ( auto it = map.constBegin(); it != map.constEnd(); ++it ) - { - vm.insert( it.key(), it.value() ); - } - - Calamares::JobQueue::instance()->globalStorage()->insert( "localeConf", vm ); - } - else - { - m_jobs.clear(); - Calamares::JobQueue::instance()->globalStorage()->remove( "localeConf" ); - } } diff --git a/src/modules/locale/LocaleViewStep.h b/src/modules/locale/LocaleViewStep.h index cb1902f2e..a1456764f 100644 --- a/src/modules/locale/LocaleViewStep.h +++ b/src/modules/locale/LocaleViewStep.h @@ -71,8 +71,6 @@ private: LocalePage* m_actualWidget; bool m_nextEnabled; - Calamares::JobList m_jobs; - CalamaresUtils::GeoIP::RegionZonePair m_startingTimezone; std::unique_ptr< CalamaresUtils::GeoIP::Handler > m_geoip; From 781d76c9e540a346f7cf6dd6cb9643a74bd0f51c Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 22 Jul 2020 00:32:29 +0200 Subject: [PATCH 032/113] [locale] Avoid nullptr if there is no location --- src/modules/locale/Config.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index cb0fe7734..0898a77f2 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -255,6 +255,11 @@ Config::setCurrentLocation( const CalamaresUtils::Locale::TZZone* location ) LocaleConfiguration Config::automaticLocaleConfiguration() const { + // Special case: no location has been set at **all** + if ( !currentLocation() ) + { + return LocaleConfiguration(); + } return LocaleConfiguration::fromLanguageAndLocation( QLocale().name(), supportedLocales(), currentLocation()->country() ); } From b607cf3f98c1cad911d84ebff116c37d97123834 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 22 Jul 2020 01:26:15 +0200 Subject: [PATCH 033/113] [locale] Get starting TZ in Config - read the *region* and *zone* settings; this duplicates what the ViewStep does and is currently unused, but .. - add new support for using the system's TZ (rather than the fixed values from *region* and *zone*). This complements GeoIP lookup. This is the actual feature that started the long rewrite of the Config object (so that all the business logic would be in one place, usable for both widgets and QML). FIXES #1381 --- src/modules/locale/Config.cpp | 26 ++++++++++++++++++++++++-- src/modules/locale/Config.h | 5 +++++ src/modules/locale/locale.conf | 12 ++++++++++++ src/modules/locale/locale.schema.yaml | 1 + 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index 0898a77f2..0ecd7e049 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -31,6 +31,7 @@ #include #include +#include /** @brief Load supported locale keys * @@ -342,17 +343,38 @@ Config::setConfigurationMap( const QVariantMap& configurationMap ) #ifdef DEBUG_TIMEZONES if ( m_adjustLiveTimezone ) { - cDebug() << "Turning off live-timezone adjustments because debugging is on."; + cWarning() << "Turning off live-timezone adjustments because debugging is on."; m_adjustLiveTimezone = false; } #endif #ifdef __FreeBSD__ if ( m_adjustLiveTimezone ) { - cDebug() << "Turning off live-timezone adjustments on FreeBSD."; + cWarning() << "Turning off live-timezone adjustments on FreeBSD."; m_adjustLiveTimezone = false; } #endif + + QString region = CalamaresUtils::getString( configurationMap, "region" ); + QString zone = CalamaresUtils::getString( configurationMap, "zone" ); + if ( !region.isEmpty() && !zone.isEmpty() ) + { + m_startingTimezone = CalamaresUtils::GeoIP::RegionZonePair( region, zone ); + } + else + { + m_startingTimezone + = CalamaresUtils::GeoIP::RegionZonePair( QStringLiteral( "America" ), QStringLiteral( "New_York" ) ); + } + + if ( CalamaresUtils::getBool( configurationMap, "useSystemTimezone", false ) ) + { + auto systemtz = CalamaresUtils::GeoIP::splitTZString( QTimeZone::systemTimeZoneId() ); + if ( systemtz.isValid() ) + { + m_startingTimezone = systemtz; + } + } } Calamares::JobList diff --git a/src/modules/locale/Config.h b/src/modules/locale/Config.h index c8710f4a9..7e634a4a8 100644 --- a/src/modules/locale/Config.h +++ b/src/modules/locale/Config.h @@ -24,6 +24,7 @@ #include "LocaleConfiguration.h" #include "Job.h" +#include "geoip/Interface.h" #include "locale/TimeZone.h" #include @@ -133,6 +134,10 @@ private: * timezone, and the live system can be made to follow that. */ bool m_adjustLiveTimezone; + + /** @brief The initial timezone (region, zone) specified in the config. + */ + CalamaresUtils::GeoIP::RegionZonePair m_startingTimezone; }; diff --git a/src/modules/locale/locale.conf b/src/modules/locale/locale.conf index 572326f0b..8236a879b 100644 --- a/src/modules/locale/locale.conf +++ b/src/modules/locale/locale.conf @@ -11,9 +11,21 @@ # the locale page can be set through keys *region* and *zone*. # If either is not set, defaults to America/New_York. # +# Note that useSystemTimezone and GeoIP settings can change the +# starting time zone. +# region: "America" zone: "New_York" +# Instead of using *region* and *zone* specified above, +# you can use the system's notion of the timezone, instead. +# This can help if your system is automatically configured with +# a sensible TZ rather than chasing a fixed default. +# +# The default is false. +# +# useSystemTimezone: true + # Should changing the system location (e.g. clicking around on the timezone # map) immediately reflect the changed timezone in the live system? # By default, installers (with a target system) do, and setup (e.g. OEM diff --git a/src/modules/locale/locale.schema.yaml b/src/modules/locale/locale.schema.yaml index 6eadb5c85..d6c35020f 100644 --- a/src/modules/locale/locale.schema.yaml +++ b/src/modules/locale/locale.schema.yaml @@ -19,6 +19,7 @@ properties: ] } zone: { type: string } + useSystemTimezone: { type: boolean, default: false } adjustLiveTimezone: { type: boolean, default: true } From f64a1eb16ada6920999e8cf2eb5e60ee412bc9fa Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 22 Jul 2020 11:52:42 +0200 Subject: [PATCH 034/113] [libcalamaresui] Document the signals from ModuleManager --- .../modulesystem/ModuleManager.h | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/libcalamaresui/modulesystem/ModuleManager.h b/src/libcalamaresui/modulesystem/ModuleManager.h index 2c51e70f7..2bac78af6 100644 --- a/src/libcalamaresui/modulesystem/ModuleManager.h +++ b/src/libcalamaresui/modulesystem/ModuleManager.h @@ -103,10 +103,36 @@ public: RequirementsModel* requirementsModel() { return m_requirementsModel; } signals: + /** @brief Emitted when all the module **configuration** has been read + * + * This indicates that all of the module.desc files have been + * loaded; bad ones are silently skipped, so this just indicates + * that the module manager is ready for the next phase (loading). + */ void initDone(); - void modulesLoaded(); /// All of the modules were loaded successfully - void modulesFailed( QStringList ); /// .. or not - // Below, see RequirementsChecker documentation + /** @brief Emitted when all the modules are loaded successfully + * + * Each module listed in the settings is loaded. Modules are loaded + * only once, even when instantiated multiple times. If all of + * the listed modules are successfully loaded, this signal is + * emitted (otherwise, it isn't, so you need to wait for **both** + * of the signals). + * + * If this is emitted (i.e. all modules have loaded) then the next + * phase, requirements checking, can be started. + */ + void modulesLoaded(); + /** @brief Emitted if any modules failed to load + * + * Modules that failed to load (for any reason) are listed by + * instance key (e.g. "welcome@welcome", "shellprocess@mycustomthing"). + */ + void modulesFailed( QStringList ); + /** @brief Emitted after all requirements have been checked + * + * The bool value indicates if all of the **mandatory** requirements + * are satisfied (e.g. whether installation can continue). + */ void requirementsComplete( bool ); private slots: From a25d61077fa09d835387caa9384a6f7a054ed315 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 22 Jul 2020 11:53:06 +0200 Subject: [PATCH 035/113] [locale] Add GeoIP settings to Config - this doesn't do the lookup **yet** - while here, refactor setConfigurationMap so it reads like a story, with chunks bitten out into a handful of static inline void methods. --- src/modules/locale/Config.cpp | 58 ++++++++++++++++++++++++++++------- src/modules/locale/Config.h | 11 +++++++ 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index 0ecd7e049..fc797c0aa 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -327,43 +327,50 @@ Config::currentLCStatus() const .arg( localeLabel( m_selectedLocaleConfiguration.lc_numeric ) ); } - -void -Config::setConfigurationMap( const QVariantMap& configurationMap ) +static inline void +getLocaleGenLines( const QVariantMap& configurationMap, QStringList& localeGenLines ) { QString localeGenPath = CalamaresUtils::getString( configurationMap, "localeGenPath" ); if ( localeGenPath.isEmpty() ) { localeGenPath = QStringLiteral( "/etc/locale.gen" ); } - m_localeGenLines = loadLocales( localeGenPath ); + localeGenLines = loadLocales( localeGenPath ); +} - m_adjustLiveTimezone +static inline void +getAdjustLiveTimezone( const QVariantMap& configurationMap, bool& adjustLiveTimezone ) +{ + adjustLiveTimezone = CalamaresUtils::getBool( configurationMap, "adjustLiveTimezone", Calamares::Settings::instance()->doChroot() ); #ifdef DEBUG_TIMEZONES if ( m_adjustLiveTimezone ) { cWarning() << "Turning off live-timezone adjustments because debugging is on."; - m_adjustLiveTimezone = false; + adjustLiveTimezone = false; } #endif #ifdef __FreeBSD__ - if ( m_adjustLiveTimezone ) + if ( adjustLiveTimezone ) { cWarning() << "Turning off live-timezone adjustments on FreeBSD."; - m_adjustLiveTimezone = false; + adjustLiveTimezone = false; } #endif +} +static inline void +getStartingTimezone( const QVariantMap& configurationMap, CalamaresUtils::GeoIP::RegionZonePair& startingTimezone ) +{ QString region = CalamaresUtils::getString( configurationMap, "region" ); QString zone = CalamaresUtils::getString( configurationMap, "zone" ); if ( !region.isEmpty() && !zone.isEmpty() ) { - m_startingTimezone = CalamaresUtils::GeoIP::RegionZonePair( region, zone ); + startingTimezone = CalamaresUtils::GeoIP::RegionZonePair( region, zone ); } else { - m_startingTimezone + startingTimezone = CalamaresUtils::GeoIP::RegionZonePair( QStringLiteral( "America" ), QStringLiteral( "New_York" ) ); } @@ -372,11 +379,40 @@ Config::setConfigurationMap( const QVariantMap& configurationMap ) auto systemtz = CalamaresUtils::GeoIP::splitTZString( QTimeZone::systemTimeZoneId() ); if ( systemtz.isValid() ) { - m_startingTimezone = systemtz; + cDebug() << "Overriding configured timezone" << startingTimezone << "with system timezone" << systemtz; + startingTimezone = systemtz; } } } +static inline void +getGeoIP( const QVariantMap& configurationMap, std::unique_ptr< CalamaresUtils::GeoIP::Handler >& geoip ) +{ + bool ok = false; + QVariantMap map = CalamaresUtils::getSubMap( configurationMap, "geoip", ok ); + if ( ok ) + { + QString url = CalamaresUtils::getString( map, "url" ); + QString style = CalamaresUtils::getString( map, "style" ); + QString selector = CalamaresUtils::getString( map, "selector" ); + + geoip = std::make_unique< CalamaresUtils::GeoIP::Handler >( style, url, selector ); + if ( !geoip->isValid() ) + { + cWarning() << "GeoIP Style" << style << "is not recognized."; + } + } +} + +void +Config::setConfigurationMap( const QVariantMap& configurationMap ) +{ + getLocaleGenLines( configurationMap, m_localeGenLines ); + getAdjustLiveTimezone( configurationMap, m_adjustLiveTimezone ); + getStartingTimezone( configurationMap, m_startingTimezone ); + getGeoIP( configurationMap, m_geoip ); +} + Calamares::JobList Config::createJobs() { diff --git a/src/modules/locale/Config.h b/src/modules/locale/Config.h index 7e634a4a8..421bb7998 100644 --- a/src/modules/locale/Config.h +++ b/src/modules/locale/Config.h @@ -24,6 +24,7 @@ #include "LocaleConfiguration.h" #include "Job.h" +#include "geoip/Handler.h" #include "geoip/Interface.h" #include "locale/TimeZone.h" @@ -136,8 +137,18 @@ private: bool m_adjustLiveTimezone; /** @brief The initial timezone (region, zone) specified in the config. + * + * This may be overridden by setting *useSystemTimezone* or by + * GeoIP settings. */ CalamaresUtils::GeoIP::RegionZonePair m_startingTimezone; + + /** @brief Handler for GeoIP lookup (if configured) + * + * The GeoIP lookup needs to be started at some suitable time, + * by explicitly calling *TODO* + */ + std::unique_ptr< CalamaresUtils::GeoIP::Handler > m_geoip; }; From 42331f6e139c2b24a741a6ad60e948fd3af79f9c Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 22 Jul 2020 14:47:43 +0200 Subject: [PATCH 036/113] [locale] Move GeoIP lookup to config - replace the weird synchronous-lookup-during-requirements-checking with a proper async lookup when the system is ready. --- src/modules/locale/Config.cpp | 61 +++++++++++++++++++++++-- src/modules/locale/Config.h | 10 +++- src/modules/locale/LocaleViewStep.cpp | 66 +-------------------------- src/modules/locale/LocaleViewStep.h | 7 --- 4 files changed, 68 insertions(+), 76 deletions(-) diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index fc797c0aa..42b6d616f 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -26,6 +26,8 @@ #include "JobQueue.h" #include "Settings.h" #include "locale/Label.h" +#include "modulesystem/ModuleManager.h" +#include "network/Manager.h" #include "utils/Logger.h" #include "utils/Variant.h" @@ -204,6 +206,15 @@ Config::timezoneData() const return ::timezoneData(); } +void +Config::setCurrentLocation() +{ + if ( !m_currentLocation && m_startingTimezone.isValid() ) + { + setCurrentLocation( m_startingTimezone.first, m_startingTimezone.second ); + } +} + void Config::setCurrentLocation( const QString& regionName, const QString& zoneName ) { @@ -252,7 +263,6 @@ Config::setCurrentLocation( const CalamaresUtils::Locale::TZZone* location ) } } - LocaleConfiguration Config::automaticLocaleConfiguration() const { @@ -341,8 +351,8 @@ getLocaleGenLines( const QVariantMap& configurationMap, QStringList& localeGenLi static inline void getAdjustLiveTimezone( const QVariantMap& configurationMap, bool& adjustLiveTimezone ) { - adjustLiveTimezone - = CalamaresUtils::getBool( configurationMap, "adjustLiveTimezone", Calamares::Settings::instance()->doChroot() ); + adjustLiveTimezone = CalamaresUtils::getBool( + configurationMap, "adjustLiveTimezone", Calamares::Settings::instance()->doChroot() ); #ifdef DEBUG_TIMEZONES if ( m_adjustLiveTimezone ) { @@ -411,6 +421,12 @@ Config::setConfigurationMap( const QVariantMap& configurationMap ) getAdjustLiveTimezone( configurationMap, m_adjustLiveTimezone ); getStartingTimezone( configurationMap, m_startingTimezone ); getGeoIP( configurationMap, m_geoip ); + + if ( m_geoip && m_geoip->isValid() ) + { + connect( + Calamares::ModuleManager::instance(), &Calamares::ModuleManager::modulesLoaded, this, &Config::startGeoIP ); + } } Calamares::JobList @@ -427,3 +443,42 @@ Config::createJobs() return list; } + +void +Config::startGeoIP() +{ + if ( m_geoip && m_geoip->isValid() ) + { + auto& network = CalamaresUtils::Network::Manager::instance(); + if ( network.hasInternet() || network.synchronousPing( m_geoip->url() ) ) + { + using Watcher = QFutureWatcher< CalamaresUtils::GeoIP::RegionZonePair >; + m_geoipWatcher = std::make_unique< Watcher >(); + m_geoipWatcher->setFuture( m_geoip->query() ); + connect( m_geoipWatcher.get(), &Watcher::finished, this, &Config::completeGeoIP ); + } + } +} + +void +Config::completeGeoIP() +{ + if ( !currentLocation() ) + { + auto r = m_geoipWatcher->result(); + if ( r.isValid() ) + { + m_startingTimezone = r; + } + else + { + cWarning() << "GeoIP returned invalid result."; + } + } + else + { + cWarning() << "GeoIP result ignored because a location is already set."; + } + m_geoipWatcher.reset(); + m_geoip.reset(); +} diff --git a/src/modules/locale/Config.h b/src/modules/locale/Config.h index 421bb7998..3d048bc28 100644 --- a/src/modules/locale/Config.h +++ b/src/modules/locale/Config.h @@ -28,6 +28,7 @@ #include "geoip/Interface.h" #include "locale/TimeZone.h" +#include #include #include @@ -59,7 +60,6 @@ public: /** @brief The currently selected location (timezone) * * The location is a pointer into the date that timezoneData() returns. - * It is possible to return nullptr, if no location has been picked yet. */ const CalamaresUtils::Locale::TZZone* currentLocation() const { return m_currentLocation; } @@ -79,6 +79,9 @@ public: CalamaresUtils::Locale::CStringListModel* regionModel() const { return m_regionModel.get(); } CalamaresUtils::Locale::CStringListModel* zonesModel() const { return m_zonesModel.get(); } + /// Special case, set location from starting timezone if not already set + void setCurrentLocation(); + public Q_SLOTS: /// Set a language by user-choice, overriding future location changes void setLanguageExplicitly( const QString& language ); @@ -149,6 +152,11 @@ private: * by explicitly calling *TODO* */ std::unique_ptr< CalamaresUtils::GeoIP::Handler > m_geoip; + + // Implementation details for doing GeoIP lookup + void startGeoIP(); + void completeGeoIP(); + std::unique_ptr< QFutureWatcher< CalamaresUtils::GeoIP::RegionZonePair > > m_geoipWatcher; }; diff --git a/src/modules/locale/LocaleViewStep.cpp b/src/modules/locale/LocaleViewStep.cpp index 5baf68b43..8ae894aa8 100644 --- a/src/modules/locale/LocaleViewStep.cpp +++ b/src/modules/locale/LocaleViewStep.cpp @@ -43,7 +43,6 @@ LocaleViewStep::LocaleViewStep( QObject* parent ) , m_widget( new QWidget() ) , m_actualWidget( nullptr ) , m_nextEnabled( false ) - , m_geoip( nullptr ) , m_config( std::make_unique< Config >() ) { QBoxLayout* mainLayout = new QHBoxLayout; @@ -66,11 +65,11 @@ LocaleViewStep::~LocaleViewStep() void LocaleViewStep::setUpPage() { + m_config->setCurrentLocation(); if ( !m_actualWidget ) { m_actualWidget = new LocalePage( m_config.get() ); } - m_config->setCurrentLocation( m_startingTimezone.first, m_startingTimezone.second ); m_widget->layout()->addWidget( m_actualWidget ); ensureSize( m_actualWidget->sizeHint() ); @@ -80,20 +79,6 @@ LocaleViewStep::setUpPage() } -void -LocaleViewStep::fetchGeoIpTimezone() -{ - if ( m_geoip && m_geoip->isValid() ) - { - m_startingTimezone = m_geoip->get(); - if ( !m_startingTimezone.isValid() ) - { - cWarning() << "GeoIP lookup at" << m_geoip->url() << "failed."; - } - } -} - - QString LocaleViewStep::prettyName() const { @@ -171,54 +156,5 @@ LocaleViewStep::onLeave() void LocaleViewStep::setConfigurationMap( const QVariantMap& configurationMap ) { - QString region = CalamaresUtils::getString( configurationMap, "region" ); - QString zone = CalamaresUtils::getString( configurationMap, "zone" ); - if ( !region.isEmpty() && !zone.isEmpty() ) - { - m_startingTimezone = CalamaresUtils::GeoIP::RegionZonePair( region, zone ); - } - else - { - m_startingTimezone - = CalamaresUtils::GeoIP::RegionZonePair( QStringLiteral( "America" ), QStringLiteral( "New_York" ) ); - } - - bool ok = false; - QVariantMap geoip = CalamaresUtils::getSubMap( configurationMap, "geoip", ok ); - if ( ok ) - { - QString url = CalamaresUtils::getString( geoip, "url" ); - QString style = CalamaresUtils::getString( geoip, "style" ); - QString selector = CalamaresUtils::getString( geoip, "selector" ); - - m_geoip = std::make_unique< CalamaresUtils::GeoIP::Handler >( style, url, selector ); - if ( !m_geoip->isValid() ) - { - cWarning() << "GeoIP Style" << style << "is not recognized."; - } - } - m_config->setConfigurationMap( configurationMap ); } - -Calamares::RequirementsList -LocaleViewStep::checkRequirements() -{ - if ( m_geoip && m_geoip->isValid() ) - { - auto& network = CalamaresUtils::Network::Manager::instance(); - if ( network.hasInternet() ) - { - fetchGeoIpTimezone(); - } - else - { - if ( network.synchronousPing( m_geoip->url() ) ) - { - fetchGeoIpTimezone(); - } - } - } - - return Calamares::RequirementsList(); -} diff --git a/src/modules/locale/LocaleViewStep.h b/src/modules/locale/LocaleViewStep.h index a1456764f..f02a3205d 100644 --- a/src/modules/locale/LocaleViewStep.h +++ b/src/modules/locale/LocaleViewStep.h @@ -58,22 +58,15 @@ public: void setConfigurationMap( const QVariantMap& configurationMap ) override; - /// @brief Do setup (returns empty list) asynchronously - virtual Calamares::RequirementsList checkRequirements() override; - private slots: void setUpPage(); private: - void fetchGeoIpTimezone(); QWidget* m_widget; LocalePage* m_actualWidget; bool m_nextEnabled; - CalamaresUtils::GeoIP::RegionZonePair m_startingTimezone; - std::unique_ptr< CalamaresUtils::GeoIP::Handler > m_geoip; - std::unique_ptr< Config > m_config; }; From 4f684be83dfc58692f94f1ffd6c9cbda5bba6dd4 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 22 Jul 2020 16:55:55 +0200 Subject: [PATCH 037/113] [locale] Avoid crashes in the map widget if there is no current location --- .../locale/timezonewidget/timezonewidget.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/modules/locale/timezonewidget/timezonewidget.cpp b/src/modules/locale/timezonewidget/timezonewidget.cpp index df1142e17..0972e3296 100644 --- a/src/modules/locale/timezonewidget/timezonewidget.cpp +++ b/src/modules/locale/timezonewidget/timezonewidget.cpp @@ -94,11 +94,18 @@ TimeZoneWidget::setCurrentLocation( const CalamaresUtils::Locale::TZZone* locati //### Private //### +struct PainterEnder +{ + QPainter& p; + ~PainterEnder() { p.end(); } +}; + void TimeZoneWidget::paintEvent( QPaintEvent* ) { QFontMetrics fontMetrics( font ); QPainter painter( this ); + PainterEnder painter_end { painter }; painter.setRenderHint( QPainter::Antialiasing ); painter.setFont( font ); @@ -109,6 +116,11 @@ TimeZoneWidget::paintEvent( QPaintEvent* ) // Draw zone image painter.drawImage( 0, 0, currentZoneImage ); + if ( !m_currentLocation ) + { + return; + } + #ifdef DEBUG_TIMEZONES QPoint point = getLocationPosition( m_currentLocation ); // Draw latitude lines @@ -168,8 +180,6 @@ TimeZoneWidget::paintEvent( QPaintEvent* ) painter.setPen( Qt::white ); painter.drawText( rect.x() + 5, rect.bottom() - 4, m_currentLocation ? m_currentLocation->tr() : QString() ); #endif - - painter.end(); } From 824cb4d4b841f59220abd09be8d191c98e93fcf3 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 22 Jul 2020 17:03:25 +0200 Subject: [PATCH 038/113] [locale] As the Page is constructed, it shouldn't change the location - since the Page hooked up a model and changed the region-selection **after** connecting to signals, it would reset the location to Africa/Abijan (alphabetically the first timezone) during construction. Don't do that. --- src/modules/locale/LocalePage.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/modules/locale/LocalePage.cpp b/src/modules/locale/LocalePage.cpp index 3999599c9..406f27a6e 100644 --- a/src/modules/locale/LocalePage.cpp +++ b/src/modules/locale/LocalePage.cpp @@ -98,6 +98,12 @@ LocalePage::LocalePage( Config* config, QWidget* parent ) setMinimumWidth( m_tzWidget->width() ); setLayout( mainLayout ); + // Set up the location before connecting signals, to avoid a signal + // storm as various parts interact. + m_regionCombo->setModel( m_config->regionModel() ); + locationChanged( m_config->currentLocation() ); // doesn't inform TZ widget + m_tzWidget->setCurrentLocation( m_config->currentLocation() ); + connect( config, &Config::currentLCStatusChanged, m_formatsLabel, &QLabel::setText ); connect( config, &Config::currentLanguageStatusChanged, m_localeLabel, &QLabel::setText ); connect( config, &Config::currentLocationChanged, m_tzWidget, &TimeZoneWidget::setCurrentLocation ); @@ -114,9 +120,6 @@ LocalePage::LocalePage( Config* config, QWidget* parent ) connect( m_formatsChangeButton, &QPushButton::clicked, this, &LocalePage::changeFormats ); CALAMARES_RETRANSLATE_SLOT( &LocalePage::updateLocaleLabels ) - - m_regionCombo->setModel( m_config->regionModel() ); - m_regionCombo->currentIndexChanged( m_regionCombo->currentIndex() ); } @@ -178,6 +181,10 @@ LocalePage::zoneChanged( int currentIndex ) void LocalePage::locationChanged( const CalamaresUtils::Locale::TZZone* location ) { + if ( !location ) + { + return; + } cBoolSetter< true > b( m_blockTzWidgetSet ); // Set region index From 1f3cb32486eb3918b977c307276920c384f9ed7a Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 22 Jul 2020 17:10:08 +0200 Subject: [PATCH 039/113] [locale] Apply coding style --- src/modules/locale/timezonewidget/TimeZoneImage.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/locale/timezonewidget/TimeZoneImage.cpp b/src/modules/locale/timezonewidget/TimeZoneImage.cpp index a60c9b7d7..22bd263d6 100644 --- a/src/modules/locale/timezonewidget/TimeZoneImage.cpp +++ b/src/modules/locale/timezonewidget/TimeZoneImage.cpp @@ -25,9 +25,9 @@ #include static const char* zoneNames[] - = { "0.0", "1.0", "2.0", "3.0", "3.5", "4.0", "4.5", "5.0", "5.5", "5.75", "6.0", "6.5", "7.0", - "8.0", "9.0", "9.5", "10.0", "10.5", "11.0", "12.0", "12.75", "13.0", "-1.0", "-2.0", "-3.0", - "-3.5", "-4.0", "-4.5", "-5.0", "-5.5", "-6.0", "-7.0", "-8.0", "-9.0", "-9.5", "-10.0", "-11.0" }; + = { "0.0", "1.0", "2.0", "3.0", "3.5", "4.0", "4.5", "5.0", "5.5", "5.75", "6.0", "6.5", "7.0", + "8.0", "9.0", "9.5", "10.0", "10.5", "11.0", "12.0", "12.75", "13.0", "-1.0", "-2.0", "-3.0", "-3.5", + "-4.0", "-4.5", "-5.0", "-5.5", "-6.0", "-7.0", "-8.0", "-9.0", "-9.5", "-10.0", "-11.0" }; static_assert( TimeZoneImageList::zoneCount == ( sizeof( zoneNames ) / sizeof( zoneNames[ 0 ] ) ), "Incorrect number of zones" ); From d90d451f427497e13ab1f7fc527ce796305e53ed Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Thu, 23 Jul 2020 10:43:31 +0200 Subject: [PATCH 040/113] [locale] Remove unnecessary includes --- src/modules/locale/LocaleViewStep.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/locale/LocaleViewStep.h b/src/modules/locale/LocaleViewStep.h index f02a3205d..0a40da861 100644 --- a/src/modules/locale/LocaleViewStep.h +++ b/src/modules/locale/LocaleViewStep.h @@ -23,8 +23,6 @@ #include "Config.h" #include "DllMacro.h" -#include "geoip/Handler.h" -#include "geoip/Interface.h" #include "utils/PluginFactory.h" #include "viewpages/ViewStep.h" From 4b7403d115a485ef679f0e35d09a92dd50316197 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Thu, 23 Jul 2020 11:11:18 +0200 Subject: [PATCH 041/113] [localeq] Re-do with new Config - remove stray and useless TODOs - remove unnecessary empty overrides - clean up includes - drop all the code that is now in Config Since the business logic (setting locations, maintaining GS, ...) is all in the Config object, the ViewStep is remarkably simple: hook up a UI to the Config, which in the case of QML is done automatically. --- src/modules/localeq/LocaleQmlViewStep.cpp | 135 ++-------------------- src/modules/localeq/LocaleQmlViewStep.h | 26 +---- 2 files changed, 11 insertions(+), 150 deletions(-) diff --git a/src/modules/localeq/LocaleQmlViewStep.cpp b/src/modules/localeq/LocaleQmlViewStep.cpp index 4354cb7bd..7891f73fd 100644 --- a/src/modules/localeq/LocaleQmlViewStep.cpp +++ b/src/modules/localeq/LocaleQmlViewStep.cpp @@ -19,30 +19,13 @@ #include "LocaleQmlViewStep.h" -#include "GlobalStorage.h" -#include "JobQueue.h" - -#include "geoip/Handler.h" -#include "network/Manager.h" -#include "utils/CalamaresUtilsGui.h" #include "utils/Logger.h" -#include "utils/Variant.h" -#include "utils/Yaml.h" - -#include "Branding.h" -#include "modulesystem/ModuleManager.h" -#include -#include -#include -#include CALAMARES_PLUGIN_FACTORY_DEFINITION( LocaleQmlViewStepFactory, registerPlugin< LocaleQmlViewStep >(); ) LocaleQmlViewStep::LocaleQmlViewStep( QObject* parent ) -: Calamares::QmlViewStep( parent ) -, m_config( new Config( this ) ) -, m_nextEnabled( false ) -, m_geoip( nullptr ) + : Calamares::QmlViewStep( parent ) + , m_config( std::make_unique< Config >( this ) ) { emit nextStatusChanged( m_nextEnabled ); } @@ -50,43 +33,7 @@ LocaleQmlViewStep::LocaleQmlViewStep( QObject* parent ) QObject* LocaleQmlViewStep::getConfig() { - return m_config; -} - -void -LocaleQmlViewStep::fetchGeoIpTimezone() -{ - if ( m_geoip && m_geoip->isValid() ) - { - m_startingTimezone = m_geoip->get(); - if ( !m_startingTimezone.isValid() ) - { - cWarning() << "GeoIP lookup at" << m_geoip->url() << "failed."; - } - } - - // m_config->setLocaleInfo(m_startingTimezone.first, m_startingTimezone.second, m_localeGenPath); -} - -Calamares::RequirementsList LocaleQmlViewStep::checkRequirements() -{ - if ( m_geoip && m_geoip->isValid() ) - { - auto& network = CalamaresUtils::Network::Manager::instance(); - if ( network.hasInternet() ) - { - fetchGeoIpTimezone(); - } - else - { - if ( network.synchronousPing( m_geoip->url() ) ) - { - fetchGeoIpTimezone(); - } - } - } - - return Calamares::RequirementsList(); + return m_config.get(); } QString @@ -98,14 +45,12 @@ LocaleQmlViewStep::prettyName() const bool LocaleQmlViewStep::isNextEnabled() const { - // TODO: should return true return true; } bool LocaleQmlViewStep::isBackEnabled() const { - // TODO: should return true (it's weird that you are not allowed to have welcome *after* anything return true; } @@ -113,7 +58,6 @@ LocaleQmlViewStep::isBackEnabled() const bool LocaleQmlViewStep::isAtBeginning() const { - // TODO: adjust to "pages" in the QML return true; } @@ -121,81 +65,18 @@ LocaleQmlViewStep::isAtBeginning() const bool LocaleQmlViewStep::isAtEnd() const { - // TODO: adjust to "pages" in the QML return true; } Calamares::JobList LocaleQmlViewStep::jobs() const { - return m_jobs; + return m_config->createJobs(); } -void LocaleQmlViewStep::onActivate() +void +LocaleQmlViewStep::setConfigurationMap( const QVariantMap& configurationMap ) { - // TODO no sure if it is needed at all or for the abstract class to start something -} - -void LocaleQmlViewStep::onLeave() -{ -#if 0 - if ( true ) - { - m_jobs = m_config->createJobs(); -// m_prettyStatus = m_actualWidget->prettyStatus(); - - auto map = m_config->localesMap(); - QVariantMap vm; - for ( auto it = map.constBegin(); it != map.constEnd(); ++it ) - { - vm.insert( it.key(), it.value() ); - } - - Calamares::JobQueue::instance()->globalStorage()->insert( "localeConf", vm ); - } - else - { - m_jobs.clear(); - Calamares::JobQueue::instance()->globalStorage()->remove( "localeConf" ); - } -#endif -} - -void LocaleQmlViewStep::setConfigurationMap(const QVariantMap& configurationMap) -{ - QString region = CalamaresUtils::getString( configurationMap, "region" ); - QString zone = CalamaresUtils::getString( configurationMap, "zone" ); - if ( !region.isEmpty() && !zone.isEmpty() ) - { - m_startingTimezone = CalamaresUtils::GeoIP::RegionZonePair( region, zone ); - } - else - { - m_startingTimezone - = CalamaresUtils::GeoIP::RegionZonePair( QStringLiteral( "America" ), QStringLiteral( "New_York" ) ); - } - - m_localeGenPath = CalamaresUtils::getString( configurationMap, "localeGenPath" ); - if ( m_localeGenPath.isEmpty() ) - { - m_localeGenPath = QStringLiteral( "/etc/locale.gen" ); - } - - bool ok = false; - QVariantMap geoip = CalamaresUtils::getSubMap( configurationMap, "geoip", ok ); - if ( ok ) - { - QString url = CalamaresUtils::getString( geoip, "url" ); - QString style = CalamaresUtils::getString( geoip, "style" ); - QString selector = CalamaresUtils::getString( geoip, "selector" ); - - m_geoip = std::make_unique< CalamaresUtils::GeoIP::Handler >( style, url, selector ); - if ( !m_geoip->isValid() ) - { - cWarning() << "GeoIP Style" << style << "is not recognized."; - } - } - - checkRequirements(); - Calamares::QmlViewStep::setConfigurationMap( configurationMap ); // call parent implementation last + m_config->setConfigurationMap( configurationMap ); + Calamares::QmlViewStep::setConfigurationMap( configurationMap ); // call parent implementation last } diff --git a/src/modules/localeq/LocaleQmlViewStep.h b/src/modules/localeq/LocaleQmlViewStep.h index 0639274d6..20689e149 100644 --- a/src/modules/localeq/LocaleQmlViewStep.h +++ b/src/modules/localeq/LocaleQmlViewStep.h @@ -20,14 +20,10 @@ #define LOCALE_QMLVIEWSTEP_H #include "Config.h" -#include "geoip/Handler.h" -#include "geoip/Interface.h" + +#include "DllMacro.h" #include "utils/PluginFactory.h" #include "viewpages/QmlViewStep.h" -#include - -#include -#include #include @@ -47,28 +43,12 @@ public: bool isAtEnd() const override; Calamares::JobList jobs() const override; - void onActivate() override; - void onLeave() override; void setConfigurationMap( const QVariantMap& configurationMap ) override; QObject* getConfig() override; - virtual Calamares::RequirementsList checkRequirements() override; - private: - // TODO: a generic QML viewstep should return a config object from a method - Config *m_config; - - bool m_nextEnabled; - QString m_prettyStatus; - - CalamaresUtils::GeoIP::RegionZonePair m_startingTimezone; - QString m_localeGenPath; - - Calamares::JobList m_jobs; - std::unique_ptr< CalamaresUtils::GeoIP::Handler > m_geoip; - - void fetchGeoIpTimezone(); + std::unique_ptr< Config > m_config; }; CALAMARES_PLUGIN_FACTORY_DECLARATION( LocaleQmlViewStepFactory ) From 51e743a67f9122c566c497ef0413fa8109444d75 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Thu, 23 Jul 2020 12:48:18 +0200 Subject: [PATCH 042/113] [libcalamares] Give GlobalStorage a parent --- src/libcalamares/GlobalStorage.cpp | 6 +++--- src/libcalamares/GlobalStorage.h | 4 ++-- src/libcalamares/JobQueue.cpp | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libcalamares/GlobalStorage.cpp b/src/libcalamares/GlobalStorage.cpp index d58a3b0c6..341fc3892 100644 --- a/src/libcalamares/GlobalStorage.cpp +++ b/src/libcalamares/GlobalStorage.cpp @@ -1,5 +1,5 @@ /* === This file is part of Calamares - === - * + * * SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac * SPDX-FileCopyrightText: 2017-2018 Adriaan de Groot * @@ -36,8 +36,8 @@ using CalamaresUtils::operator""_MiB; namespace Calamares { -GlobalStorage::GlobalStorage() - : QObject( nullptr ) +GlobalStorage::GlobalStorage( QObject* parent ) + : QObject( parent ) { } diff --git a/src/libcalamares/GlobalStorage.h b/src/libcalamares/GlobalStorage.h index e9ba1da8a..a2848f888 100644 --- a/src/libcalamares/GlobalStorage.h +++ b/src/libcalamares/GlobalStorage.h @@ -1,5 +1,5 @@ /* === This file is part of Calamares - === - * + * * SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac * SPDX-FileCopyrightText: 2017-2018 Adriaan de Groot * @@ -39,7 +39,7 @@ class GlobalStorage : public QObject { Q_OBJECT public: - explicit GlobalStorage(); + explicit GlobalStorage( QObject* parent = nullptr ); //NOTE: thread safety is guaranteed by JobQueue, which executes jobs one by one. // If at any time jobs become concurrent, this class must be made thread-safe. diff --git a/src/libcalamares/JobQueue.cpp b/src/libcalamares/JobQueue.cpp index adff9464b..64cc4794d 100644 --- a/src/libcalamares/JobQueue.cpp +++ b/src/libcalamares/JobQueue.cpp @@ -1,5 +1,5 @@ /* === This file is part of Calamares - === - * + * * SPDX-FileCopyrightText: 2014-2015 Teo Mrnjavac * SPDX-FileCopyrightText: 2018 Adriaan de Groot * @@ -170,7 +170,7 @@ JobQueue::globalStorage() const JobQueue::JobQueue( QObject* parent ) : QObject( parent ) , m_thread( new JobThread( this ) ) - , m_storage( new GlobalStorage() ) + , m_storage( new GlobalStorage( this ) ) { Q_ASSERT( !s_instance ); s_instance = this; From 36fb1124be8b0a215e0eb88418bf042bf3861792 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Thu, 23 Jul 2020 12:57:01 +0200 Subject: [PATCH 043/113] [libcalamares] Export network status as Q_PROPERTY and to QML --- src/libcalamares/network/Manager.cpp | 8 ++++++-- src/libcalamares/network/Manager.h | 25 ++++++++++++++++++------- src/libcalamaresui/utils/Qml.cpp | 5 +++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/libcalamares/network/Manager.cpp b/src/libcalamares/network/Manager.cpp index d70988a0a..9d7534e99 100644 --- a/src/libcalamares/network/Manager.cpp +++ b/src/libcalamares/network/Manager.cpp @@ -1,5 +1,5 @@ /* === This file is part of Calamares - === - * + * * SPDX-FileCopyrightText: 2019 Adriaan de Groot * * Calamares is free software: you can redistribute it and/or modify @@ -168,7 +168,11 @@ Manager::checkHasInternet() { hasInternet = synchronousPing( d->m_hasInternetUrl ); } - d->m_hasInternet = hasInternet; + if ( hasInternet != d->m_hasInternet ) + { + d->m_hasInternet = hasInternet; + emit hasInternetChanged( hasInternet ); + } return hasInternet; } diff --git a/src/libcalamares/network/Manager.h b/src/libcalamares/network/Manager.h index 1ba3eb411..8673d340b 100644 --- a/src/libcalamares/network/Manager.h +++ b/src/libcalamares/network/Manager.h @@ -1,5 +1,5 @@ /* === This file is part of Calamares - === - * + * * SPDX-FileCopyrightText: 2019 Adriaan de Groot * * Calamares is free software: you can redistribute it and/or modify @@ -98,9 +98,10 @@ struct RequestStatus QDebug& operator<<( QDebug& s, const RequestStatus& e ); -class DLLEXPORT Manager : QObject +class DLLEXPORT Manager : public QObject { Q_OBJECT + Q_PROPERTY( bool hasInternet READ hasInternet NOTIFY hasInternetChanged FINAL ) Manager(); @@ -133,6 +134,16 @@ public: /// @brief Set the URL which is used for the general "is there internet" check. void setCheckHasInternetUrl( const QUrl& url ); + + /** @brief Do a network request asynchronously. + * + * Returns a pointer to the reply-from-the-request. + * This may be a nullptr if an error occurs immediately. + * The caller is responsible for cleaning up the reply (eventually). + */ + QNetworkReply* asynchronousGet( const QUrl& url, const RequestOptions& options = RequestOptions() ); + +public Q_SLOTS: /** @brief Do an explicit check for internet connectivity. * * This **may** do a ping to the configured check URL, but can also @@ -148,13 +159,13 @@ public: */ bool hasInternet(); - /** @brief Do a network request asynchronously. +signals: + /** @brief Indicates that internet connectivity status has changed * - * Returns a pointer to the reply-from-the-request. - * This may be a nullptr if an error occurs immediately. - * The caller is responsible for cleaning up the reply (eventually). + * The value is that returned from hasInternet() -- @c true when there + * is connectivity, @c false otherwise. */ - QNetworkReply* asynchronousGet( const QUrl& url, const RequestOptions& options = RequestOptions() ); + void hasInternetChanged( bool ); private: class Private; diff --git a/src/libcalamaresui/utils/Qml.cpp b/src/libcalamaresui/utils/Qml.cpp index 4f53aa317..1f1152fa2 100644 --- a/src/libcalamaresui/utils/Qml.cpp +++ b/src/libcalamaresui/utils/Qml.cpp @@ -23,6 +23,7 @@ #include "JobQueue.h" #include "Settings.h" #include "ViewManager.h" +#include "network/Manager.h" #include "utils/Dirs.h" #include "utils/Logger.h" @@ -242,6 +243,10 @@ registerQmlModels() "io.calamares.core", 1, 0, "Global", []( QQmlEngine*, QJSEngine* ) -> QObject* { return Calamares::JobQueue::instance()->globalStorage(); } ); + qmlRegisterSingletonType< CalamaresUtils::Network::Manager >( + "io.calamares.core", 1, 0, "Network", []( QQmlEngine*, QJSEngine* ) -> QObject* { + return &CalamaresUtils::Network::Manager::instance(); + } ); } } From fb927c976397d57d45a8bff6008fa8b356ac8583 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Thu, 23 Jul 2020 12:57:26 +0200 Subject: [PATCH 044/113] [localeq] Use network-connected property to direct map-loading --- src/modules/localeq/localeq.qml | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/src/modules/localeq/localeq.qml b/src/modules/localeq/localeq.qml index ffd87f5b5..df82b1f9b 100644 --- a/src/modules/localeq/localeq.qml +++ b/src/modules/localeq/localeq.qml @@ -31,41 +31,13 @@ Page { property var confLang: "American English" property var confLocale: "Nederland" - //Needs to come from .conf/geoip - property var hasInternet: true - - function getInt(format) { - var requestURL = "https://example.org/"; - var xhr = new XMLHttpRequest; - - xhr.onreadystatechange = function() { - if (xhr.readyState === XMLHttpRequest.DONE) { - - if (xhr.status !== 200) { - console.log("Disconnected!!"); - var connected = false - hasInternet = connected - return; - } - - else { - console.log("Connected!!"); - } - } - } - xhr.open("GET", requestURL, true); - xhr.send(); - } - Component.onCompleted: { - getInt(); - } Loader { id: image anchors.horizontalCenter: parent.horizontalCenter width: parent.width height: parent.height / 1.28 - source: (hasInternet) ? "Map.qml" : "Offline.qml" + source: (Network.hasInternet) ? "Map.qml" : "Offline.qml" } RowLayout { From fdbfbfe2845e735ae9d3c1e2d46d355e6fde95b6 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Thu, 23 Jul 2020 17:46:20 +0200 Subject: [PATCH 045/113] [localeq] Fix build, missed one case of removed member variable --- src/modules/localeq/LocaleQmlViewStep.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/localeq/LocaleQmlViewStep.cpp b/src/modules/localeq/LocaleQmlViewStep.cpp index 7891f73fd..e0ef75f63 100644 --- a/src/modules/localeq/LocaleQmlViewStep.cpp +++ b/src/modules/localeq/LocaleQmlViewStep.cpp @@ -27,7 +27,6 @@ LocaleQmlViewStep::LocaleQmlViewStep( QObject* parent ) : Calamares::QmlViewStep( parent ) , m_config( std::make_unique< Config >( this ) ) { - emit nextStatusChanged( m_nextEnabled ); } QObject* From 75da1bece4f5da57275e87a3ce276efe3fdd2c83 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Thu, 23 Jul 2020 23:25:52 +0200 Subject: [PATCH 046/113] [locale] Add properties for language and LC codes - we already had the human-readable status strings, but also want the actual code (particularly for being able to **update** the code from QML) --- src/modules/locale/Config.cpp | 2 ++ src/modules/locale/Config.h | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index 42b6d616f..21f4d90a7 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -288,6 +288,7 @@ Config::setLanguageExplicitly( const QString& language ) m_selectedLocaleConfiguration.explicit_lang = true; emit currentLanguageStatusChanged( currentLanguageStatus() ); + emit currentLanguageCodeChanged( currentLanguageCode() ); } void @@ -306,6 +307,7 @@ Config::setLCLocaleExplicitly( const QString& locale ) m_selectedLocaleConfiguration.explicit_lc = true; emit currentLCStatusChanged( currentLCStatus() ); + emit currentLCCodeChanged( currentLCCode() ); } QString diff --git a/src/modules/locale/Config.h b/src/modules/locale/Config.h index 3d048bc28..484c1032e 100644 --- a/src/modules/locale/Config.h +++ b/src/modules/locale/Config.h @@ -43,9 +43,13 @@ class Config : public QObject Q_PROPERTY( const CalamaresUtils::Locale::TZZone* currentLocation READ currentLocation WRITE setCurrentLocation NOTIFY currentLocationChanged ) + // Status are complete, human-readable, messages Q_PROPERTY( QString currentLocationStatus READ currentLocationStatus NOTIFY currentLanguageStatusChanged ) Q_PROPERTY( QString currentLanguageStatus READ currentLanguageStatus NOTIFY currentLanguageStatusChanged ) Q_PROPERTY( QString currentLCStatus READ currentLCStatus NOTIFY currentLCStatusChanged ) + // Code are internal identifiers, like "en_US.UTF-8" + Q_PROPERTY( QString currentLanguageCode READ currentLanguageCode WRITE setLanguageExplicitly NOTIFY currentLanguageCodeChanged ) + Q_PROPERTY( QString currentLCCode READ currentLCCode WRITE setLCLocaleExplicitly NOTIFY currentLCCodeChanged ) public: Config( QObject* parent = nullptr ); @@ -103,11 +107,16 @@ public Q_SLOTS: */ void setCurrentLocation( const CalamaresUtils::Locale::TZZone* location ); + QString currentLanguageCode() const { return localeConfiguration().language(); } + QString currentLCCode() const { return localeConfiguration().lc_numeric; } + signals: void currentLocationChanged( const CalamaresUtils::Locale::TZZone* location ) const; void currentLocationStatusChanged( const QString& ) const; void currentLanguageStatusChanged( const QString& ) const; void currentLCStatusChanged( const QString& ) const; + void currentLanguageCodeChanged( const QString& ) const; + void currentLCCodeChanged( const QString& ) const; private: /// A list of supported locale identifiers (e.g. "en_US.UTF-8") From 00e945434453841c09269e0384bffe7b1d047ef1 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Thu, 23 Jul 2020 23:26:15 +0200 Subject: [PATCH 047/113] [localeq] Hook up to Config object - get network status from the global Network object; document that - get the strings describing the language and LC settings from the config-object instead of roll-our-own - use the model of supported locales from Config to populate listboxes - connect selection of language or LC to the Config object --- src/modules/localeq/i18n.qml | 20 +++++++------------- src/modules/localeq/localeq.qml | 7 ++++--- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/modules/localeq/i18n.qml b/src/modules/localeq/i18n.qml index a806d0174..eea9864a3 100644 --- a/src/modules/localeq/i18n.qml +++ b/src/modules/localeq/i18n.qml @@ -32,10 +32,6 @@ Item { anchors.fill: parent } - //Needs to come from Locale config - property var confLang: "en_US.UTF8" - property var confLocale: "nl_NL.UTF8" - Rectangle { id: textArea x: 28 @@ -57,7 +53,7 @@ Item { width: 240 wrapMode: Text.WordWrap text: qsTr("

Languages


- The system locale setting affects the language and character set for some command line user interface elements. The current setting is %1.").arg(confLang) + The system locale setting affects the language and character set for some command line user interface elements. The current setting is %1.").arg(config.currentLanguageCode) font.pointSize: 10 } } @@ -76,8 +72,7 @@ Item { id: list1 focus: true - // bogus entries, need to come from Locale config - model: ["en_GB.UTF-8 UTF-8", "en_US.UTF-8 UTF-8 ", "nl_NL.UTF-8 UTF-8", "en_GB.UTF-8 UTF-8", "en_US.UTF-8 UTF-8 ", "nl_NL.UTF-8 UTF-8", "en_GB.UTF-8 UTF-8", "en_US.UTF-8 UTF-8 ", "nl_NL.UTF-8 UTF-8", "en_GB.UTF-8 UTF-8", "en_US.UTF-8 UTF-8 ", "nl_NL.UTF-8 UTF-8", "en_GB.UTF-8 UTF-8", "en_US.UTF-8 UTF-8 ", "nl_NL.UTF-8 UTF-8"] + model: config.supportedLocales currentIndex: 1 highlight: Rectangle { @@ -95,17 +90,17 @@ Item { } onClicked: { list1.currentIndex = index - confLang = list1.currentIndex } } } + onCurrentItemChanged: { config.currentLanguageCode = model[currentIndex] } /* This works because model is a stringlist */ } } } } Column { - id: i18n + id: lc_numeric x: 430 y: 40 @@ -118,7 +113,7 @@ Item { width: 240 wrapMode: Text.WordWrap text: qsTr("

Locales


- The system locale setting affects the language and character set for some command line user interface elements. The current setting is %1.").arg(confLocale) + The system locale setting affects the numbers and dates format. The current setting is %1.").arg(config.currentLCCode) font.pointSize: 10 } } @@ -139,7 +134,7 @@ Item { focus: true // bogus entries, need to come from Locale config - model: ["en_GB.UTF-8 UTF-8", "en_US.UTF-8 UTF-8 ", "nl_NL.UTF-8 UTF-8", "en_GB.UTF-8 UTF-8", "en_US.UTF-8 UTF-8 ", "nl_NL.UTF-8 UTF-8", "en_GB.UTF-8 UTF-8", "en_US.UTF-8 UTF-8 ", "nl_NL.UTF-8 UTF-8", "en_GB.UTF-8 UTF-8", "en_US.UTF-8 UTF-8 ", "nl_NL.UTF-8 UTF-8", "en_GB.UTF-8 UTF-8", "en_US.UTF-8 UTF-8 ", "nl_NL.UTF-8 UTF-8"] + model: config.supportedLocales currentIndex: 2 highlight: Rectangle { @@ -154,11 +149,10 @@ Item { cursorShape: Qt.PointingHandCursor onClicked: { list2.currentIndex = index - confLocale = list1.currentIndex } } } - onCurrentItemChanged: console.debug(currentIndex) + onCurrentItemChanged: { config.currentLCCode = model[currentIndex]; } /* This works because model is a stringlist */ } } } diff --git a/src/modules/localeq/localeq.qml b/src/modules/localeq/localeq.qml index df82b1f9b..c48140d12 100644 --- a/src/modules/localeq/localeq.qml +++ b/src/modules/localeq/localeq.qml @@ -37,7 +37,8 @@ Page { anchors.horizontalCenter: parent.horizontalCenter width: parent.width height: parent.height / 1.28 - source: (Network.hasInternet) ? "Map.qml" : "Offline.qml" + // Network is in io.calamares.core + source: Network.hasInternet ? "Map.qml" : "Offline.qml" } RowLayout { @@ -67,7 +68,7 @@ Page { Label { Layout.fillWidth: true wrapMode: Text.WordWrap - text: qsTr("System language set to %1").arg(confLang) + text: config.currentLanguageStatus } Kirigami.Separator { Layout.fillWidth: true @@ -75,7 +76,7 @@ Page { Label { Layout.fillWidth: true wrapMode: Text.WordWrap - text: qsTr("Numbers and dates locale set to %1").arg(confLocale) + text: config.currentLCStatus } } Button { From e78cde7ccbdcfb3bb739ced851ef7056f1c09017 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Thu, 23 Jul 2020 23:30:43 +0200 Subject: [PATCH 048/113] [locale] Update GS when the LC value changes (not just location) --- src/modules/locale/Config.cpp | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index 21f4d90a7..f0198dd21 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -166,11 +166,23 @@ Config::Config( QObject* parent ) // we don't need to call an update-GS method, or introduce an intermediate // update-thing-and-GS method. And everywhere where we **do** change // language or location, we already emit the signal. - connect( this, &Config::currentLanguageStatusChanged, [&]() { + connect( this, &Config::currentLanguageCodeChanged, [&]() { auto* gs = Calamares::JobQueue::instance()->globalStorage(); gs->insert( "locale", m_selectedLocaleConfiguration.toBcp47() ); } ); + connect( this, &Config::currentLCCodeChanged, [&]() { + auto* gs = Calamares::JobQueue::instance()->globalStorage(); + // Update GS localeConf (the LC_ variables) + auto map = localeConfiguration().toMap(); + QVariantMap vm; + for ( auto it = map.constBegin(); it != map.constEnd(); ++it ) + { + vm.insert( it.key(), it.value() ); + } + gs->insert( "localeConf", vm ); + } ); + connect( this, &Config::currentLocationChanged, [&]() { auto* gs = Calamares::JobQueue::instance()->globalStorage(); @@ -186,15 +198,6 @@ Config::Config( QObject* parent ) QProcess::execute( "timedatectl", // depends on systemd { "set-timezone", location->region() + '/' + location->zone() } ); } - - // Update GS localeConf (the LC_ variables) - auto map = localeConfiguration().toMap(); - QVariantMap vm; - for ( auto it = map.constBegin(); it != map.constEnd(); ++it ) - { - vm.insert( it.key(), it.value() ); - } - gs->insert( "localeConf", vm ); } ); } From a4ed160060b9addd7302f62e227d73a24d4d443c Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Fri, 24 Jul 2020 11:07:58 +0200 Subject: [PATCH 049/113] [localeq] Offer a Config setting to set location from region/zone - already had methods for various kinds of broken-up data, but not one for plain "region/zone" strings; having this makes it easier for QML to report a zone. - use the region/zone method from QML, so that clicking on the world map updates the actual TZ in Config. --- src/modules/locale/Config.cpp | 9 +++++++++ src/modules/locale/Config.h | 9 ++++++++- src/modules/localeq/Map.qml | 7 ++++--- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index f0198dd21..f28a2d0b5 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -218,6 +218,15 @@ Config::setCurrentLocation() } } +void Config::setCurrentLocation(const QString& regionzone) +{ + auto r = CalamaresUtils::GeoIP::splitTZString( regionzone ); + if ( r.isValid() ) + { + setCurrentLocation( r.first, r.second ); + } +} + void Config::setCurrentLocation( const QString& regionName, const QString& zoneName ) { diff --git a/src/modules/locale/Config.h b/src/modules/locale/Config.h index 484c1032e..d74555ee5 100644 --- a/src/modules/locale/Config.h +++ b/src/modules/locale/Config.h @@ -92,7 +92,14 @@ public Q_SLOTS: /// Set LC (formats) by user-choice, overriding future location changes void setLCLocaleExplicitly( const QString& locale ); - /** @brief Sets a location by name + /** @brief Sets a location by full name + * + * @p regionzone should be an identifier from zone.tab, e.g. "Africa/Abidjan", + * which is split into regon and zone. Invalid names will **not** + * change the actual location. + */ + void setCurrentLocation( const QString& regionzone ); + /** @brief Sets a location by split name * * @p region should be "America" or the like, while @p zone * names a zone within that region. diff --git a/src/modules/localeq/Map.qml b/src/modules/localeq/Map.qml index 023de6d1b..080d7388a 100644 --- a/src/modules/localeq/Map.qml +++ b/src/modules/localeq/Map.qml @@ -74,6 +74,7 @@ Column { var tz2 = responseJSON.timezoneId tzText.text = "Timezone: " + tz2 + config.setCurrentLocation(tz2) } } @@ -126,7 +127,7 @@ Column { anchorPoint.x: image.width/4 anchorPoint.y: image.height coordinate: QtPositioning.coordinate( - map.center.latitude, + map.center.latitude, map.center.longitude) //coordinate: QtPositioning.coordinate(40.730610, -73.935242) // New York @@ -156,7 +157,7 @@ Column { map.center.longitude = coordinate.longitude getTz(); - + console.log(coordinate.latitude, coordinate.longitude) } } @@ -199,7 +200,7 @@ Column { } Rectangle { - width: parent.width + width: parent.width height: 100 anchors.horizontalCenter: parent.horizontalCenter From 07c096673d1f4b46a8746a6782a0d3e5cd8d6ab4 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Fri, 24 Jul 2020 11:10:56 +0200 Subject: [PATCH 050/113] [localeq] Report summary before install --- src/modules/localeq/LocaleQmlViewStep.cpp | 7 +++++++ src/modules/localeq/LocaleQmlViewStep.h | 1 + 2 files changed, 8 insertions(+) diff --git a/src/modules/localeq/LocaleQmlViewStep.cpp b/src/modules/localeq/LocaleQmlViewStep.cpp index e0ef75f63..d1186af1f 100644 --- a/src/modules/localeq/LocaleQmlViewStep.cpp +++ b/src/modules/localeq/LocaleQmlViewStep.cpp @@ -41,6 +41,13 @@ LocaleQmlViewStep::prettyName() const return tr( "Location" ); } +QString +LocaleQmlViewStep::prettyStatus() const +{ + QStringList l { m_config->currentLocationStatus(), m_config->currentLanguageStatus(), m_config->currentLCStatus() }; + return l.join( QStringLiteral( "
" ) ); +} + bool LocaleQmlViewStep::isNextEnabled() const { diff --git a/src/modules/localeq/LocaleQmlViewStep.h b/src/modules/localeq/LocaleQmlViewStep.h index 20689e149..3d73c6f79 100644 --- a/src/modules/localeq/LocaleQmlViewStep.h +++ b/src/modules/localeq/LocaleQmlViewStep.h @@ -35,6 +35,7 @@ public: explicit LocaleQmlViewStep( QObject* parent = nullptr ); QString prettyName() const override; + QString prettyStatus() const override; bool isNextEnabled() const override; bool isBackEnabled() const override; From 23810aae3d8372d225e2ebe142267bc1fa11fde7 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Fri, 24 Jul 2020 11:29:47 +0200 Subject: [PATCH 051/113] CMake: switch to autorcc from manual futzing --- CMakeModules/CalamaresAddLibrary.cmake | 9 ++++--- CMakeModules/CalamaresAutomoc.cmake | 36 ++++++++++++++++++++------ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/CMakeModules/CalamaresAddLibrary.cmake b/CMakeModules/CalamaresAddLibrary.cmake index 88978e751..901791e30 100644 --- a/CMakeModules/CalamaresAddLibrary.cmake +++ b/CMakeModules/CalamaresAddLibrary.cmake @@ -62,10 +62,8 @@ function(calamares_add_library) include_directories(${CMAKE_CURRENT_BINARY_DIR}) # add resources from current dir - if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/${LIBRARY_RESOURCES}") - qt5_add_resources(LIBRARY_RC_SOURCES "${LIBRARY_RESOURCES}") - list(APPEND LIBRARY_SOURCES ${LIBRARY_RC_SOURCES}) - unset(LIBRARY_RC_SOURCES) + if(LIBRARY_RESOURCES) + list(APPEND LIBRARY_SOURCES ${LIBRARY_RESOURCES}) endif() # add target @@ -81,6 +79,9 @@ function(calamares_add_library) if(LIBRARY_UI) calamares_autouic(${target} ${LIBRARY_UI}) endif() + if(LIBRARY_RESOURCES) + calamares_autorcc(${target} ${LIBRARY_RESOURCES}) + endif() if(LIBRARY_EXPORT_MACRO) set_target_properties(${target} PROPERTIES COMPILE_DEFINITIONS ${LIBRARY_EXPORT_MACRO}) diff --git a/CMakeModules/CalamaresAutomoc.cmake b/CMakeModules/CalamaresAutomoc.cmake index 3de586ad2..c9a08a20d 100644 --- a/CMakeModules/CalamaresAutomoc.cmake +++ b/CMakeModules/CalamaresAutomoc.cmake @@ -18,17 +18,28 @@ # ### # -# Helper function for doing automoc on a target, and autoui on a .ui file. +# Helper function for doing automoc, autouic, autorcc on targets, +# and on the corresponding .ui or .rcc files. # -# Sets AUTOMOC TRUE for a target. +# calamares_automoc(target) +# Sets AUTOMOC TRUE for a target. # -# If the global variable CALAMARES_AUTOMOC_OPTIONS is set, uses that -# as well to set options passed to MOC. This can be used to add -# libcalamares/utils/moc-warnings.h file to the moc, which in turn -# reduces compiler warnings in generated MOC code. +# If the global variable CALAMARES_AUTOMOC_OPTIONS is set, uses that +# as well to set options passed to MOC. This can be used to add +# libcalamares/utils/moc-warnings.h file to the moc, which in turn +# reduces compiler warnings in generated MOC code. # -# If the global variable CALAMARES_AUTOUIC_OPTIONS is set, adds that -# to the options passed to uic. +# calamares_autouic(target [uifile ..]) +# Sets AUTOUIC TRUE for a target. +# +# If the global variable CALAMARES_AUTOUIC_OPTIONS is set, adds that +# to the options passed to uic for each of the named uifiles. +# +# calamares_autorcc(target [rcfile ..]) +# Sets AUTOUIC TRUE for a target. +# +# If the global variable CALAMARES_AUTORCC_OPTIONS is set, adds that +# to the options passed to rcc for each of the named rcfiles. function(calamares_automoc TARGET) set_target_properties( ${TARGET} PROPERTIES AUTOMOC TRUE ) @@ -45,3 +56,12 @@ function(calamares_autouic TARGET) endforeach() endif() endfunction() + +function(calamares_autorcc TARGET) + set_target_properties( ${TARGET} PROPERTIES AUTORCC TRUE ) + if ( CALAMARES_AUTORCC_OPTIONS ) + foreach(S ${ARGN}) + set_property(SOURCE ${S} PROPERTY AUTORCC_OPTIONS "${CALAMARES_AUTORCC_OPTIONS}") + endforeach() + endif() +endfunction() From a080e47f4b775d488589180ecb6f29b90c9f7967 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Fri, 24 Jul 2020 11:53:32 +0200 Subject: [PATCH 052/113] [locale] Add prettyStatus to Config - this is present in the previous config, and helps make the modules consistent by returning prettyStatus in both ViewSteps. --- src/modules/locale/Config.cpp | 12 ++++++++++++ src/modules/locale/Config.h | 7 +++++++ src/modules/locale/LocaleViewStep.cpp | 3 +-- src/modules/localeq/LocaleQmlViewStep.cpp | 3 +-- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index f28a2d0b5..7a49525f2 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -199,6 +199,11 @@ Config::Config( QObject* parent ) { "set-timezone", location->region() + '/' + location->zone() } ); } } ); + + auto prettyStatusNotify = [&]() { emit prettyStatusChanged( prettyStatus() ); }; + connect( this, &Config::currentLanguageStatusChanged, prettyStatusNotify ); + connect( this, &Config::currentLCStatusChanged, prettyStatusNotify ); + connect( this, &Config::currentLocationStatusChanged, prettyStatusNotify ); } Config::~Config() {} @@ -351,6 +356,13 @@ Config::currentLCStatus() const .arg( localeLabel( m_selectedLocaleConfiguration.lc_numeric ) ); } +QString +Config::prettyStatus() const +{ + QStringList l { currentLocationStatus(), currentLanguageStatus(), currentLCStatus() }; + return l.join( QStringLiteral( "
" ) ); +} + static inline void getLocaleGenLines( const QVariantMap& configurationMap, QStringList& localeGenLines ) { diff --git a/src/modules/locale/Config.h b/src/modules/locale/Config.h index d74555ee5..e9a8e6373 100644 --- a/src/modules/locale/Config.h +++ b/src/modules/locale/Config.h @@ -51,6 +51,9 @@ class Config : public QObject Q_PROPERTY( QString currentLanguageCode READ currentLanguageCode WRITE setLanguageExplicitly NOTIFY currentLanguageCodeChanged ) Q_PROPERTY( QString currentLCCode READ currentLCCode WRITE setLCLocaleExplicitly NOTIFY currentLCCodeChanged ) + // This is a long human-readable string with all three statuses + Q_PROPERTY( QString prettyStatus READ prettyStatus NOTIFY prettyStatusChanged FINAL ) + public: Config( QObject* parent = nullptr ); ~Config(); @@ -79,6 +82,9 @@ public: /// The human-readable description of what locale (LC_*) is used QString currentLCStatus() const; + /// The human-readable summary of what the module will do + QString prettyStatus() const; + const QStringList& supportedLocales() const { return m_localeGenLines; } CalamaresUtils::Locale::CStringListModel* regionModel() const { return m_regionModel.get(); } CalamaresUtils::Locale::CStringListModel* zonesModel() const { return m_zonesModel.get(); } @@ -122,6 +128,7 @@ signals: void currentLocationStatusChanged( const QString& ) const; void currentLanguageStatusChanged( const QString& ) const; void currentLCStatusChanged( const QString& ) const; + void prettyStatusChanged( const QString& ) const; void currentLanguageCodeChanged( const QString& ) const; void currentLCCodeChanged( const QString& ) const; diff --git a/src/modules/locale/LocaleViewStep.cpp b/src/modules/locale/LocaleViewStep.cpp index 8ae894aa8..a85c87e4f 100644 --- a/src/modules/locale/LocaleViewStep.cpp +++ b/src/modules/locale/LocaleViewStep.cpp @@ -89,8 +89,7 @@ LocaleViewStep::prettyName() const QString LocaleViewStep::prettyStatus() const { - QStringList l { m_config->currentLocationStatus(), m_config->currentLanguageStatus(), m_config->currentLCStatus() }; - return l.join( QStringLiteral( "
" ) ); + return m_config->prettyStatus(); } diff --git a/src/modules/localeq/LocaleQmlViewStep.cpp b/src/modules/localeq/LocaleQmlViewStep.cpp index d1186af1f..ead2e2673 100644 --- a/src/modules/localeq/LocaleQmlViewStep.cpp +++ b/src/modules/localeq/LocaleQmlViewStep.cpp @@ -44,8 +44,7 @@ LocaleQmlViewStep::prettyName() const QString LocaleQmlViewStep::prettyStatus() const { - QStringList l { m_config->currentLocationStatus(), m_config->currentLanguageStatus(), m_config->currentLCStatus() }; - return l.join( QStringLiteral( "
" ) ); + return m_config->prettyStatus(); } bool From 09020d68b093c1a28f251db95d645a70d6e9cded Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Fri, 24 Jul 2020 12:15:27 +0200 Subject: [PATCH 053/113] [libcalamaresui] Make dox of ModuleManager signals more explicit --- src/libcalamaresui/modulesystem/ModuleManager.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libcalamaresui/modulesystem/ModuleManager.h b/src/libcalamaresui/modulesystem/ModuleManager.h index 2bac78af6..bea8acf41 100644 --- a/src/libcalamaresui/modulesystem/ModuleManager.h +++ b/src/libcalamaresui/modulesystem/ModuleManager.h @@ -130,10 +130,10 @@ signals: void modulesFailed( QStringList ); /** @brief Emitted after all requirements have been checked * - * The bool value indicates if all of the **mandatory** requirements + * The bool @p canContinue indicates if all of the **mandatory** requirements * are satisfied (e.g. whether installation can continue). */ - void requirementsComplete( bool ); + void requirementsComplete( bool canContinue ); private slots: void doInit(); From 682146aa9b9d5267b379c6144c0454caa462594e Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Thu, 9 Jul 2020 16:08:51 +0200 Subject: [PATCH 054/113] [libcalamares] Expand dox on TimeZone pairs --- src/libcalamares/locale/TimeZone.h | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/libcalamares/locale/TimeZone.h b/src/libcalamares/locale/TimeZone.h index 60900ef21..05820817a 100644 --- a/src/libcalamares/locale/TimeZone.h +++ b/src/libcalamares/locale/TimeZone.h @@ -1,6 +1,7 @@ /* === This file is part of Calamares - === - * + * * SPDX-FileCopyrightText: 2019 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later * * Calamares is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,8 +16,6 @@ * You should have received a copy of the GNU General Public License * along with Calamares. If not, see . * - * SPDX-License-Identifier: GPL-3.0-or-later - * License-Filename: LICENSE * */ @@ -88,7 +87,13 @@ public: } }; -/// @brief A pair of strings for timezone regions (e.g. "America") +/** @brief Timezone regions (e.g. "America") + * + * A region has a key and a human-readable name, but also + * a collection of associated timezone zones (TZZone, below). + * This class is not usually constructed, but uses fromFile() + * to load a complete tree structure of timezones. + */ class TZRegion : public CStringPair { Q_OBJECT @@ -120,7 +125,12 @@ private: CStringPairList m_zones; }; -/// @brief A pair of strings for specific timezone names (e.g. "New_York") +/** @brief Specific timezone zones (e.g. "New_York", "New York") + * + * A timezone zone lives in a region, and has some associated + * data like the country (used to map likely languages) and latitude + * and longitude information. + */ class TZZone : public CStringPair { Q_OBJECT From a835bb9a10014dc18935c8919802020f7e3f5789 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Fri, 24 Jul 2020 12:26:02 +0200 Subject: [PATCH 055/113] Changes: document new locale features --- CHANGES | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index d7510976a..ad374e40a 100644 --- a/CHANGES +++ b/CHANGES @@ -9,10 +9,18 @@ This release contains contributions from (alphabetically by first name): - No external contributors yet ## Core ## - - No core changes yet + - A new object *Network* is available to QML modules in `io.calamares.core`. + It exposes network status through the *hasInternet* property. ## Modules ## - - No module changes yet + - The *locale* module has been completely redone on the inside. + Users should see no changes. #1391 + - The *localeq* module uses the redone internals of the locale module. + It can now be used to set timezone, language and locale information + and is a suitable alternative module. Thanks to Anke Boersma who did + the work of figuring out maps. Note that the map uses several GeoIP + and GeoData providers and you may need to configure the URLs + with suitable usernames for those services. #1426 # 3.2.27 (2020-07-11) # From 11482559ad64a11946ffc591e270ee3cd768c05c Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Fri, 24 Jul 2020 13:39:43 +0200 Subject: [PATCH 056/113] [netinstall] There is no netinstall.qrc --- src/modules/netinstall/CMakeLists.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/netinstall/CMakeLists.txt b/src/modules/netinstall/CMakeLists.txt index 762e92985..0f2cf06f8 100644 --- a/src/modules/netinstall/CMakeLists.txt +++ b/src/modules/netinstall/CMakeLists.txt @@ -9,8 +9,6 @@ calamares_add_plugin( netinstall PackageModel.cpp UI page_netinst.ui - RESOURCES - netinstall.qrc LINK_PRIVATE_LIBRARIES calamaresui Qt5::Network From 4d3422b93181188bcff06c2938c70748ed9a3a5e Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Fri, 24 Jul 2020 14:24:03 +0200 Subject: [PATCH 057/113] [libcalamares] dox for Permissions - Expand the documentation, emphasize octal-vs-decimal - east-const consistently in this file (most of Calamares is west-const) - shuffle the is-valid bool to the end of the data members, so sorting by size. --- src/libcalamares/utils/Permissions.cpp | 6 +++--- src/libcalamares/utils/Permissions.h | 29 ++++++++++++++++++++------ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/libcalamares/utils/Permissions.cpp b/src/libcalamares/utils/Permissions.cpp index d9d3226e6..3166d840a 100644 --- a/src/libcalamares/utils/Permissions.cpp +++ b/src/libcalamares/utils/Permissions.cpp @@ -14,20 +14,20 @@ Permissions::Permissions() : m_username() , m_group() - , m_valid( false ) , m_value( 0 ) + , m_valid( false ) { } -Permissions::Permissions( QString p ) +Permissions::Permissions( QString const& p ) : Permissions() { parsePermissions( p ); } void -Permissions::parsePermissions( const QString& p ) +Permissions::parsePermissions( QString const& p ) { QStringList segments = p.split( ":" ); diff --git a/src/libcalamares/utils/Permissions.h b/src/libcalamares/utils/Permissions.h index baa5da554..b6e2d3a44 100644 --- a/src/libcalamares/utils/Permissions.h +++ b/src/libcalamares/utils/Permissions.h @@ -25,27 +25,44 @@ public: /** @brief Constructor * * Splits the string @p at the colon (":") into separate elements for - * , , and (permissions), where is returned as - * an **octal** integer. + * , , and (permissions), where is interpreted + * as an **octal** integer. That is, "root:wheel:755" will give + * you an integer value of four-hundred-ninety-three (493), + * corresponding to the UNIX file permissions rwxr-xr-x, + * as one would expect from chmod and other command-line utilities. */ - Permissions( QString p ); + Permissions( QString const& p ); - /** @brief Default constructor of an invalid Permissions. */ + /// @brief Default constructor of an invalid Permissions. Permissions(); + /// @brief Was the Permissions object constructed from valid data? bool isValid() const { return m_valid; } + /// @brief The user (first component, e.g. "root" in "root:wheel:755") QString username() const { return m_username; } + /// @brief The group (second component, e.g. "wheel" in "root:wheel:755") QString group() const { return m_group; } + /** @brief The value (file permission) as an integer. + * + * Bear in mind that input is in octal, but integers are just integers; + * naively printing them will get decimal results (e.g. 493 from the + * input of "root:wheel:755"). + */ int value() const { return m_value; } - QString octal() const { return QString::number( m_value, 8 ); } + /** @brief The value (file permission) as octal string + * + * This is suitable for passing to chmod-the-program, or for + * recreating the original Permissions string. + */ + QString octal() const { return QString::number( value(), 8 ); } private: void parsePermissions( QString const& p ); QString m_username; QString m_group; - bool m_valid; int m_value; + bool m_valid; }; #endif // LIBCALAMARES_PERMISSIONS_H From bc484ae5da7ee5f52ca661c6e66bfc463d82614c Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 22 Jun 2020 16:40:12 +0200 Subject: [PATCH 058/113] [users] Refactor /etc/group file handing --- src/modules/users/CreateUserJob.cpp | 58 +++++++++++++++++------------ 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/src/modules/users/CreateUserJob.cpp b/src/modules/users/CreateUserJob.cpp index 776b45ce9..349d6c295 100644 --- a/src/modules/users/CreateUserJob.cpp +++ b/src/modules/users/CreateUserJob.cpp @@ -54,6 +54,36 @@ CreateUserJob::prettyStatusMessage() const return tr( "Creating user %1." ).arg( m_userName ); } +static QStringList +groupsInTargetSystem( const QDir& targetRoot ) +{ + QFileInfo groupsFi( targetRoot.absoluteFilePath( "etc/group" ) ); + QFile groupsFile( groupsFi.absoluteFilePath() ); + if ( !groupsFile.open( QIODevice::ReadOnly | QIODevice::Text ) ) + { + return QStringList(); + } + QString groupsData = QString::fromLocal8Bit( groupsFile.readAll() ); + QStringList groupsLines = groupsData.split( '\n' ); + for ( QStringList::iterator it = groupsLines.begin(); it != groupsLines.end(); ++it ) + { + int indexOfFirstToDrop = it->indexOf( ':' ); + it->truncate( indexOfFirstToDrop ); + } + return groupsLines; +} + +static void +ensureGroupsExistInTarget( const QStringList& wantedGroups, const QStringList& availableGroups ) +{ + for ( const QString& group : wantedGroups ) + { + if ( !availableGroups.contains( group ) ) + { + CalamaresUtils::System::instance()->targetEnvCall( { "groupadd", group } ); + } + } +} Calamares::JobResult CreateUserJob::exec() @@ -89,36 +119,16 @@ CreateUserJob::exec() cDebug() << "[CREATEUSER]: preparing groups"; - QFileInfo groupsFi( destDir.absoluteFilePath( "etc/group" ) ); - QFile groupsFile( groupsFi.absoluteFilePath() ); - if ( !groupsFile.open( QIODevice::ReadOnly | QIODevice::Text ) ) - { - return Calamares::JobResult::error( tr( "Cannot open groups file for reading." ) ); - } - QString groupsData = QString::fromLocal8Bit( groupsFile.readAll() ); - QStringList groupsLines = groupsData.split( '\n' ); - for ( QStringList::iterator it = groupsLines.begin(); it != groupsLines.end(); ++it ) - { - int indexOfFirstToDrop = it->indexOf( ':' ); - it->truncate( indexOfFirstToDrop ); - } - - for ( const QString& group : m_defaultGroups ) - { - if ( !groupsLines.contains( group ) ) - { - CalamaresUtils::System::instance()->targetEnvCall( { "groupadd", group } ); - } - } + QStringList availableGroups = groupsInTargetSystem( destDir ); + ensureGroupsExistInTarget( m_defaultGroups, availableGroups ); QString defaultGroups = m_defaultGroups.join( ',' ); if ( m_autologin ) { - QString autologinGroup; if ( gs->contains( "autologinGroup" ) && !gs->value( "autologinGroup" ).toString().isEmpty() ) { - autologinGroup = gs->value( "autologinGroup" ).toString(); - CalamaresUtils::System::instance()->targetEnvCall( { "groupadd", autologinGroup } ); + QString autologinGroup = gs->value( "autologinGroup" ).toString(); + ensureGroupsExistInTarget( QStringList { autologinGroup }, availableGroups ); defaultGroups.append( QString( ",%1" ).arg( autologinGroup ) ); } } From 409ab6ee868b22f8e42083ce668876d3f5f313e8 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 22 Jun 2020 17:10:03 +0200 Subject: [PATCH 059/113] [users] Refactor writing sudoers file - use existing convenience methods --- src/modules/users/CreateUserJob.cpp | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/modules/users/CreateUserJob.cpp b/src/modules/users/CreateUserJob.cpp index 349d6c295..f0b1dca88 100644 --- a/src/modules/users/CreateUserJob.cpp +++ b/src/modules/users/CreateUserJob.cpp @@ -95,26 +95,23 @@ CreateUserJob::exec() { cDebug() << "[CREATEUSER]: preparing sudoers"; - QFileInfo sudoersFi( destDir.absoluteFilePath( "etc/sudoers.d/10-installer" ) ); + QString sudoersLine = QString( "%%1 ALL=(ALL) ALL\n" ).arg( gs->value( "sudoersGroup" ).toString() ); + auto fileResult + = CalamaresUtils::System::instance()->createTargetFile( QStringLiteral( "/etc/sudoers.d/10-installer" ), + sudoersLine.toUtf8().constData(), + CalamaresUtils::System::WriteMode::Overwrite ); - if ( !sudoersFi.absoluteDir().exists() ) + if ( fileResult ) { - return Calamares::JobResult::error( tr( "Sudoers dir is not writable." ) ); + if ( QProcess::execute( "chmod", { "440", fileResult.path() } ) ) + { + return Calamares::JobResult::error( tr( "Cannot chmod sudoers file." ) ); + } } - - QFile sudoersFile( sudoersFi.absoluteFilePath() ); - if ( !sudoersFile.open( QIODevice::WriteOnly | QIODevice::Text ) ) + else { return Calamares::JobResult::error( tr( "Cannot create sudoers file for writing." ) ); } - - QString sudoersGroup = gs->value( "sudoersGroup" ).toString(); - - QTextStream sudoersOut( &sudoersFile ); - sudoersOut << QString( "%%1 ALL=(ALL) ALL\n" ).arg( sudoersGroup ); - - if ( QProcess::execute( "chmod", { "440", sudoersFi.absoluteFilePath() } ) ) - return Calamares::JobResult::error( tr( "Cannot chmod sudoers file." ) ); } cDebug() << "[CREATEUSER]: preparing groups"; From d114c383fa911f9c447b6f964243ba889a74316c Mon Sep 17 00:00:00 2001 From: demmm Date: Fri, 24 Jul 2020 17:34:14 +0200 Subject: [PATCH 060/113] [localeq] remove obsolete vars & comments set index in i18n.qml to -1, old settings were just for reading from the bogus model current model uses strings, so index fails to read from it. This fixes cala crashing on loading i18n.qml --- src/modules/localeq/i18n.qml | 5 ++--- src/modules/localeq/localeq.qml | 3 --- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/modules/localeq/i18n.qml b/src/modules/localeq/i18n.qml index eea9864a3..6907b1ba8 100644 --- a/src/modules/localeq/i18n.qml +++ b/src/modules/localeq/i18n.qml @@ -74,7 +74,7 @@ Item { model: config.supportedLocales - currentIndex: 1 + currentIndex: -1 highlight: Rectangle { color: Kirigami.Theme.highlightColor } @@ -133,10 +133,9 @@ Item { width: 180; height: 200 focus: true - // bogus entries, need to come from Locale config model: config.supportedLocales - currentIndex: 2 + currentIndex: -1 highlight: Rectangle { color: Kirigami.Theme.highlightColor } diff --git a/src/modules/localeq/localeq.qml b/src/modules/localeq/localeq.qml index c48140d12..49719db65 100644 --- a/src/modules/localeq/localeq.qml +++ b/src/modules/localeq/localeq.qml @@ -29,9 +29,6 @@ Page { width: 800 height: 550 - property var confLang: "American English" - property var confLocale: "Nederland" - Loader { id: image anchors.horizontalCenter: parent.horizontalCenter From 2b3cc1778236a21339d8fd5de97cb80fa7abac2a Mon Sep 17 00:00:00 2001 From: apt-ghetto Date: Fri, 24 Jul 2020 17:56:58 +0200 Subject: [PATCH 061/113] Revert Manual Partition instructions With PR calamares/calamares#1357 the label of the "Manual partitioning" option was changed, which introduced several downsides: * The label is shown for UEFI and for BIOS installations. * The mountpoint of the ESP is and should be distro specific. * The label always mentioned GPT, which is irrelevant. * The label should explain, what the option does, and not, what problems can occur under certain circumstances. --- src/modules/partition/gui/ChoicePage.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/modules/partition/gui/ChoicePage.cpp b/src/modules/partition/gui/ChoicePage.cpp index 69a740d20..9e48e69ac 100644 --- a/src/modules/partition/gui/ChoicePage.cpp +++ b/src/modules/partition/gui/ChoicePage.cpp @@ -332,9 +332,7 @@ ChoicePage::setupChoices() CALAMARES_RETRANSLATE( m_somethingElseButton->setText( tr( "Manual partitioning
" - "You can create or resize partitions yourself." - " Having a GPT partition table and fat32 512Mb /boot partition " - "is a must for UEFI installs, either use an existing without formatting or create one." ) ); + "You can create or resize partitions yourself." ) ); updateSwapChoicesTr( m_eraseSwapChoiceComboBox ); ) } From 01b22d27a8cc7d8447d554d8162613f2c4b4a992 Mon Sep 17 00:00:00 2001 From: apt-ghetto Date: Sat, 25 Jul 2020 15:59:59 +0200 Subject: [PATCH 062/113] Do not allow 'root' as username On the "Users" tab, the user can choose a username. It was possible to use 'root' as username, which led to an installation error, because 'root' exists already. Added a new check to the username validation. Fixes #1462. --- src/modules/users/UsersPage.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/modules/users/UsersPage.cpp b/src/modules/users/UsersPage.cpp index 9c7ce6f7b..5c649d622 100644 --- a/src/modules/users/UsersPage.cpp +++ b/src/modules/users/UsersPage.cpp @@ -409,6 +409,13 @@ UsersPage::validateUsernameText( const QString& textRef ) tr( "Only lowercase letters, numbers, underscore and hyphen are allowed." ) ); m_readyUsername = false; } + else if ( 0 == QString::compare("root", text, Qt::CaseSensitive ) ) + { + labelError( ui->labelUsername, + ui->labelUsernameError, + tr( "'root' is not allowed as user name." ) ); + m_readyUsername = false; + } else { labelOk( ui->labelUsername, ui->labelUsernameError ); From 3a3507f2b29e9e0b080e5d118a70e76f06959960 Mon Sep 17 00:00:00 2001 From: demmm Date: Sat, 25 Jul 2020 17:18:28 +0200 Subject: [PATCH 063/113] [keyboardq] remove background image use make the module more in line with the look of the rest of Calamares --- src/modules/keyboardq/ListViewTemplate.qml | 2 +- src/modules/keyboardq/ResponsiveBase.qml | 47 +++------------------ src/modules/keyboardq/keyboard.jpg | Bin 103372 -> 0 bytes src/modules/keyboardq/keyboardq.qml | 5 ++- src/modules/keyboardq/keyboardq.qrc | 1 - 5 files changed, 11 insertions(+), 44 deletions(-) delete mode 100644 src/modules/keyboardq/keyboard.jpg diff --git a/src/modules/keyboardq/ListViewTemplate.qml b/src/modules/keyboardq/ListViewTemplate.qml index eb160afab..4564b887b 100644 --- a/src/modules/keyboardq/ListViewTemplate.qml +++ b/src/modules/keyboardq/ListViewTemplate.qml @@ -15,7 +15,7 @@ ListView { z: parent.z - 1 anchors.fill: parent - color: Kirigami.Theme.backgroundColor + color: "#BDC3C7" radius: 5 opacity: 0.7 } diff --git a/src/modules/keyboardq/ResponsiveBase.qml b/src/modules/keyboardq/ResponsiveBase.qml index c9f5c7091..38fa15d1b 100644 --- a/src/modules/keyboardq/ResponsiveBase.qml +++ b/src/modules/keyboardq/ResponsiveBase.qml @@ -13,8 +13,8 @@ Page { width: 800 //parent.width height: 550 //parent.height - Kirigami.Theme.backgroundColor: "#fafafa" - Kirigami.Theme.textColor: "#333" + Kirigami.Theme.backgroundColor: "#FAFAFA" + Kirigami.Theme.textColor: "#1F1F1F" property string subtitle property string message @@ -22,39 +22,6 @@ Page { default property alias content : _content.data property alias stackView: _stackView - background: Item { - - id: _background - - Image { - - id: _wallpaper - height: parent.height - width: parent.width - - sourceSize.width: 800 - sourceSize.height: 550 - - fillMode: Image.PreserveAspectCrop - antialiasing: false - smooth: false - asynchronous: true - cache: true - - source: "keyboard.jpg" - } - - FastBlur { - - id: fastBlur - anchors.fill: parent - source: _wallpaper - radius: 32 - transparentBorder: false - cached: true - } - } - ColumnLayout { id: _content @@ -63,7 +30,7 @@ Page { spacing: Kirigami.Units.smallSpacing * 5 anchors.margins: Kirigami.Units.smallSpacing * 5 anchors.bottomMargin: 20 - + Label { Layout.fillWidth: true @@ -72,7 +39,7 @@ Page { wrapMode: Text.NoWrap elide: Text.ElideMiddle text: control.title - color: "white" + color: Kirigami.Theme.textColor font.bold: true font.weight: Font.Bold font.pointSize: 24 @@ -86,7 +53,7 @@ Page { wrapMode: Text.Wrap elide: Text.ElideMiddle text: control.subtitle - color: "white" + color: Kirigami.Theme.textColor font.weight: Font.Light font.pointSize: 12 } @@ -99,7 +66,7 @@ Page { wrapMode: Text.Wrap elide: Text.ElideMiddle text: control.message - color: "white" + color: Kirigami.Theme.textColor font.weight: Font.Light font.pointSize: 10 } @@ -110,7 +77,7 @@ Page { Layout.fillHeight: true Layout.preferredWidth: parent.width clip: true - } + } } } diff --git a/src/modules/keyboardq/keyboard.jpg b/src/modules/keyboardq/keyboard.jpg deleted file mode 100644 index 9c0600fac5da2748cc80a4ba18e5b1df8e285465..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 103372 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.qml ListViewTemplate.qml ResponsiveBase.qml - keyboard.jpg From e0bb7d9f6f6556e94e3dd3bb689c880a54b1183a Mon Sep 17 00:00:00 2001 From: Calamares CI Date: Sun, 26 Jul 2020 11:02:36 +0200 Subject: [PATCH 064/113] i18n: [calamares] Automatic merge of Transifex translations --- lang/calamares_de.ts | 2 +- lang/calamares_ie.ts | 3941 ++++++++++++++++++++++++++++++++++++++ lang/calamares_it_IT.ts | 26 +- lang/calamares_nl.ts | 18 +- lang/calamares_tg.ts | 3947 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 7911 insertions(+), 23 deletions(-) create mode 100644 lang/calamares_ie.ts create mode 100644 lang/calamares_tg.ts diff --git a/lang/calamares_de.ts b/lang/calamares_de.ts index 9098ab48c..c0d0cadaf 100644 --- a/lang/calamares_de.ts +++ b/lang/calamares_de.ts @@ -792,7 +792,7 @@ Dies wird das Installationsprogramm beenden und alle Änderungen gehen verloren. <h1>Welcome to the %1 installer</h1> - + <h1>Willkommen zum %1 Installationsprogramm</h1>
diff --git a/lang/calamares_ie.ts b/lang/calamares_ie.ts new file mode 100644 index 000000000..e65456755 --- /dev/null +++ b/lang/calamares_ie.ts @@ -0,0 +1,3941 @@ + + + + + BootInfoWidget + + + The <strong>boot environment</strong> of this system.<br><br>Older x86 systems only support <strong>BIOS</strong>.<br>Modern systems usually use <strong>EFI</strong>, but may also show up as BIOS if started in compatibility mode. + + + + + This system was started with an <strong>EFI</strong> boot environment.<br><br>To configure startup from an EFI environment, this installer must deploy a boot loader application, like <strong>GRUB</strong> or <strong>systemd-boot</strong> on an <strong>EFI System Partition</strong>. This is automatic, unless you choose manual partitioning, in which case you must choose it or create it on your own. + + + + + This system was started with a <strong>BIOS</strong> boot environment.<br><br>To configure startup from a BIOS environment, this installer must install a boot loader, like <strong>GRUB</strong>, either at the beginning of a partition or on the <strong>Master Boot Record</strong> near the beginning of the partition table (preferred). This is automatic, unless you choose manual partitioning, in which case you must set it up on your own. + + + + + BootLoaderModel + + + Master Boot Record of %1 + + + + + Boot Partition + + + + + System Partition + + + + + Do not install a boot loader + + + + + %1 (%2) + + + + + Calamares::BlankViewStep + + + Blank Page + + + + + Calamares::DebugWindow + + + Form + + + + + GlobalStorage + + + + + JobQueue + + + + + Modules + + + + + Type: + + + + + + none + + + + + Interface: + + + + + Tools + + + + + Reload Stylesheet + + + + + Widget Tree + + + + + Debug information + + + + + Calamares::ExecutionViewStep + + + Set up + + + + + Install + + + + + Calamares::FailJob + + + Job failed (%1) + + + + + Programmed job failure was explicitly requested. + + + + + Calamares::JobThread + + + Done + + + + + Calamares::NamedJob + + + Example job (%1) + + + + + Calamares::ProcessJob + + + Run command '%1' in target system. + + + + + Run command '%1'. + + + + + Running command %1 %2 + + + + + Calamares::PythonJob + + + Running %1 operation. + + + + + Bad working directory path + + + + + Working directory %1 for python job %2 is not readable. + + + + + Bad main script file + + + + + Main script file %1 for python job %2 is not readable. + + + + + Boost.Python error in job "%1". + + + + + Calamares::QmlViewStep + + + Loading ... + + + + + QML Step <i>%1</i>. + + + + + Loading failed. + + + + + Calamares::RequirementsChecker + + + Requirements checking for module <i>%1</i> is complete. + + + + + Waiting for %n module(s). + + + + + + + + (%n second(s)) + + + + + + + + System-requirements checking is complete. + + + + + Calamares::ViewManager + + + Setup Failed + + + + + Installation Failed + + + + + Would you like to paste the install log to the web? + + + + + Error + + + + + + &Yes + + + + + + &No + + + + + &Close + + + + + Install Log Paste URL + + + + + The upload was unsuccessful. No web-paste was done. + + + + + Calamares Initialization Failed + + + + + %1 can not be installed. Calamares was unable to load all of the configured modules. This is a problem with the way Calamares is being used by the distribution. + + + + + <br/>The following modules could not be loaded: + + + + + Continue with setup? + + + + + Continue with installation? + + + + + The %1 setup program is about to make changes to your disk in order to set up %2.<br/><strong>You will not be able to undo these changes.</strong> + + + + + The %1 installer is about to make changes to your disk in order to install %2.<br/><strong>You will not be able to undo these changes.</strong> + + + + + &Set up now + + + + + &Install now + + + + + Go &back + + + + + &Set up + + + + + &Install + + + + + Setup is complete. Close the setup program. + + + + + The installation is complete. Close the installer. + + + + + Cancel setup without changing the system. + + + + + Cancel installation without changing the system. + + + + + &Next + + + + + &Back + + + + + &Done + + + + + &Cancel + + + + + Cancel setup? + + + + + Cancel installation? + + + + + Do you really want to cancel the current setup process? +The setup program will quit and all changes will be lost. + + + + + Do you really want to cancel the current install process? +The installer will quit and all changes will be lost. + + + + + CalamaresPython::Helper + + + Unknown exception type + + + + + unparseable Python error + + + + + unparseable Python traceback + + + + + Unfetchable Python error. + + + + + CalamaresUtils + + + Install log posted to: +%1 + + + + + CalamaresWindow + + + Show debug information + + + + + &Back + + + + + &Next + + + + + &Cancel + + + + + %1 Setup Program + + + + + %1 Installer + + + + + CheckerContainer + + + Gathering system information... + + + + + ChoicePage + + + Form + + + + + Select storage de&vice: + + + + + + + + Current: + + + + + After: + + + + + <strong>Manual partitioning</strong><br/>You can create or resize partitions yourself. Having a GPT partition table and <strong>fat32 512Mb /boot partition is a must for UEFI installs</strong>, either use an existing without formatting or create one. + + + + + Reuse %1 as home partition for %2. + + + + + <strong>Select a partition to shrink, then drag the bottom bar to resize</strong> + + + + + %1 will be shrunk to %2MiB and a new %3MiB partition will be created for %4. + + + + + Boot loader location: + + + + + <strong>Select a partition to install on</strong> + + + + + An EFI system partition cannot be found anywhere on this system. Please go back and use manual partitioning to set up %1. + + + + + The EFI system partition at %1 will be used for starting %2. + + + + + EFI system partition: + + + + + This storage device does not seem to have an operating system on it. What would you like to do?<br/>You will be able to review and confirm your choices before any change is made to the storage device. + + + + + + + + <strong>Erase disk</strong><br/>This will <font color="red">delete</font> all data currently present on the selected storage device. + + + + + + + + <strong>Install alongside</strong><br/>The installer will shrink a partition to make room for %1. + + + + + + + + <strong>Replace a partition</strong><br/>Replaces a partition with %1. + + + + + This storage device has %1 on it. What would you like to do?<br/>You will be able to review and confirm your choices before any change is made to the storage device. + + + + + This storage device already has an operating system on it. What would you like to do?<br/>You will be able to review and confirm your choices before any change is made to the storage device. + + + + + This storage device has multiple operating systems on it. What would you like to do?<br/>You will be able to review and confirm your choices before any change is made to the storage device. + + + + + No Swap + + + + + Reuse Swap + + + + + Swap (no Hibernate) + + + + + Swap (with Hibernate) + + + + + Swap to file + + + + + ClearMountsJob + + + Clear mounts for partitioning operations on %1 + + + + + Clearing mounts for partitioning operations on %1. + + + + + Cleared all mounts for %1 + + + + + ClearTempMountsJob + + + Clear all temporary mounts. + + + + + Clearing all temporary mounts. + + + + + Cannot get list of temporary mounts. + + + + + Cleared all temporary mounts. + + + + + CommandList + + + + Could not run command. + + + + + The command runs in the host environment and needs to know the root path, but no rootMountPoint is defined. + + + + + The command needs to know the user's name, but no username is defined. + + + + + Config + + + Set keyboard model to %1.<br/> + + + + + Set keyboard layout to %1/%2. + + + + + The system language will be set to %1. + + + + + The numbers and dates locale will be set to %1. + + + + + Set timezone to %1/%2.<br/> + + + + + Network Installation. (Disabled: Incorrect configuration) + + + + + Network Installation. (Disabled: Received invalid groups data) + + + + + Network Installation. (Disabled: internal error) + + + + + Network Installation. (Disabled: Unable to fetch package lists, check your network connection) + + + + + This computer does not satisfy the minimum requirements for setting up %1.<br/>Setup cannot continue. <a href="#details">Details...</a> + + + + + This computer does not satisfy the minimum requirements for installing %1.<br/>Installation cannot continue. <a href="#details">Details...</a> + + + + + This computer does not satisfy some of the recommended requirements for setting up %1.<br/>Setup can continue, but some features might be disabled. + + + + + This computer does not satisfy some of the recommended requirements for installing %1.<br/>Installation can continue, but some features might be disabled. + + + + + This program will ask you some questions and set up %2 on your computer. + + + + + <h1>Welcome to the Calamares setup program for %1</h1> + + + + + <h1>Welcome to %1 setup</h1> + + + + + <h1>Welcome to the Calamares installer for %1</h1> + + + + + <h1>Welcome to the %1 installer</h1> + + + + + ContextualProcessJob + + + Contextual Processes Job + + + + + CreatePartitionDialog + + + Create a Partition + + + + + Si&ze: + + + + + MiB + + + + + Partition &Type: + + + + + &Primary + + + + + E&xtended + + + + + Fi&le System: + + + + + LVM LV name + + + + + &Mount Point: + + + + + Flags: + + + + + En&crypt + + + + + Logical + + + + + Primary + + + + + GPT + + + + + Mountpoint already in use. Please select another one. + + + + + CreatePartitionJob + + + Create new %2MiB partition on %4 (%3) with file system %1. + + + + + Create new <strong>%2MiB</strong> partition on <strong>%4</strong> (%3) with file system <strong>%1</strong>. + + + + + Creating new %1 partition on %2. + + + + + The installer failed to create partition on disk '%1'. + + + + + CreatePartitionTableDialog + + + Create Partition Table + + + + + Creating a new partition table will delete all existing data on the disk. + + + + + What kind of partition table do you want to create? + + + + + Master Boot Record (MBR) + + + + + GUID Partition Table (GPT) + + + + + CreatePartitionTableJob + + + Create new %1 partition table on %2. + + + + + Create new <strong>%1</strong> partition table on <strong>%2</strong> (%3). + + + + + Creating new %1 partition table on %2. + + + + + The installer failed to create a partition table on %1. + + + + + CreateUserJob + + + Create user %1 + + + + + Create user <strong>%1</strong>. + + + + + Creating user %1. + + + + + Sudoers dir is not writable. + + + + + Cannot create sudoers file for writing. + + + + + Cannot chmod sudoers file. + + + + + Cannot open groups file for reading. + + + + + CreateVolumeGroupDialog + + + Create Volume Group + + + + + CreateVolumeGroupJob + + + Create new volume group named %1. + + + + + Create new volume group named <strong>%1</strong>. + + + + + Creating new volume group named %1. + + + + + The installer failed to create a volume group named '%1'. + + + + + DeactivateVolumeGroupJob + + + + Deactivate volume group named %1. + + + + + Deactivate volume group named <strong>%1</strong>. + + + + + The installer failed to deactivate a volume group named %1. + + + + + DeletePartitionJob + + + Delete partition %1. + + + + + Delete partition <strong>%1</strong>. + + + + + Deleting partition %1. + + + + + The installer failed to delete partition %1. + + + + + DeviceInfoWidget + + + This device has a <strong>%1</strong> partition table. + + + + + This is a <strong>loop</strong> device.<br><br>It is a pseudo-device with no partition table that makes a file accessible as a block device. This kind of setup usually only contains a single filesystem. + + + + + This installer <strong>cannot detect a partition table</strong> on the selected storage device.<br><br>The device either has no partition table, or the partition table is corrupted or of an unknown type.<br>This installer can create a new partition table for you, either automatically, or through the manual partitioning page. + + + + + <br><br>This is the recommended partition table type for modern systems which start from an <strong>EFI</strong> boot environment. + + + + + <br><br>This partition table type is only advisable on older systems which start from a <strong>BIOS</strong> boot environment. GPT is recommended in most other cases.<br><br><strong>Warning:</strong> the MBR partition table is an obsolete MS-DOS era standard.<br>Only 4 <em>primary</em> partitions may be created, and of those 4, one can be an <em>extended</em> partition, which may in turn contain many <em>logical</em> partitions. + + + + + The type of <strong>partition table</strong> on the selected storage device.<br><br>The only way to change the partition table type is to erase and recreate the partition table from scratch, which destroys all data on the storage device.<br>This installer will keep the current partition table unless you explicitly choose otherwise.<br>If unsure, on modern systems GPT is preferred. + + + + + DeviceModel + + + %1 - %2 (%3) + device[name] - size[number] (device-node[name]) + + + + + %1 - (%2) + device[name] - (device-node[name]) + + + + + DracutLuksCfgJob + + + Write LUKS configuration for Dracut to %1 + + + + + Skip writing LUKS configuration for Dracut: "/" partition is not encrypted + + + + + Failed to open %1 + + + + + DummyCppJob + + + Dummy C++ Job + + + + + EditExistingPartitionDialog + + + Edit Existing Partition + + + + + Content: + + + + + &Keep + + + + + Format + + + + + Warning: Formatting the partition will erase all existing data. + + + + + &Mount Point: + + + + + Si&ze: + + + + + MiB + + + + + Fi&le System: + + + + + Flags: + + + + + Mountpoint already in use. Please select another one. + + + + + EncryptWidget + + + Form + + + + + En&crypt system + + + + + Passphrase + + + + + Confirm passphrase + + + + + Please enter the same passphrase in both boxes. + + + + + FillGlobalStorageJob + + + Set partition information + + + + + Install %1 on <strong>new</strong> %2 system partition. + + + + + Set up <strong>new</strong> %2 partition with mount point <strong>%1</strong>. + + + + + Install %2 on %3 system partition <strong>%1</strong>. + + + + + Set up %3 partition <strong>%1</strong> with mount point <strong>%2</strong>. + + + + + Install boot loader on <strong>%1</strong>. + + + + + Setting up mount points. + + + + + FinishedPage + + + Form + + + + + &Restart now + + + + + <h1>All done.</h1><br/>%1 has been set up on your computer.<br/>You may now start using your new system. + + + + + <html><head/><body><p>When this box is checked, your system will restart immediately when you click on <span style="font-style:italic;">Done</span> or close the setup program.</p></body></html> + + + + + <h1>All done.</h1><br/>%1 has been installed on your computer.<br/>You may now restart into your new system, or continue using the %2 Live environment. + + + + + <html><head/><body><p>When this box is checked, your system will restart immediately when you click on <span style="font-style:italic;">Done</span> or close the installer.</p></body></html> + + + + + <h1>Setup Failed</h1><br/>%1 has not been set up on your computer.<br/>The error message was: %2. + + + + + <h1>Installation Failed</h1><br/>%1 has not been installed on your computer.<br/>The error message was: %2. + + + + + FinishedViewStep + + + Finish + + + + + Setup Complete + + + + + Installation Complete + + + + + The setup of %1 is complete. + + + + + The installation of %1 is complete. + + + + + FormatPartitionJob + + + Format partition %1 (file system: %2, size: %3 MiB) on %4. + + + + + Format <strong>%3MiB</strong> partition <strong>%1</strong> with file system <strong>%2</strong>. + + + + + Formatting partition %1 with file system %2. + + + + + The installer failed to format partition %1 on disk '%2'. + + + + + GeneralRequirements + + + has at least %1 GiB available drive space + + + + + There is not enough drive space. At least %1 GiB is required. + + + + + has at least %1 GiB working memory + + + + + The system does not have enough working memory. At least %1 GiB is required. + + + + + is plugged in to a power source + + + + + The system is not plugged in to a power source. + + + + + is connected to the Internet + + + + + The system is not connected to the Internet. + + + + + is running the installer as an administrator (root) + + + + + The setup program is not running with administrator rights. + + + + + The installer is not running with administrator rights. + + + + + has a screen large enough to show the whole installer + + + + + The screen is too small to display the setup program. + + + + + The screen is too small to display the installer. + + + + + HostInfoJob + + + Collecting information about your machine. + + + + + IDJob + + + + + + OEM Batch Identifier + + + + + Could not create directories <code>%1</code>. + + + + + Could not open file <code>%1</code>. + + + + + Could not write to file <code>%1</code>. + + + + + InitcpioJob + + + Creating initramfs with mkinitcpio. + + + + + InitramfsJob + + + Creating initramfs. + + + + + InteractiveTerminalPage + + + Konsole not installed + + + + + Please install KDE Konsole and try again! + + + + + Executing script: &nbsp;<code>%1</code> + + + + + InteractiveTerminalViewStep + + + Script + + + + + KeyboardPage + + + Set keyboard model to %1.<br/> + + + + + Set keyboard layout to %1/%2. + + + + + KeyboardQmlViewStep + + + Keyboard + + + + + KeyboardViewStep + + + Keyboard + + + + + LCLocaleDialog + + + System locale setting + + + + + The system locale setting affects the language and character set for some command line user interface elements.<br/>The current setting is <strong>%1</strong>. + + + + + &Cancel + + + + + &OK + + + + + LicensePage + + + Form + + + + + <h1>License Agreement</h1> + + + + + I accept the terms and conditions above. + + + + + Please review the End User License Agreements (EULAs). + + + + + This setup procedure will install proprietary software that is subject to licensing terms. + + + + + If you do not agree with the terms, the setup procedure cannot continue. + + + + + This setup procedure can install proprietary software that is subject to licensing terms in order to provide additional features and enhance the user experience. + + + + + If you do not agree with the terms, proprietary software will not be installed, and open source alternatives will be used instead. + + + + + LicenseViewStep + + + License + + + + + LicenseWidget + + + URL: %1 + + + + + <strong>%1 driver</strong><br/>by %2 + %1 is an untranslatable product name, example: Creative Audigy driver + + + + + <strong>%1 graphics driver</strong><br/><font color="Grey">by %2</font> + %1 is usually a vendor name, example: Nvidia graphics driver + + + + + <strong>%1 browser plugin</strong><br/><font color="Grey">by %2</font> + + + + + <strong>%1 codec</strong><br/><font color="Grey">by %2</font> + + + + + <strong>%1 package</strong><br/><font color="Grey">by %2</font> + + + + + <strong>%1</strong><br/><font color="Grey">by %2</font> + + + + + File: %1 + + + + + Hide license text + + + + + Show the license text + + + + + Open license agreement in browser. + + + + + LocalePage + + + Region: + + + + + Zone: + + + + + + &Change... + + + + + The system language will be set to %1. + + + + + The numbers and dates locale will be set to %1. + + + + + Set timezone to %1/%2.<br/> + + + + + LocaleQmlViewStep + + + Location + + + + + LocaleViewStep + + + Location + + + + + LuksBootKeyFileJob + + + Configuring LUKS key file. + + + + + + No partitions are defined. + + + + + + + Encrypted rootfs setup error + + + + + Root partition %1 is LUKS but no passphrase has been set. + + + + + Could not create LUKS key file for root partition %1. + + + + + Could not configure LUKS key file on partition %1. + + + + + MachineIdJob + + + Generate machine-id. + + + + + Configuration Error + + + + + No root mount point is set for MachineId. + + + + + Map + + + Please select your preferred location on the map so the installer can suggest the locale + and timezone settings for you. You can fine-tune the suggested settings below. Search the map by dragging + to move and using the +/- buttons to zoom in/out or use mouse scrolling for zooming. + + + + + NetInstallViewStep + + + + Package selection + + + + + Office software + + + + + Office package + + + + + Browser software + + + + + Browser package + + + + + Web browser + + + + + Kernel + + + + + Services + + + + + Login + + + + + Desktop + + + + + Applications + + + + + Communication + + + + + Development + + + + + Office + + + + + Multimedia + + + + + Internet + + + + + Theming + + + + + Gaming + + + + + Utilities + + + + + NotesQmlViewStep + + + Notes + + + + + OEMPage + + + Ba&tch: + + + + + <html><head/><body><p>Enter a batch-identifier here. This will be stored in the target system.</p></body></html> + + + + + <html><head/><body><h1>OEM Configuration</h1><p>Calamares will use OEM settings while configuring the target system.</p></body></html> + + + + + OEMViewStep + + + OEM Configuration + + + + + Set the OEM Batch Identifier to <code>%1</code>. + + + + + Offline + + + Timezone: %1 + + + + + To be able to select a timezone, make sure you are connected to the internet. Restart the installer after connecting. You can fine-tune Language and Locale settings below. + + + + + PWQ + + + Password is too short + + + + + Password is too long + + + + + Password is too weak + + + + + Memory allocation error when setting '%1' + + + + + Memory allocation error + + + + + The password is the same as the old one + + + + + The password is a palindrome + + + + + The password differs with case changes only + + + + + The password is too similar to the old one + + + + + The password contains the user name in some form + + + + + The password contains words from the real name of the user in some form + + + + + The password contains forbidden words in some form + + + + + The password contains less than %1 digits + + + + + The password contains too few digits + + + + + The password contains less than %1 uppercase letters + + + + + The password contains too few uppercase letters + + + + + The password contains less than %1 lowercase letters + + + + + The password contains too few lowercase letters + + + + + The password contains less than %1 non-alphanumeric characters + + + + + The password contains too few non-alphanumeric characters + + + + + The password is shorter than %1 characters + + + + + The password is too short + + + + + The password is just rotated old one + + + + + The password contains less than %1 character classes + + + + + The password does not contain enough character classes + + + + + The password contains more than %1 same characters consecutively + + + + + The password contains too many same characters consecutively + + + + + The password contains more than %1 characters of the same class consecutively + + + + + The password contains too many characters of the same class consecutively + + + + + The password contains monotonic sequence longer than %1 characters + + + + + The password contains too long of a monotonic character sequence + + + + + No password supplied + + + + + Cannot obtain random numbers from the RNG device + + + + + Password generation failed - required entropy too low for settings + + + + + The password fails the dictionary check - %1 + + + + + The password fails the dictionary check + + + + + Unknown setting - %1 + + + + + Unknown setting + + + + + Bad integer value of setting - %1 + + + + + Bad integer value + + + + + Setting %1 is not of integer type + + + + + Setting is not of integer type + + + + + Setting %1 is not of string type + + + + + Setting is not of string type + + + + + Opening the configuration file failed + + + + + The configuration file is malformed + + + + + Fatal failure + + + + + Unknown error + + + + + Password is empty + + + + + PackageChooserPage + + + Form + + + + + Product Name + + + + + TextLabel + + + + + Long Product Description + + + + + Package Selection + + + + + Please pick a product from the list. The selected product will be installed. + + + + + PackageChooserViewStep + + + Packages + + + + + PackageModel + + + Name + + + + + Description + + + + + Page_Keyboard + + + Form + + + + + Keyboard Model: + + + + + Type here to test your keyboard + + + + + Page_UserSetup + + + Form + + + + + What is your name? + + + + + Your Full Name + + + + + What name do you want to use to log in? + + + + + login + + + + + What is the name of this computer? + + + + + <small>This name will be used if you make the computer visible to others on a network.</small> + + + + + Computer Name + + + + + Choose a password to keep your account safe. + + + + + + <small>Enter the same password twice, so that it can be checked for typing errors. A good password will contain a mixture of letters, numbers and punctuation, should be at least eight characters long, and should be changed at regular intervals.</small> + + + + + + Password + + + + + + Repeat Password + + + + + When this box is checked, password-strength checking is done and you will not be able to use a weak password. + + + + + Require strong passwords. + + + + + Log in automatically without asking for the password. + + + + + Use the same password for the administrator account. + + + + + Choose a password for the administrator account. + + + + + + <small>Enter the same password twice, so that it can be checked for typing errors.</small> + + + + + PartitionLabelsView + + + Root + + + + + Home + + + + + Boot + + + + + EFI system + + + + + Swap + + + + + New partition for %1 + + + + + New partition + + + + + %1 %2 + size[number] filesystem[name] + + + + + PartitionModel + + + + Free Space + + + + + + New partition + + + + + Name + + + + + File System + + + + + Mount Point + + + + + Size + + + + + PartitionPage + + + Form + + + + + Storage de&vice: + + + + + &Revert All Changes + + + + + New Partition &Table + + + + + Cre&ate + + + + + &Edit + + + + + &Delete + + + + + New Volume Group + + + + + Resize Volume Group + + + + + Deactivate Volume Group + + + + + Remove Volume Group + + + + + I&nstall boot loader on: + + + + + Are you sure you want to create a new partition table on %1? + + + + + Can not create new partition + + + + + The partition table on %1 already has %2 primary partitions, and no more can be added. Please remove one primary partition and add an extended partition, instead. + + + + + PartitionViewStep + + + Gathering system information... + + + + + Partitions + + + + + Install %1 <strong>alongside</strong> another operating system. + + + + + <strong>Erase</strong> disk and install %1. + + + + + <strong>Replace</strong> a partition with %1. + + + + + <strong>Manual</strong> partitioning. + + + + + Install %1 <strong>alongside</strong> another operating system on disk <strong>%2</strong> (%3). + + + + + <strong>Erase</strong> disk <strong>%2</strong> (%3) and install %1. + + + + + <strong>Replace</strong> a partition on disk <strong>%2</strong> (%3) with %1. + + + + + <strong>Manual</strong> partitioning on disk <strong>%1</strong> (%2). + + + + + Disk <strong>%1</strong> (%2) + + + + + Current: + + + + + After: + + + + + No EFI system partition configured + + + + + An EFI system partition is necessary to start %1.<br/><br/>To configure an EFI system partition, go back and select or create a FAT32 filesystem with the <strong>%3</strong> flag enabled and mount point <strong>%2</strong>.<br/><br/>You can continue without setting up an EFI system partition but your system may fail to start. + + + + + An EFI system partition is necessary to start %1.<br/><br/>A partition was configured with mount point <strong>%2</strong> but its <strong>%3</strong> flag is not set.<br/>To set the flag, go back and edit the partition.<br/><br/>You can continue without setting the flag but your system may fail to start. + + + + + EFI system partition flag not set + + + + + Option to use GPT on BIOS + + + + + A GPT partition table is the best option for all systems. This installer supports such a setup for BIOS systems too.<br/><br/>To configure a GPT partition table on BIOS, (if not done so already) go back and set the partition table to GPT, next create a 8 MB unformatted partition with the <strong>bios_grub</strong> flag enabled.<br/><br/>An unformatted 8 MB partition is necessary to start %1 on a BIOS system with GPT. + + + + + Boot partition not encrypted + + + + + A separate boot partition was set up together with an encrypted root partition, but the boot partition is not encrypted.<br/><br/>There are security concerns with this kind of setup, because important system files are kept on an unencrypted partition.<br/>You may continue if you wish, but filesystem unlocking will happen later during system startup.<br/>To encrypt the boot partition, go back and recreate it, selecting <strong>Encrypt</strong> in the partition creation window. + + + + + has at least one disk device available. + + + + + There are no partitions to install on. + + + + + PlasmaLnfJob + + + Plasma Look-and-Feel Job + + + + + + Could not select KDE Plasma Look-and-Feel package + + + + + PlasmaLnfPage + + + Form + + + + + Please choose a look-and-feel for the KDE Plasma Desktop. You can also skip this step and configure the look-and-feel once the system is set up. Clicking on a look-and-feel selection will give you a live preview of that look-and-feel. + + + + + Please choose a look-and-feel for the KDE Plasma Desktop. You can also skip this step and configure the look-and-feel once the system is installed. Clicking on a look-and-feel selection will give you a live preview of that look-and-feel. + + + + + PlasmaLnfViewStep + + + Look-and-Feel + + + + + PreserveFiles + + + Saving files for later ... + + + + + No files configured to save for later. + + + + + Not all of the configured files could be preserved. + + + + + ProcessResult + + + +There was no output from the command. + + + + + +Output: + + + + + + External command crashed. + + + + + Command <i>%1</i> crashed. + + + + + External command failed to start. + + + + + Command <i>%1</i> failed to start. + + + + + Internal error when starting command. + + + + + Bad parameters for process job call. + + + + + External command failed to finish. + + + + + Command <i>%1</i> failed to finish in %2 seconds. + + + + + External command finished with errors. + + + + + Command <i>%1</i> finished with exit code %2. + + + + + QObject + + + %1 (%2) + + + + + unknown + + + + + extended + + + + + unformatted + + + + + swap + + + + + Default Keyboard Model + + + + + + Default + + + + + + + + File not found + + + + + Path <pre>%1</pre> must be an absolute path. + + + + + Could not create new random file <pre>%1</pre>. + + + + + No product + + + + + No description provided. + + + + + (no mount point) + + + + + Unpartitioned space or unknown partition table + + + + + Recommended + + + <p>This computer does not satisfy some of the recommended requirements for setting up %1.<br/> + Setup can continue, but some features might be disabled.</p> + + + + + RemoveUserJob + + + Remove live user from target system + + + + + RemoveVolumeGroupJob + + + + Remove Volume Group named %1. + + + + + Remove Volume Group named <strong>%1</strong>. + + + + + The installer failed to remove a volume group named '%1'. + + + + + ReplaceWidget + + + Form + + + + + Select where to install %1.<br/><font color="red">Warning: </font>this will delete all files on the selected partition. + + + + + The selected item does not appear to be a valid partition. + + + + + %1 cannot be installed on empty space. Please select an existing partition. + + + + + %1 cannot be installed on an extended partition. Please select an existing primary or logical partition. + + + + + %1 cannot be installed on this partition. + + + + + Data partition (%1) + + + + + Unknown system partition (%1) + + + + + %1 system partition (%2) + + + + + <strong>%4</strong><br/><br/>The partition %1 is too small for %2. Please select a partition with capacity at least %3 GiB. + + + + + <strong>%2</strong><br/><br/>An EFI system partition cannot be found anywhere on this system. Please go back and use manual partitioning to set up %1. + + + + + + + <strong>%3</strong><br/><br/>%1 will be installed on %2.<br/><font color="red">Warning: </font>all data on partition %2 will be lost. + + + + + The EFI system partition at %1 will be used for starting %2. + + + + + EFI system partition: + + + + + Requirements + + + <p>This computer does not satisfy the minimum requirements for installing %1.<br/> + Installation cannot continue.</p> + + + + + <p>This computer does not satisfy some of the recommended requirements for setting up %1.<br/> + Setup can continue, but some features might be disabled.</p> + + + + + ResizeFSJob + + + Resize Filesystem Job + + + + + Invalid configuration + + + + + The file-system resize job has an invalid configuration and will not run. + + + + + KPMCore not Available + + + + + Calamares cannot start KPMCore for the file-system resize job. + + + + + + + + + Resize Failed + + + + + The filesystem %1 could not be found in this system, and cannot be resized. + + + + + The device %1 could not be found in this system, and cannot be resized. + + + + + + The filesystem %1 cannot be resized. + + + + + + The device %1 cannot be resized. + + + + + The filesystem %1 must be resized, but cannot. + + + + + The device %1 must be resized, but cannot + + + + + ResizePartitionJob + + + Resize partition %1. + + + + + Resize <strong>%2MiB</strong> partition <strong>%1</strong> to <strong>%3MiB</strong>. + + + + + Resizing %2MiB partition %1 to %3MiB. + + + + + The installer failed to resize partition %1 on disk '%2'. + + + + + ResizeVolumeGroupDialog + + + Resize Volume Group + + + + + ResizeVolumeGroupJob + + + + Resize volume group named %1 from %2 to %3. + + + + + Resize volume group named <strong>%1</strong> from <strong>%2</strong> to <strong>%3</strong>. + + + + + The installer failed to resize a volume group named '%1'. + + + + + ResultsListDialog + + + For best results, please ensure that this computer: + + + + + System requirements + + + + + ResultsListWidget + + + This computer does not satisfy the minimum requirements for setting up %1.<br/>Setup cannot continue. <a href="#details">Details...</a> + + + + + This computer does not satisfy the minimum requirements for installing %1.<br/>Installation cannot continue. <a href="#details">Details...</a> + + + + + This computer does not satisfy some of the recommended requirements for setting up %1.<br/>Setup can continue, but some features might be disabled. + + + + + This computer does not satisfy some of the recommended requirements for installing %1.<br/>Installation can continue, but some features might be disabled. + + + + + This program will ask you some questions and set up %2 on your computer. + + + + + ScanningDialog + + + Scanning storage devices... + + + + + Partitioning + + + + + SetHostNameJob + + + Set hostname %1 + + + + + Set hostname <strong>%1</strong>. + + + + + Setting hostname %1. + + + + + + Internal Error + + + + + + Cannot write hostname to target system + + + + + SetKeyboardLayoutJob + + + Set keyboard model to %1, layout to %2-%3 + + + + + Failed to write keyboard configuration for the virtual console. + + + + + + + Failed to write to %1 + + + + + Failed to write keyboard configuration for X11. + + + + + Failed to write keyboard configuration to existing /etc/default directory. + + + + + SetPartFlagsJob + + + Set flags on partition %1. + + + + + Set flags on %1MiB %2 partition. + + + + + Set flags on new partition. + + + + + Clear flags on partition <strong>%1</strong>. + + + + + Clear flags on %1MiB <strong>%2</strong> partition. + + + + + Clear flags on new partition. + + + + + Flag partition <strong>%1</strong> as <strong>%2</strong>. + + + + + Flag %1MiB <strong>%2</strong> partition as <strong>%3</strong>. + + + + + Flag new partition as <strong>%1</strong>. + + + + + Clearing flags on partition <strong>%1</strong>. + + + + + Clearing flags on %1MiB <strong>%2</strong> partition. + + + + + Clearing flags on new partition. + + + + + Setting flags <strong>%2</strong> on partition <strong>%1</strong>. + + + + + Setting flags <strong>%3</strong> on %1MiB <strong>%2</strong> partition. + + + + + Setting flags <strong>%1</strong> on new partition. + + + + + The installer failed to set flags on partition %1. + + + + + SetPasswordJob + + + Set password for user %1 + + + + + Setting password for user %1. + + + + + Bad destination system path. + + + + + rootMountPoint is %1 + + + + + Cannot disable root account. + + + + + passwd terminated with error code %1. + + + + + Cannot set password for user %1. + + + + + usermod terminated with error code %1. + + + + + SetTimezoneJob + + + Set timezone to %1/%2 + + + + + Cannot access selected timezone path. + + + + + Bad path: %1 + + + + + Cannot set timezone. + + + + + Link creation failed, target: %1; link name: %2 + + + + + Cannot set timezone, + + + + + Cannot open /etc/timezone for writing + + + + + ShellProcessJob + + + Shell Processes Job + + + + + SlideCounter + + + %L1 / %L2 + slide counter, %1 of %2 (numeric) + + + + + SummaryPage + + + This is an overview of what will happen once you start the setup procedure. + + + + + This is an overview of what will happen once you start the install procedure. + + + + + SummaryViewStep + + + Summary + + + + + TrackingInstallJob + + + Installation feedback + + + + + Sending installation feedback. + + + + + Internal error in install-tracking. + + + + + HTTP request timed out. + + + + + TrackingKUserFeedbackJob + + + KDE user feedback + + + + + Configuring KDE user feedback. + + + + + + Error in KDE user feedback configuration. + + + + + Could not configure KDE user feedback correctly, script error %1. + + + + + Could not configure KDE user feedback correctly, Calamares error %1. + + + + + TrackingMachineUpdateManagerJob + + + Machine feedback + + + + + Configuring machine feedback. + + + + + + Error in machine feedback configuration. + + + + + Could not configure machine feedback correctly, script error %1. + + + + + Could not configure machine feedback correctly, Calamares error %1. + + + + + TrackingPage + + + Form + + + + + Placeholder + + + + + <html><head/><body><p>Click here to send <span style=" font-weight:600;">no information at all</span> about your installation.</p></body></html> + + + + + <html><head/><body><p><a href="placeholder"><span style=" text-decoration: underline; color:#2980b9;">Click here for more information about user feedback</span></a></p></body></html> + + + + + Tracking helps %1 to see how often it is installed, what hardware it is installed on and which applications are used. To see what will be sent, please click the help icon next to each area. + + + + + By selecting this you will send information about your installation and hardware. This information will only be sent <b>once</b> after the installation finishes. + + + + + By selecting this you will periodically send information about your <b>machine</b> installation, hardware and applications, to %1. + + + + + By selecting this you will regularly send information about your <b>user</b> installation, hardware, applications and application usage patterns, to %1. + + + + + TrackingViewStep + + + Feedback + + + + + UsersPage + + + <small>If more than one person will use this computer, you can create multiple accounts after setup.</small> + + + + + <small>If more than one person will use this computer, you can create multiple accounts after installation.</small> + + + + + Your username is too long. + + + + + Your username must start with a lowercase letter or underscore. + + + + + Only lowercase letters, numbers, underscore and hyphen are allowed. + + + + + Your hostname is too short. + + + + + Your hostname is too long. + + + + + Only letters, numbers, underscore and hyphen are allowed. + + + + + Your passwords do not match! + + + + + UsersViewStep + + + Users + + + + + VariantModel + + + Key + + + + + Value + + + + + VolumeGroupBaseDialog + + + Create Volume Group + + + + + List of Physical Volumes + + + + + Volume Group Name: + + + + + Volume Group Type: + + + + + Physical Extent Size: + + + + + MiB + + + + + Total Size: + + + + + Used Size: + + + + + Total Sectors: + + + + + Quantity of LVs: + + + + + WelcomePage + + + Form + + + + + + Select application and system language + + + + + &About + + + + + Open donations website + + + + + &Donate + + + + + Open help and support website + + + + + &Support + + + + + Open issues and bug-tracking website + + + + + &Known issues + + + + + Open release notes website + + + + + &Release notes + + + + + <h1>Welcome to the Calamares setup program for %1.</h1> + + + + + <h1>Welcome to %1 setup.</h1> + + + + + <h1>Welcome to the Calamares installer for %1.</h1> + + + + + <h1>Welcome to the %1 installer.</h1> + + + + + %1 support + + + + + About %1 setup + + + + + About %1 installer + + + + + <h1>%1</h1><br/><strong>%2<br/>for %3</strong><br/><br/>Copyright 2014-2017 Teo Mrnjavac &lt;teo@kde.org&gt;<br/>Copyright 2017-2020 Adriaan de Groot &lt;groot@kde.org&gt;<br/>Thanks to <a href="https://calamares.io/team/">the Calamares team</a> and the <a href="https://www.transifex.com/calamares/calamares/">Calamares translators team</a>.<br/><br/><a href="https://calamares.io/">Calamares</a> development is sponsored by <br/><a href="http://www.blue-systems.com/">Blue Systems</a> - Liberating Software. + + + + + WelcomeQmlViewStep + + + Welcome + + + + + WelcomeViewStep + + + Welcome + + + + + about + + + <h1>%1</h1><br/> + <strong>%2<br/> + for %3</strong><br/><br/> + Copyright 2014-2017 Teo Mrnjavac &lt;teo@kde.org&gt;<br/> + Copyright 2017-2020 Adriaan de Groot &lt;groot@kde.org&gt;<br/> + Thanks to <a href='https://calamares.io/team/'>the Calamares team</a> + and the <a href='https://www.transifex.com/calamares/calamares/'>Calamares + translators team</a>.<br/><br/> + <a href='https://calamares.io/'>Calamares</a> + development is sponsored by <br/> + <a href='http://www.blue-systems.com/'>Blue Systems</a> - + Liberating Software. + + + + + Back + + + + + i18n + + + <h1>Languages</h1> </br> + The system locale setting affects the language and character set for some command line user interface elements. The current setting is <strong>%1</strong>. + + + + + <h1>Locales</h1> </br> + The system locale setting affects the language and character set for some command line user interface elements. The current setting is <strong>%1</strong>. + + + + + Back + + + + + keyboardq + + + Keyboard Model + + + + + Pick your preferred keyboard model or use the default one based on the detected hardware + + + + + Refresh + + + + + + Layouts + + + + + + Keyboard Layout + + + + + Models + + + + + Variants + + + + + Test your keyboard + + + + + localeq + + + System language set to %1 + + + + + Numbers and dates locale set to %1 + + + + + Change + + + + + notesqml + + + <h3>%1</h3> + <p>These are example release notes.</p> + + + + + release_notes + + + <h3>%1</h3> + <p>This an example QML file, showing options in RichText with Flickable content.</p> + + <p>QML with RichText can use HTML tags, Flickable content is useful for touchscreens.</p> + + <p><b>This is bold text</b></p> + <p><i>This is italic text</i></p> + <p><u>This is underlined text</u></p> + <p><center>This text will be center-aligned.</center></p> + <p><s>This is strikethrough</s></p> + + <p>Code example: + <code>ls -l /home</code></p> + + <p><b>Lists:</b></p> + <ul> + <li>Intel CPU systems</li> + <li>AMD CPU systems</li> + </ul> + + <p>The vertical scrollbar is adjustable, current width set to 10.</p> + + + + + Back + + + + + welcomeq + + + <h3>Welcome to the %1 <quote>%2</quote> installer</h3> + <p>This program will ask you some questions and set up %1 on your computer.</p> + + + + + About + + + + + Support + + + + + Known issues + + + + + Release notes + + + + + Donate + + + + diff --git a/lang/calamares_it_IT.ts b/lang/calamares_it_IT.ts index 3a36cc2a8..136eb7532 100644 --- a/lang/calamares_it_IT.ts +++ b/lang/calamares_it_IT.ts @@ -776,22 +776,22 @@ Il programma d'installazione sarà terminato e tutte le modifiche andranno perse <h1>Welcome to the Calamares setup program for %1</h1> - + Benvenuto nel programma di installazione Calamares di %1 <h1>Welcome to %1 setup</h1> - + Benvenuto nell'installazione di %1 <h1>Welcome to the Calamares installer for %1</h1> - + Benvenuto nel programma di installazione Calamares di %1 <h1>Welcome to the %1 installer</h1> - + Benvenuto nel programma di installazione di %1 @@ -1780,7 +1780,7 @@ Il programma d'installazione sarà terminato e tutte le modifiche andranno perse Please select your preferred location on the map so the installer can suggest the locale and timezone settings for you. You can fine-tune the suggested settings below. Search the map by dragging to move and using the +/- buttons to zoom in/out or use mouse scrolling for zooming. - + Seleziona la tua posizione sulla mappa in modo che il programma di installazione possa suggerirti la localizzazione e le impostazioni del fuso orario. Puoi modificare le impostazioni suggerite nella parte in basso. Trascina la mappa per spostarti e usa i pulsanti +/- oppure la rotella del mouse per ingrandire o rimpicciolire.
@@ -1926,12 +1926,12 @@ Il programma d'installazione sarà terminato e tutte le modifiche andranno perse Timezone: %1 - + Fuso orario: %1 To be able to select a timezone, make sure you are connected to the internet. Restart the installer after connecting. You can fine-tune Language and Locale settings below. - + Per selezionare un fuso orario, assicurati di essere collegato ad Internet. Riavvia il programma di installazione dopo esserti collegato. Puoi modificare le impostazioni relative alla lingua e alla posizione nella parte in basso. @@ -2836,7 +2836,7 @@ Output: <p>This computer does not satisfy some of the recommended requirements for setting up %1.<br/> Setup can continue, but some features might be disabled.</p> - + Questo computer non soddisfa alcuni requisiti raccomandati per poter installare %1. L'installazione può continuare, ma alcune funzionalità potrebbero essere disabilitate. @@ -2947,13 +2947,13 @@ Output: <p>This computer does not satisfy the minimum requirements for installing %1.<br/> Installation cannot continue.</p> - + Questo computer non soddisfa i requisiti minimi per poter installare %1. L'installazione non può continuare. <p>This computer does not satisfy some of the recommended requirements for setting up %1.<br/> Setup can continue, but some features might be disabled.</p> - + Questo computer non soddisfa alcuni requisiti raccomandati per poter installare %1. L'installazione può continuare, ma alcune funzionalità potrebbero essere disabilitate. @@ -3419,12 +3419,12 @@ Output: KDE user feedback - + Riscontro dell'utente di KDE Configuring KDE user feedback. - + Sto configurando il riscontro dell'utente di KDE @@ -3865,7 +3865,7 @@ Output: System language set to %1 - + Lingua di sistema impostata su %1 diff --git a/lang/calamares_nl.ts b/lang/calamares_nl.ts index 07ac3dfcd..2af094205 100644 --- a/lang/calamares_nl.ts +++ b/lang/calamares_nl.ts @@ -245,7 +245,7 @@ (%n second(s)) (%n seconde) - (%n seconden) + (%n seconde(n)) @@ -528,7 +528,7 @@ Het installatieprogramma zal afsluiten en alle wijzigingen zullen verloren gaan. <strong>Manual partitioning</strong><br/>You can create or resize partitions yourself. Having a GPT partition table and <strong>fat32 512Mb /boot partition is a must for UEFI installs</strong>, either use an existing without formatting or create one. - + <strong>Handmatige partitionering</strong><br/>Je kunt zelf de partities creëren of wijzigen. Het hebben van een GPT partititie tabel and <strong>een FAT-32 512 MB /boot partitie is belangrijk voor UEFI installaties</strong>. Gebruik een bestaande zonder formatteren of maak een nieuwe aan. @@ -1426,7 +1426,7 @@ Het installatieprogramma zal afsluiten en alle wijzigingen zullen verloren gaan. The screen is too small to display the installer. - Het schem is te klein on het installatieprogramma te vertonen. + Het scherm is te klein on het installatieprogramma te laten zien. @@ -1655,12 +1655,12 @@ Het installatieprogramma zal afsluiten en alle wijzigingen zullen verloren gaan. Hide license text - + Verberg licentietekst Show the license text - + Toon licentietekst @@ -2268,7 +2268,7 @@ Het installatieprogramma zal afsluiten en alle wijzigingen zullen verloren gaan. Your Full Name - Volledige Naam + Volledige naam @@ -2278,7 +2278,7 @@ Het installatieprogramma zal afsluiten en alle wijzigingen zullen verloren gaan. login - + Gebruikersnaam @@ -2310,13 +2310,13 @@ Het installatieprogramma zal afsluiten en alle wijzigingen zullen verloren gaan. Password - + Wachtwoord Repeat Password - + Herhaal wachtwoord diff --git a/lang/calamares_tg.ts b/lang/calamares_tg.ts new file mode 100644 index 000000000..24713cdb1 --- /dev/null +++ b/lang/calamares_tg.ts @@ -0,0 +1,3947 @@ + + + + + BootInfoWidget + + + The <strong>boot environment</strong> of this system.<br><br>Older x86 systems only support <strong>BIOS</strong>.<br>Modern systems usually use <strong>EFI</strong>, but may also show up as BIOS if started in compatibility mode. + <strong>Муҳити роҳандозӣ</strong> барои низоми ҷорӣ.<br><br>Низомҳои x86 куҳна танҳо <strong>BIOS</strong>-ро дастгирӣ менамоянд.<br>Низомҳои муосир одатан <strong>EFI</strong>-ро истифода мебаранд, аммо инчунин метавонанд ҳамчун BIOS намоиш дода шаванд, агар дар реҷаи мувофиқсозӣ оғоз шавад. + + + + This system was started with an <strong>EFI</strong> boot environment.<br><br>To configure startup from an EFI environment, this installer must deploy a boot loader application, like <strong>GRUB</strong> or <strong>systemd-boot</strong> on an <strong>EFI System Partition</strong>. This is automatic, unless you choose manual partitioning, in which case you must choose it or create it on your own. + Низоми ҷорӣ бо муҳити роҳандозии <strong>EFI</strong> оғоз ёфт.<br><br>Барои танзими оғози кор аз муҳити EFI насбкунандаи ҷорӣ бояд барномаи боркунандаи роҳандозиро монанди <strong>GRUB</strong> ё <strong>systemd-boot</strong> дар <strong>Қисми диски низомии EFI</strong> ба кор дарорад. Ин амал бояд ба таври худкор иҷро шавад, агар шумо барои қисмбандии диск тарзи дастиро интихоб накунед. Дар ин маврид шумо бояд онро мустақилона интихоб ё эҷод кунед. + + + + This system was started with a <strong>BIOS</strong> boot environment.<br><br>To configure startup from a BIOS environment, this installer must install a boot loader, like <strong>GRUB</strong>, either at the beginning of a partition or on the <strong>Master Boot Record</strong> near the beginning of the partition table (preferred). This is automatic, unless you choose manual partitioning, in which case you must set it up on your own. + Низоми ҷорӣ бо муҳити роҳандозии <strong>BIOS</strong> оғоз ёфт.<br><br>Барои танзими оғози кор аз муҳити BIOS насбкунандаи ҷорӣ бояд боркунандаи роҳандозиро монанди <strong>GRUB</strong> дар аввали қисми диск ё дар <strong>Сабти роҳандозии асосӣ</strong> назди аввали ҷадвали қисми диск (тарзи пазируфта) насб намояд. Ин амал бояд ба таври худкор иҷро шавад, агар шумо барои қисмбандии диск тарзи дастиро интихоб накунед. Дар ин маврид шумо бояд онро мустақилона интихоб ё эҷод кунед. + + + + BootLoaderModel + + + Master Boot Record of %1 + Сабти роҳандозии асосӣ барои %1 + + + + Boot Partition + Қисми диски роҳандозӣ + + + + System Partition + Қисми диски низомӣ + + + + Do not install a boot loader + Боркунандаи роҳандозӣ насб карда нашавад + + + + %1 (%2) + %1 (%2) + + + + Calamares::BlankViewStep + + + Blank Page + Саҳифаи холӣ + + + + Calamares::DebugWindow + + + Form + Шакл + + + + GlobalStorage + Захирагоҳи умумӣ + + + + JobQueue + Навбати вазифа + + + + Modules + Модулҳо + + + + Type: + Навъ: + + + + + none + ҳеҷ чиз + + + + Interface: + Интерфейс: + + + + Tools + Абзорҳо + + + + Reload Stylesheet + Аз нав бор кардани варақаи услубҳо + + + + Widget Tree + Дарахти виҷетҳо + + + + Debug information + Иттилооти ислоҳи нуқсонҳо + + + + Calamares::ExecutionViewStep + + + Set up + Танзимкунӣ + + + + Install + Насбкунӣ + + + + Calamares::FailJob + + + Job failed (%1) + Вазифа иҷро нашуд (%1) + + + + Programmed job failure was explicitly requested. + Қатъшавии вазифаи барномавӣ ботафсил дархост карда шуд. + + + + Calamares::JobThread + + + Done + Тайёр + + + + Calamares::NamedJob + + + Example job (%1) + Вазифаи намунавӣ (%1) + + + + Calamares::ProcessJob + + + Run command '%1' in target system. + Иҷро кардани фармони '%1' дар низоми интихобшуда. + + + + Run command '%1'. + Иҷро кардани фармони '%1'. + + + + Running command %1 %2 + Иҷрокунии фармони %1 %2 + + + + Calamares::PythonJob + + + Running %1 operation. + Иҷрокунии амалиёти %1. + + + + Bad working directory path + Масири феҳристи корӣ нодуруст аст + + + + Working directory %1 for python job %2 is not readable. + Феҳристи кории %1 барои вазифаи "python"-и %2 хонда намешавад. + + + + Bad main script file + Файли нақши асосӣ нодуруст аст + + + + Main script file %1 for python job %2 is not readable. + Файли нақши асосии %1 барои вазифаи "python"-и %2 хонда намешавад. + + + + Boost.Python error in job "%1". + Хатои "Boost.Python" дар вазифаи "%1". + + + + Calamares::QmlViewStep + + + Loading ... + Бор шуда истодааст... + + + + QML Step <i>%1</i>. + Қадами QML <i>%1</i>. + + + + Loading failed. + Боршавӣ қатъ шуд. + + + + Calamares::RequirementsChecker + + + Requirements checking for module <i>%1</i> is complete. + Санҷиши талабот барои модули <i>%1</i> ба анҷом расид. + + + + Waiting for %n module(s). + + Дар ҳоли интизори %n модул. + Дар ҳоли интизори %n модул. + + + + + (%n second(s)) + + (%n сония) + (%n сония) + + + + + System-requirements checking is complete. + Санҷиши талаботи низомӣ ба анҷом расид. + + + + Calamares::ViewManager + + + Setup Failed + Танзимкунӣ иҷро нашуд + + + + Installation Failed + Насбкунӣ иҷро нашуд + + + + Would you like to paste the install log to the web? + Шумо мехоҳед, ки сабти рӯйдодҳои насбро ба шабака нусха бардоред? + + + + Error + Хато + + + + + &Yes + &Ҳа + + + + + &No + &Не + + + + &Close + &Пӯшидан + + + + Install Log Paste URL + Гузоштани нишонии URL-и сабти рӯйдодҳои насб + + + + The upload was unsuccessful. No web-paste was done. + Боркунӣ иҷро нашуд. Гузариш ба шабака иҷро нашуд. + + + + Calamares Initialization Failed + Омодашавии Calamares қатъ шуд + + + + %1 can not be installed. Calamares was unable to load all of the configured modules. This is a problem with the way Calamares is being used by the distribution. + %1 насб карда намешавад. Calamares ҳамаи модулҳои танзимкардашударо бор карда натавонист. Ин мушкилие мебошад, ки бо ҳамин роҳ Calamares дар дистрибутиви ҷори кор мекунад. + + + + <br/>The following modules could not be loaded: + <br/>Модулҳои зерин бор карда намешаванд: + + + + Continue with setup? + Танзимкуниро идома медиҳед? + + + + Continue with installation? + Насбкуниро идома медиҳед? + + + + The %1 setup program is about to make changes to your disk in order to set up %2.<br/><strong>You will not be able to undo these changes.</strong> + Барномаи танзимкунии %1 барои танзим кардани %2 ба диски компютери шумо тағйиротро ворид мекунад.<br/><strong>Шумо ин тағйиротро ботил карда наметавонед.</strong> + + + + The %1 installer is about to make changes to your disk in order to install %2.<br/><strong>You will not be able to undo these changes.</strong> + Насбкунандаи %1 барои насб кардани %2 ба диски компютери шумо тағйиротро ворид мекунад.<br/><strong>Шумо ин тағйиротро ботил карда наметавонед.</strong> + + + + &Set up now + &Ҳозир танзим карда шавад + + + + &Install now + &Ҳозир насб карда шавад + + + + Go &back + &Бозгашт + + + + &Set up + &Танзим кардан + + + + &Install + &Насб кардан + + + + Setup is complete. Close the setup program. + Танзим ба анҷом расид. Барномаи танзимкуниро пӯшед. + + + + The installation is complete. Close the installer. + Насб ба анҷом расид. Барномаи насбкуниро пӯшед. + + + + Cancel setup without changing the system. + Бекор кардани танзимкунӣ бе тағйирдиҳии низом + + + + Cancel installation without changing the system. + Бекор кардани насбкунӣ бе тағйирдиҳии низом + + + + &Next + &Навбатӣ + + + + &Back + &Ба қафо + + + + &Done + &Тайёр + + + + &Cancel + &Бекор кардан + + + + Cancel setup? + Танзимкуниро бекор мекунед? + + + + Cancel installation? + Насбкуниро бекор мекунед? + + + + Do you really want to cancel the current setup process? +The setup program will quit and all changes will be lost. + Шумо дар ҳақиқат мехоҳед, ки раванди танзимкунии ҷориро бекор намоед? +Барномаи танзимкунӣ хомӯш карда мешавад ва ҳамаи тағйирот гум карда мешаванд. + + + + Do you really want to cancel the current install process? +The installer will quit and all changes will be lost. + Шумо дар ҳақиқат мехоҳед, ки раванди насбкунии ҷориро бекор намоед? +Насбкунанда хомӯш карда мешавад ва ҳамаи тағйирот гум карда мешаванд. + + + + CalamaresPython::Helper + + + Unknown exception type + Навъи истисноии номаълум + + + + unparseable Python error + + + + + unparseable Python traceback + + + + + Unfetchable Python error. + + + + + CalamaresUtils + + + Install log posted to: +%1 + + + + + CalamaresWindow + + + Show debug information + + + + + &Back + &Ба қафо + + + + &Next + &Навбатӣ + + + + &Cancel + &Бекор кардан + + + + %1 Setup Program + Барномаи танзимкунии %1 + + + + %1 Installer + Насбкунандаи %1 + + + + CheckerContainer + + + Gathering system information... + Ҷамъкунии иттилооти низомӣ... + + + + ChoicePage + + + Form + Шакл + + + + Select storage de&vice: + Интихоби дастгоҳи &захирагоҳ: + + + + + + + Current: + Ҷорӣ: + + + + After: + Баъд аз: + + + + <strong>Manual partitioning</strong><br/>You can create or resize partitions yourself. Having a GPT partition table and <strong>fat32 512Mb /boot partition is a must for UEFI installs</strong>, either use an existing without formatting or create one. + + + + + Reuse %1 as home partition for %2. + Дубора истифода бурдани %1 ҳамчун диски асосӣ барои %2. + + + + <strong>Select a partition to shrink, then drag the bottom bar to resize</strong> + + + + + %1 will be shrunk to %2MiB and a new %3MiB partition will be created for %4. + + + + + Boot loader location: + + + + + <strong>Select a partition to install on</strong> + + + + + An EFI system partition cannot be found anywhere on this system. Please go back and use manual partitioning to set up %1. + + + + + The EFI system partition at %1 will be used for starting %2. + + + + + EFI system partition: + Қисми диски низомии: + + + + This storage device does not seem to have an operating system on it. What would you like to do?<br/>You will be able to review and confirm your choices before any change is made to the storage device. + + + + + + + + <strong>Erase disk</strong><br/>This will <font color="red">delete</font> all data currently present on the selected storage device. + + + + + + + + <strong>Install alongside</strong><br/>The installer will shrink a partition to make room for %1. + + + + + + + + <strong>Replace a partition</strong><br/>Replaces a partition with %1. + + + + + This storage device has %1 on it. What would you like to do?<br/>You will be able to review and confirm your choices before any change is made to the storage device. + + + + + This storage device already has an operating system on it. What would you like to do?<br/>You will be able to review and confirm your choices before any change is made to the storage device. + + + + + This storage device has multiple operating systems on it. What would you like to do?<br/>You will be able to review and confirm your choices before any change is made to the storage device. + + + + + No Swap + + + + + Reuse Swap + + + + + Swap (no Hibernate) + + + + + Swap (with Hibernate) + + + + + Swap to file + + + + + ClearMountsJob + + + Clear mounts for partitioning operations on %1 + + + + + Clearing mounts for partitioning operations on %1. + + + + + Cleared all mounts for %1 + + + + + ClearTempMountsJob + + + Clear all temporary mounts. + + + + + Clearing all temporary mounts. + + + + + Cannot get list of temporary mounts. + + + + + Cleared all temporary mounts. + + + + + CommandList + + + + Could not run command. + + + + + The command runs in the host environment and needs to know the root path, but no rootMountPoint is defined. + + + + + The command needs to know the user's name, but no username is defined. + + + + + Config + + + Set keyboard model to %1.<br/> + Намунаи клавиатура ба %1 танзим карда мешавад.<br/> + + + + Set keyboard layout to %1/%2. + Тарҳбандии клавиатура ба %1 %1/%2 танзим карда мешавад. + + + + The system language will be set to %1. + Забони низом ба %1 танзим карда мешавад. + + + + The numbers and dates locale will be set to %1. + Низоми рақамҳо ва санаҳо ба %1 танзим карда мешавад. + + + + Set timezone to %1/%2.<br/> + Танзими минтақаи вақт ба %1/%2.<br/> + + + + Network Installation. (Disabled: Incorrect configuration) + + + + + Network Installation. (Disabled: Received invalid groups data) + + + + + Network Installation. (Disabled: internal error) + + + + + Network Installation. (Disabled: Unable to fetch package lists, check your network connection) + + + + + This computer does not satisfy the minimum requirements for setting up %1.<br/>Setup cannot continue. <a href="#details">Details...</a> + + + + + This computer does not satisfy the minimum requirements for installing %1.<br/>Installation cannot continue. <a href="#details">Details...</a> + + + + + This computer does not satisfy some of the recommended requirements for setting up %1.<br/>Setup can continue, but some features might be disabled. + + + + + This computer does not satisfy some of the recommended requirements for installing %1.<br/>Installation can continue, but some features might be disabled. + + + + + This program will ask you some questions and set up %2 on your computer. + Ин барнома аз Шумо якчанд савол мепурсад ва %2-ро дар компютери шумо танзим мекунад. + + + + <h1>Welcome to the Calamares setup program for %1</h1> + <h1>Хуш омадед ба барномаи танзимкунии Calamares барои %1</h1> + + + + <h1>Welcome to %1 setup</h1> + <h1>Хуш омадед ба танзимкунии %1</h1> + + + + <h1>Welcome to the Calamares installer for %1</h1> + <h1>Хуш омадед ба насбкунандаи Calamares барои %1</h1> + + + + <h1>Welcome to the %1 installer</h1> + <h1>Хуш омадед ба насбкунандаи %1</h1> + + + + ContextualProcessJob + + + Contextual Processes Job + Вазифаи равандҳои мазмунӣ + + + + CreatePartitionDialog + + + Create a Partition + Эҷод кардани қисми диск + + + + Si&ze: + &Андоза: + + + + MiB + МБ + + + + Partition &Type: + &Навъи қисми диск: + + + + &Primary + &Асосӣ + + + + E&xtended + &Афзуда + + + + Fi&le System: + &Низоми файлӣ: + + + + LVM LV name + Номи LVM LV + + + + &Mount Point: + &Нуқтаи васл: + + + + Flags: + Нишонҳо: + + + + En&crypt + &Рамзгузорӣ кардан + + + + Logical + Мантиқӣ + + + + Primary + Асосӣ + + + + GPT + GPT + + + + Mountpoint already in use. Please select another one. + + + + + CreatePartitionJob + + + Create new %2MiB partition on %4 (%3) with file system %1. + + + + + Create new <strong>%2MiB</strong> partition on <strong>%4</strong> (%3) with file system <strong>%1</strong>. + + + + + Creating new %1 partition on %2. + + + + + The installer failed to create partition on disk '%1'. + + + + + CreatePartitionTableDialog + + + Create Partition Table + + + + + Creating a new partition table will delete all existing data on the disk. + + + + + What kind of partition table do you want to create? + + + + + Master Boot Record (MBR) + + + + + GUID Partition Table (GPT) + + + + + CreatePartitionTableJob + + + Create new %1 partition table on %2. + + + + + Create new <strong>%1</strong> partition table on <strong>%2</strong> (%3). + + + + + Creating new %1 partition table on %2. + + + + + The installer failed to create a partition table on %1. + + + + + CreateUserJob + + + Create user %1 + Эҷод кардани корбари %1 + + + + Create user <strong>%1</strong>. + Эҷод кардани корбари <strong>%1</strong>. + + + + Creating user %1. + Эҷодкунии корбари %1. + + + + Sudoers dir is not writable. + + + + + Cannot create sudoers file for writing. + + + + + Cannot chmod sudoers file. + + + + + Cannot open groups file for reading. + + + + + CreateVolumeGroupDialog + + + Create Volume Group + Эҷод кардани гурӯҳи ҳаҷм + + + + CreateVolumeGroupJob + + + Create new volume group named %1. + Эҷод кардани гурӯҳи ҳаҷми нав бо номи %1. + + + + Create new volume group named <strong>%1</strong>. + Эҷод кардани гурӯҳи ҳаҷми нав бо номи <strong>%1</strong>. + + + + Creating new volume group named %1. + Эҷодкунии гурӯҳи ҳаҷм бо номи %1. + + + + The installer failed to create a volume group named '%1'. + Насбкунанда гурӯҳи ҳаҷмро бо номи '%1' эҷод карда натавонист. + + + + DeactivateVolumeGroupJob + + + + Deactivate volume group named %1. + Ғайрифаъол кардани гурӯҳи ҳаҷм бо номи %1. + + + + Deactivate volume group named <strong>%1</strong>. + Ғайрифаъол кардани гурӯҳи ҳаҷм бо номи <strong>%1</strong>. + + + + The installer failed to deactivate a volume group named %1. + Насбкунанда гурӯҳи ҳаҷмро бо номи %1 ғайрифаъол карда натавонист. + + + + DeletePartitionJob + + + Delete partition %1. + Нест кардани қисми диски %1. + + + + Delete partition <strong>%1</strong>. + Нест кардани қисми диски <strong>%1</strong>. + + + + Deleting partition %1. + Несткунии қисми диски %1. + + + + The installer failed to delete partition %1. + Насбкунанда қисми диски %1-ро нест карда натавонист. + + + + DeviceInfoWidget + + + This device has a <strong>%1</strong> partition table. + + + + + This is a <strong>loop</strong> device.<br><br>It is a pseudo-device with no partition table that makes a file accessible as a block device. This kind of setup usually only contains a single filesystem. + + + + + This installer <strong>cannot detect a partition table</strong> on the selected storage device.<br><br>The device either has no partition table, or the partition table is corrupted or of an unknown type.<br>This installer can create a new partition table for you, either automatically, or through the manual partitioning page. + + + + + <br><br>This is the recommended partition table type for modern systems which start from an <strong>EFI</strong> boot environment. + + + + + <br><br>This partition table type is only advisable on older systems which start from a <strong>BIOS</strong> boot environment. GPT is recommended in most other cases.<br><br><strong>Warning:</strong> the MBR partition table is an obsolete MS-DOS era standard.<br>Only 4 <em>primary</em> partitions may be created, and of those 4, one can be an <em>extended</em> partition, which may in turn contain many <em>logical</em> partitions. + + + + + The type of <strong>partition table</strong> on the selected storage device.<br><br>The only way to change the partition table type is to erase and recreate the partition table from scratch, which destroys all data on the storage device.<br>This installer will keep the current partition table unless you explicitly choose otherwise.<br>If unsure, on modern systems GPT is preferred. + + + + + DeviceModel + + + %1 - %2 (%3) + device[name] - size[number] (device-node[name]) + %1 - %2 (%3) + + + + %1 - (%2) + device[name] - (device-node[name]) + %1 - (%2) + + + + DracutLuksCfgJob + + + Write LUKS configuration for Dracut to %1 + + + + + Skip writing LUKS configuration for Dracut: "/" partition is not encrypted + + + + + Failed to open %1 + %1 кушода нашуд + + + + DummyCppJob + + + Dummy C++ Job + + + + + EditExistingPartitionDialog + + + Edit Existing Partition + + + + + Content: + Муҳтаво: + + + + &Keep + &Нигоҳ доштан + + + + Format + + + + + Warning: Formatting the partition will erase all existing data. + + + + + &Mount Point: + &Нуқтаи васл: + + + + Si&ze: + &Андоза: + + + + MiB + МБ + + + + Fi&le System: + &Низоми файлӣ: + + + + Flags: + Нишонҳо: + + + + Mountpoint already in use. Please select another one. + + + + + EncryptWidget + + + Form + Шакл + + + + En&crypt system + &Рамзгузории низом + + + + Passphrase + Гузарвожа + + + + Confirm passphrase + + + + + Please enter the same passphrase in both boxes. + + + + + FillGlobalStorageJob + + + Set partition information + + + + + Install %1 on <strong>new</strong> %2 system partition. + + + + + Set up <strong>new</strong> %2 partition with mount point <strong>%1</strong>. + + + + + Install %2 on %3 system partition <strong>%1</strong>. + + + + + Set up %3 partition <strong>%1</strong> with mount point <strong>%2</strong>. + + + + + Install boot loader on <strong>%1</strong>. + + + + + Setting up mount points. + + + + + FinishedPage + + + Form + Шакл + + + + &Restart now + + + + + <h1>All done.</h1><br/>%1 has been set up on your computer.<br/>You may now start using your new system. + + + + + <html><head/><body><p>When this box is checked, your system will restart immediately when you click on <span style="font-style:italic;">Done</span> or close the setup program.</p></body></html> + + + + + <h1>All done.</h1><br/>%1 has been installed on your computer.<br/>You may now restart into your new system, or continue using the %2 Live environment. + + + + + <html><head/><body><p>When this box is checked, your system will restart immediately when you click on <span style="font-style:italic;">Done</span> or close the installer.</p></body></html> + + + + + <h1>Setup Failed</h1><br/>%1 has not been set up on your computer.<br/>The error message was: %2. + + + + + <h1>Installation Failed</h1><br/>%1 has not been installed on your computer.<br/>The error message was: %2. + + + + + FinishedViewStep + + + Finish + Анҷом + + + + Setup Complete + + + + + Installation Complete + + + + + The setup of %1 is complete. + + + + + The installation of %1 is complete. + + + + + FormatPartitionJob + + + Format partition %1 (file system: %2, size: %3 MiB) on %4. + + + + + Format <strong>%3MiB</strong> partition <strong>%1</strong> with file system <strong>%2</strong>. + + + + + Formatting partition %1 with file system %2. + + + + + The installer failed to format partition %1 on disk '%2'. + + + + + GeneralRequirements + + + has at least %1 GiB available drive space + + + + + There is not enough drive space. At least %1 GiB is required. + + + + + has at least %1 GiB working memory + + + + + The system does not have enough working memory. At least %1 GiB is required. + + + + + is plugged in to a power source + + + + + The system is not plugged in to a power source. + + + + + is connected to the Internet + + + + + The system is not connected to the Internet. + + + + + is running the installer as an administrator (root) + + + + + The setup program is not running with administrator rights. + + + + + The installer is not running with administrator rights. + + + + + has a screen large enough to show the whole installer + + + + + The screen is too small to display the setup program. + + + + + The screen is too small to display the installer. + + + + + HostInfoJob + + + Collecting information about your machine. + + + + + IDJob + + + + + + OEM Batch Identifier + + + + + Could not create directories <code>%1</code>. + + + + + Could not open file <code>%1</code>. + + + + + Could not write to file <code>%1</code>. + + + + + InitcpioJob + + + Creating initramfs with mkinitcpio. + + + + + InitramfsJob + + + Creating initramfs. + + + + + InteractiveTerminalPage + + + Konsole not installed + + + + + Please install KDE Konsole and try again! + + + + + Executing script: &nbsp;<code>%1</code> + + + + + InteractiveTerminalViewStep + + + Script + Нақш + + + + KeyboardPage + + + Set keyboard model to %1.<br/> + Намунаи клавиатура ба %1 танзим карда мешавад.<br/> + + + + Set keyboard layout to %1/%2. + Тарҳбандии клавиатура ба %1 %1/%2 танзим карда мешавад. + + + + KeyboardQmlViewStep + + + Keyboard + Клавиатура + + + + KeyboardViewStep + + + Keyboard + Клавиатура + + + + LCLocaleDialog + + + System locale setting + + + + + The system locale setting affects the language and character set for some command line user interface elements.<br/>The current setting is <strong>%1</strong>. + + + + + &Cancel + &Бекор кардан + + + + &OK + &ХУБ + + + + LicensePage + + + Form + Шакл + + + + <h1>License Agreement</h1> + <h1>Созишномаи иҷозатномавӣ</h1> + + + + I accept the terms and conditions above. + Ман шарту шароитҳои дар боло зикршударо қабул мекунам. + + + + Please review the End User License Agreements (EULAs). + + + + + This setup procedure will install proprietary software that is subject to licensing terms. + + + + + If you do not agree with the terms, the setup procedure cannot continue. + + + + + This setup procedure can install proprietary software that is subject to licensing terms in order to provide additional features and enhance the user experience. + + + + + If you do not agree with the terms, proprietary software will not be installed, and open source alternatives will be used instead. + + + + + LicenseViewStep + + + License + + + + + LicenseWidget + + + URL: %1 + + + + + <strong>%1 driver</strong><br/>by %2 + %1 is an untranslatable product name, example: Creative Audigy driver + + + + + <strong>%1 graphics driver</strong><br/><font color="Grey">by %2</font> + %1 is usually a vendor name, example: Nvidia graphics driver + + + + + <strong>%1 browser plugin</strong><br/><font color="Grey">by %2</font> + + + + + <strong>%1 codec</strong><br/><font color="Grey">by %2</font> + + + + + <strong>%1 package</strong><br/><font color="Grey">by %2</font> + + + + + <strong>%1</strong><br/><font color="Grey">by %2</font> + + + + + File: %1 + Файл: %1 + + + + Hide license text + + + + + Show the license text + + + + + Open license agreement in browser. + + + + + LocalePage + + + Region: + Минтақа: + + + + Zone: + Шаҳр: + + + + + &Change... + &Тағйир додан... + + + + The system language will be set to %1. + Забони низом ба %1 танзим карда мешавад. + + + + The numbers and dates locale will be set to %1. + Низоми рақамҳо ва санаҳо ба %1 танзим карда мешавад. + + + + Set timezone to %1/%2.<br/> + Танзим кардани минтақаи вақт ба %1/%2.<br/> + + + + LocaleQmlViewStep + + + Location + Ҷойгиршавӣ + + + + LocaleViewStep + + + Location + Ҷойгиршавӣ + + + + LuksBootKeyFileJob + + + Configuring LUKS key file. + + + + + + No partitions are defined. + + + + + + + Encrypted rootfs setup error + Хатои танзими рамзгузории "rootfs" + + + + Root partition %1 is LUKS but no passphrase has been set. + + + + + Could not create LUKS key file for root partition %1. + + + + + Could not configure LUKS key file on partition %1. + + + + + MachineIdJob + + + Generate machine-id. + + + + + Configuration Error + Хатои танзимкунӣ + + + + No root mount point is set for MachineId. + + + + + Map + + + Please select your preferred location on the map so the installer can suggest the locale + and timezone settings for you. You can fine-tune the suggested settings below. Search the map by dragging + to move and using the +/- buttons to zoom in/out or use mouse scrolling for zooming. + + + + + NetInstallViewStep + + + + Package selection + + + + + Office software + Нармафзори идорӣ + + + + Office package + + + + + Browser software + + + + + Browser package + + + + + Web browser + + + + + Kernel + + + + + Services + Хидматҳо + + + + Login + Воридшавӣ + + + + Desktop + Мизи корӣ + + + + Applications + Барномаҳо + + + + Communication + Воситаҳои алоқа + + + + Development + Барномарезӣ + + + + Office + Идора + + + + Multimedia + Мултимедиа + + + + Internet + Интернет + + + + Theming + Мавзӯъҳо + + + + Gaming + Бозиҳо + + + + Utilities + Барномаҳои муфид + + + + NotesQmlViewStep + + + Notes + Ёддоштҳо + + + + OEMPage + + + Ba&tch: + + + + + <html><head/><body><p>Enter a batch-identifier here. This will be stored in the target system.</p></body></html> + + + + + <html><head/><body><h1>OEM Configuration</h1><p>Calamares will use OEM settings while configuring the target system.</p></body></html> + + + + + OEMViewStep + + + OEM Configuration + + + + + Set the OEM Batch Identifier to <code>%1</code>. + + + + + Offline + + + Timezone: %1 + Минтақаи вақт: %1 + + + + To be able to select a timezone, make sure you are connected to the internet. Restart the installer after connecting. You can fine-tune Language and Locale settings below. + + + + + PWQ + + + Password is too short + Ниҳонвожа хеле кӯтоҳ аст + + + + Password is too long + Ниҳонвожа хеле дароз аст + + + + Password is too weak + Ниҳонвожа хеле заиф аст + + + + Memory allocation error when setting '%1' + + + + + Memory allocation error + + + + + The password is the same as the old one + + + + + The password is a palindrome + + + + + The password differs with case changes only + + + + + The password is too similar to the old one + + + + + The password contains the user name in some form + + + + + The password contains words from the real name of the user in some form + + + + + The password contains forbidden words in some form + + + + + The password contains less than %1 digits + + + + + The password contains too few digits + + + + + The password contains less than %1 uppercase letters + + + + + The password contains too few uppercase letters + + + + + The password contains less than %1 lowercase letters + + + + + The password contains too few lowercase letters + + + + + The password contains less than %1 non-alphanumeric characters + + + + + The password contains too few non-alphanumeric characters + + + + + The password is shorter than %1 characters + + + + + The password is too short + Ниҳонвожа хеле кӯтоҳ аст + + + + The password is just rotated old one + + + + + The password contains less than %1 character classes + + + + + The password does not contain enough character classes + + + + + The password contains more than %1 same characters consecutively + + + + + The password contains too many same characters consecutively + + + + + The password contains more than %1 characters of the same class consecutively + + + + + The password contains too many characters of the same class consecutively + + + + + The password contains monotonic sequence longer than %1 characters + + + + + The password contains too long of a monotonic character sequence + + + + + No password supplied + + + + + Cannot obtain random numbers from the RNG device + + + + + Password generation failed - required entropy too low for settings + + + + + The password fails the dictionary check - %1 + + + + + The password fails the dictionary check + + + + + Unknown setting - %1 + Танзими номаълум - %1 + + + + Unknown setting + Танзими номаълум + + + + Bad integer value of setting - %1 + + + + + Bad integer value + + + + + Setting %1 is not of integer type + + + + + Setting is not of integer type + + + + + Setting %1 is not of string type + + + + + Setting is not of string type + + + + + Opening the configuration file failed + + + + + The configuration file is malformed + + + + + Fatal failure + + + + + Unknown error + Хатои номаълум + + + + Password is empty + Ниҳонвожаро ворид накардед + + + + PackageChooserPage + + + Form + Шакл + + + + Product Name + Номи маҳсул + + + + TextLabel + + + + + Long Product Description + + + + + Package Selection + Интихоби бастаҳо + + + + Please pick a product from the list. The selected product will be installed. + Лутфан, маҳсулеро аз рӯйхат интихоб намоед. Маҳсули интихобшуда насб карда мешавад. + + + + PackageChooserViewStep + + + Packages + Бастаҳо + + + + PackageModel + + + Name + Ном + + + + Description + Тавсиф + + + + Page_Keyboard + + + Form + Шакл + + + + Keyboard Model: + Намунаи клавиатура: + + + + Type here to test your keyboard + Барои санҷидани клавиатура матнро дар ин сатр ворид намоед + + + + Page_UserSetup + + + Form + Шакл + + + + What is your name? + Номи шумо чист? + + + + Your Full Name + Номи пурраи шумо + + + + What name do you want to use to log in? + Кадом номро барои ворид шудан ба низом истифода мебаред? + + + + login + воридшавӣ + + + + What is the name of this computer? + Номи ин компютер чист? + + + + <small>This name will be used if you make the computer visible to others on a network.</small> + + + + + Computer Name + Номи компютер + + + + Choose a password to keep your account safe. + Барои эмин нигоҳ доштани ҳисоби худ ниҳонвожаеро интихоб намоед. + + + + + <small>Enter the same password twice, so that it can be checked for typing errors. A good password will contain a mixture of letters, numbers and punctuation, should be at least eight characters long, and should be changed at regular intervals.</small> + + + + + + Password + Ниҳонвожа + + + + + Repeat Password + Ниҳонвожаро такроран ворид намоед + + + + When this box is checked, password-strength checking is done and you will not be able to use a weak password. + + + + + Require strong passwords. + Ниҳонвожаҳои қавӣ лозиманд. + + + + Log in automatically without asking for the password. + Ба таври худкор бе дархости ниҳонвожа ворид карда шавад. + + + + Use the same password for the administrator account. + + + + + Choose a password for the administrator account. + + + + + + <small>Enter the same password twice, so that it can be checked for typing errors.</small> + + + + + PartitionLabelsView + + + Root + Реша + + + + Home + Асосӣ + + + + Boot + Роҳандозӣ + + + + EFI system + Низоми EFI + + + + Swap + Мубодила + + + + New partition for %1 + Қисми диски нав барои %1 + + + + New partition + Қисми диски нав + + + + %1 %2 + size[number] filesystem[name] + %1 %2 + + + + PartitionModel + + + + Free Space + Фазои озод + + + + + New partition + Қисми диски нав + + + + Name + Ном + + + + File System + Низоми файлӣ + + + + Mount Point + Нуқтаи васл + + + + Size + Андоза + + + + PartitionPage + + + Form + Шакл + + + + Storage de&vice: + &Дастгоҳи захирагоҳ: + + + + &Revert All Changes + &Бозгардонидани ҳамаи тағйирот + + + + New Partition &Table + &Ҷадвали қисми диски нав + + + + Cre&ate + &Эҷод кардан + + + + &Edit + &Таҳрир кардан + + + + &Delete + &Нест кардан + + + + New Volume Group + Эҷод кардани гурӯҳи ҳаҷми нав + + + + Resize Volume Group + Иваз кардани андозаи гурӯҳи ҳаҷм + + + + Deactivate Volume Group + Ғайрифаъол кардани гурӯҳи ҳаҷм + + + + Remove Volume Group + Тоза кардани гурӯҳи ҳаҷм + + + + I&nstall boot loader on: + &Насб кардани боркунандаи роҳандозӣ дар: + + + + Are you sure you want to create a new partition table on %1? + Шумо мутмаин ҳастед, ки мехоҳед ҷадвали қисми диски навро дар %1 эҷод намоед? + + + + Can not create new partition + Қисми диски нав эҷод карда намешавад + + + + The partition table on %1 already has %2 primary partitions, and no more can be added. Please remove one primary partition and add an extended partition, instead. + + + + + PartitionViewStep + + + Gathering system information... + Ҷамъкунии иттилооти низомӣ... + + + + Partitions + Қисмҳои диск + + + + Install %1 <strong>alongside</strong> another operating system. + + + + + <strong>Erase</strong> disk and install %1. + <strong>Пок кардани</strong> диск ва насб кардани %1. + + + + <strong>Replace</strong> a partition with %1. + <strong>Иваз кардани</strong> қисми диск бо %1. + + + + <strong>Manual</strong> partitioning. + <strong>Ба таври дастӣ</strong> эҷод кардани қисмҳои диск. + + + + Install %1 <strong>alongside</strong> another operating system on disk <strong>%2</strong> (%3). + + + + + <strong>Erase</strong> disk <strong>%2</strong> (%3) and install %1. + + + + + <strong>Replace</strong> a partition on disk <strong>%2</strong> (%3) with %1. + + + + + <strong>Manual</strong> partitioning on disk <strong>%1</strong> (%2). + + + + + Disk <strong>%1</strong> (%2) + Диски <strong>%1</strong> (%2) + + + + Current: + Ҷорӣ: + + + + After: + Баъд аз: + + + + No EFI system partition configured + + + + + An EFI system partition is necessary to start %1.<br/><br/>To configure an EFI system partition, go back and select or create a FAT32 filesystem with the <strong>%3</strong> flag enabled and mount point <strong>%2</strong>.<br/><br/>You can continue without setting up an EFI system partition but your system may fail to start. + + + + + An EFI system partition is necessary to start %1.<br/><br/>A partition was configured with mount point <strong>%2</strong> but its <strong>%3</strong> flag is not set.<br/>To set the flag, go back and edit the partition.<br/><br/>You can continue without setting the flag but your system may fail to start. + + + + + EFI system partition flag not set + + + + + Option to use GPT on BIOS + + + + + A GPT partition table is the best option for all systems. This installer supports such a setup for BIOS systems too.<br/><br/>To configure a GPT partition table on BIOS, (if not done so already) go back and set the partition table to GPT, next create a 8 MB unformatted partition with the <strong>bios_grub</strong> flag enabled.<br/><br/>An unformatted 8 MB partition is necessary to start %1 on a BIOS system with GPT. + + + + + Boot partition not encrypted + Қисми диски роҳандозӣ рамзгузорӣ нашудааст + + + + A separate boot partition was set up together with an encrypted root partition, but the boot partition is not encrypted.<br/><br/>There are security concerns with this kind of setup, because important system files are kept on an unencrypted partition.<br/>You may continue if you wish, but filesystem unlocking will happen later during system startup.<br/>To encrypt the boot partition, go back and recreate it, selecting <strong>Encrypt</strong> in the partition creation window. + + + + + has at least one disk device available. + + + + + There are no partitions to install on. + + + + + PlasmaLnfJob + + + Plasma Look-and-Feel Job + + + + + + Could not select KDE Plasma Look-and-Feel package + + + + + PlasmaLnfPage + + + Form + Шакл + + + + Please choose a look-and-feel for the KDE Plasma Desktop. You can also skip this step and configure the look-and-feel once the system is set up. Clicking on a look-and-feel selection will give you a live preview of that look-and-feel. + + + + + Please choose a look-and-feel for the KDE Plasma Desktop. You can also skip this step and configure the look-and-feel once the system is installed. Clicking on a look-and-feel selection will give you a live preview of that look-and-feel. + + + + + PlasmaLnfViewStep + + + Look-and-Feel + Намуди зоҳирӣ + + + + PreserveFiles + + + Saving files for later ... + + + + + No files configured to save for later. + + + + + Not all of the configured files could be preserved. + + + + + ProcessResult + + + +There was no output from the command. + + + + + +Output: + + + + + + External command crashed. + + + + + Command <i>%1</i> crashed. + + + + + External command failed to start. + + + + + Command <i>%1</i> failed to start. + + + + + Internal error when starting command. + + + + + Bad parameters for process job call. + + + + + External command failed to finish. + + + + + Command <i>%1</i> failed to finish in %2 seconds. + + + + + External command finished with errors. + + + + + Command <i>%1</i> finished with exit code %2. + + + + + QObject + + + %1 (%2) + %1 (%2) + + + + unknown + номаълум + + + + extended + афзуда + + + + unformatted + + + + + swap + мубодила + + + + Default Keyboard Model + Намунаи клавиатураи муқаррар + + + + + Default + Муқаррар + + + + + + + File not found + Файл ёфт нашуд + + + + Path <pre>%1</pre> must be an absolute path. + + + + + Could not create new random file <pre>%1</pre>. + + + + + No product + + + + + No description provided. + + + + + (no mount point) + + + + + Unpartitioned space or unknown partition table + + + + + Recommended + + + <p>This computer does not satisfy some of the recommended requirements for setting up %1.<br/> + Setup can continue, but some features might be disabled.</p> + + + + + RemoveUserJob + + + Remove live user from target system + + + + + RemoveVolumeGroupJob + + + + Remove Volume Group named %1. + Тоза кардани гурӯҳи ҳаҷм бо номи %1. + + + + Remove Volume Group named <strong>%1</strong>. + Тоза кардани гурӯҳи ҳаҷм бо номи <strong>%1</strong>. + + + + The installer failed to remove a volume group named '%1'. + Насбкунанда гурӯҳи ҳаҷмро бо номи '%1' тоза карда натавонист. + + + + ReplaceWidget + + + Form + Шакл + + + + Select where to install %1.<br/><font color="red">Warning: </font>this will delete all files on the selected partition. + + + + + The selected item does not appear to be a valid partition. + + + + + %1 cannot be installed on empty space. Please select an existing partition. + + + + + %1 cannot be installed on an extended partition. Please select an existing primary or logical partition. + + + + + %1 cannot be installed on this partition. + %1 дар ин қисми диск насб карда намешавад. + + + + Data partition (%1) + Қисми диски иттилоотӣ (%1) + + + + Unknown system partition (%1) + Қисми диски низомии номаълум (%1) + + + + %1 system partition (%2) + Қисми диски низомии %1 (%2) + + + + <strong>%4</strong><br/><br/>The partition %1 is too small for %2. Please select a partition with capacity at least %3 GiB. + + + + + <strong>%2</strong><br/><br/>An EFI system partition cannot be found anywhere on this system. Please go back and use manual partitioning to set up %1. + + + + + + + <strong>%3</strong><br/><br/>%1 will be installed on %2.<br/><font color="red">Warning: </font>all data on partition %2 will be lost. + + + + + The EFI system partition at %1 will be used for starting %2. + + + + + EFI system partition: + Қисми диски низомии: + + + + Requirements + + + <p>This computer does not satisfy the minimum requirements for installing %1.<br/> + Installation cannot continue.</p> + + + + + <p>This computer does not satisfy some of the recommended requirements for setting up %1.<br/> + Setup can continue, but some features might be disabled.</p> + + + + + ResizeFSJob + + + Resize Filesystem Job + + + + + Invalid configuration + + + + + The file-system resize job has an invalid configuration and will not run. + + + + + KPMCore not Available + + + + + Calamares cannot start KPMCore for the file-system resize job. + + + + + + + + + Resize Failed + Андоза иваз карда нашуд + + + + The filesystem %1 could not be found in this system, and cannot be resized. + + + + + The device %1 could not be found in this system, and cannot be resized. + + + + + + The filesystem %1 cannot be resized. + + + + + + The device %1 cannot be resized. + + + + + The filesystem %1 must be resized, but cannot. + + + + + The device %1 must be resized, but cannot + + + + + ResizePartitionJob + + + Resize partition %1. + Иваз кардани андозаи қисми диски %1. + + + + Resize <strong>%2MiB</strong> partition <strong>%1</strong> to <strong>%3MiB</strong>. + + + + + Resizing %2MiB partition %1 to %3MiB. + + + + + The installer failed to resize partition %1 on disk '%2'. + + + + + ResizeVolumeGroupDialog + + + Resize Volume Group + Иваз кардани андозаи гурӯҳи ҳаҷм + + + + ResizeVolumeGroupJob + + + + Resize volume group named %1 from %2 to %3. + Иваз кардани андозаи гурӯҳи ҳаҷм бо номи %1 аз %2 ба %3. + + + + Resize volume group named <strong>%1</strong> from <strong>%2</strong> to <strong>%3</strong>. + Иваз кардани андозаи гурӯҳи ҳаҷм бо номи <strong>%1</strong> аз <strong>%2</strong> ба <strong>%3</strong>. + + + + The installer failed to resize a volume group named '%1'. + Насбкунанда андозаи гурӯҳи ҳаҷмро бо номи '%1' иваз карда натавонист. + + + + ResultsListDialog + + + For best results, please ensure that this computer: + + + + + System requirements + Талаботи низом + + + + ResultsListWidget + + + This computer does not satisfy the minimum requirements for setting up %1.<br/>Setup cannot continue. <a href="#details">Details...</a> + + + + + This computer does not satisfy the minimum requirements for installing %1.<br/>Installation cannot continue. <a href="#details">Details...</a> + + + + + This computer does not satisfy some of the recommended requirements for setting up %1.<br/>Setup can continue, but some features might be disabled. + + + + + This computer does not satisfy some of the recommended requirements for installing %1.<br/>Installation can continue, but some features might be disabled. + + + + + This program will ask you some questions and set up %2 on your computer. + Ин барнома аз Шумо якчанд савол мепурсад ва %2-ро дар компютери шумо танзим мекунад. + + + + ScanningDialog + + + Scanning storage devices... + + + + + Partitioning + + + + + SetHostNameJob + + + Set hostname %1 + + + + + Set hostname <strong>%1</strong>. + + + + + Setting hostname %1. + + + + + + Internal Error + Хатои дохилӣ + + + + + Cannot write hostname to target system + + + + + SetKeyboardLayoutJob + + + Set keyboard model to %1, layout to %2-%3 + Танзимкунии намунаи клавиатура ба %1, тарҳбандӣ ба %2-%3 + + + + Failed to write keyboard configuration for the virtual console. + + + + + + + Failed to write to %1 + + + + + Failed to write keyboard configuration for X11. + + + + + Failed to write keyboard configuration to existing /etc/default directory. + + + + + SetPartFlagsJob + + + Set flags on partition %1. + + + + + Set flags on %1MiB %2 partition. + + + + + Set flags on new partition. + + + + + Clear flags on partition <strong>%1</strong>. + + + + + Clear flags on %1MiB <strong>%2</strong> partition. + + + + + Clear flags on new partition. + + + + + Flag partition <strong>%1</strong> as <strong>%2</strong>. + + + + + Flag %1MiB <strong>%2</strong> partition as <strong>%3</strong>. + + + + + Flag new partition as <strong>%1</strong>. + + + + + Clearing flags on partition <strong>%1</strong>. + + + + + Clearing flags on %1MiB <strong>%2</strong> partition. + + + + + Clearing flags on new partition. + + + + + Setting flags <strong>%2</strong> on partition <strong>%1</strong>. + + + + + Setting flags <strong>%3</strong> on %1MiB <strong>%2</strong> partition. + + + + + Setting flags <strong>%1</strong> on new partition. + + + + + The installer failed to set flags on partition %1. + + + + + SetPasswordJob + + + Set password for user %1 + Танзими ниҳонвожа барои корбари %1 + + + + Setting password for user %1. + Танзимкунии ниҳонвожа барои корбари %1. + + + + Bad destination system path. + + + + + rootMountPoint is %1 + + + + + Cannot disable root account. + + + + + passwd terminated with error code %1. + + + + + Cannot set password for user %1. + Ниҳонвожа барои корбари %1 танзим карда намешавад. + + + + usermod terminated with error code %1. + + + + + SetTimezoneJob + + + Set timezone to %1/%2 + + + + + Cannot access selected timezone path. + + + + + Bad path: %1 + + + + + Cannot set timezone. + + + + + Link creation failed, target: %1; link name: %2 + + + + + Cannot set timezone, + + + + + Cannot open /etc/timezone for writing + + + + + ShellProcessJob + + + Shell Processes Job + + + + + SlideCounter + + + %L1 / %L2 + slide counter, %1 of %2 (numeric) + + + + + SummaryPage + + + This is an overview of what will happen once you start the setup procedure. + + + + + This is an overview of what will happen once you start the install procedure. + + + + + SummaryViewStep + + + Summary + Ҷамъбаст + + + + TrackingInstallJob + + + Installation feedback + + + + + Sending installation feedback. + + + + + Internal error in install-tracking. + + + + + HTTP request timed out. + + + + + TrackingKUserFeedbackJob + + + KDE user feedback + Изҳори назари корбари KDE + + + + Configuring KDE user feedback. + Танзимкунии изҳори назари корбари KDE. + + + + + Error in KDE user feedback configuration. + + + + + Could not configure KDE user feedback correctly, script error %1. + + + + + Could not configure KDE user feedback correctly, Calamares error %1. + + + + + TrackingMachineUpdateManagerJob + + + Machine feedback + + + + + Configuring machine feedback. + + + + + + Error in machine feedback configuration. + + + + + Could not configure machine feedback correctly, script error %1. + + + + + Could not configure machine feedback correctly, Calamares error %1. + + + + + TrackingPage + + + Form + Шакл + + + + Placeholder + + + + + <html><head/><body><p>Click here to send <span style=" font-weight:600;">no information at all</span> about your installation.</p></body></html> + + + + + <html><head/><body><p><a href="placeholder"><span style=" text-decoration: underline; color:#2980b9;">Click here for more information about user feedback</span></a></p></body></html> + + + + + Tracking helps %1 to see how often it is installed, what hardware it is installed on and which applications are used. To see what will be sent, please click the help icon next to each area. + + + + + By selecting this you will send information about your installation and hardware. This information will only be sent <b>once</b> after the installation finishes. + + + + + By selecting this you will periodically send information about your <b>machine</b> installation, hardware and applications, to %1. + + + + + By selecting this you will regularly send information about your <b>user</b> installation, hardware, applications and application usage patterns, to %1. + + + + + TrackingViewStep + + + Feedback + Изҳори назар + + + + UsersPage + + + <small>If more than one person will use this computer, you can create multiple accounts after setup.</small> + + + + + <small>If more than one person will use this computer, you can create multiple accounts after installation.</small> + + + + + Your username is too long. + Номи корбари шумо хеле дароз аст. + + + + Your username must start with a lowercase letter or underscore. + + + + + Only lowercase letters, numbers, underscore and hyphen are allowed. + + + + + Your hostname is too short. + + + + + Your hostname is too long. + + + + + Only letters, numbers, underscore and hyphen are allowed. + + + + + Your passwords do not match! + + + + + UsersViewStep + + + Users + Корбарон + + + + VariantModel + + + Key + Тугма + + + + Value + Қимат + + + + VolumeGroupBaseDialog + + + Create Volume Group + Эҷод кардани гурӯҳи ҳаҷм + + + + List of Physical Volumes + + + + + Volume Group Name: + Номи гурӯҳи ҳаҷм: + + + + Volume Group Type: + Навъи гурӯҳи ҳаҷм: + + + + Physical Extent Size: + + + + + MiB + МБ + + + + Total Size: + Андозаи умумӣ: + + + + Used Size: + Андозаи истифодашуда: + + + + Total Sectors: + Бахшҳои умумӣ: + + + + Quantity of LVs: + Шумораи LV-ҳо: + + + + WelcomePage + + + Form + Шакл + + + + + Select application and system language + Интихоби забон барои низом ва барномаҳо + + + + &About + &Дар бораи барнома + + + + Open donations website + Сомонаи саҳмгузориро кушоед + + + + &Donate + &Саҳмгузорӣ + + + + Open help and support website + Сомонаи кумак ва дастгириро кушоед + + + + &Support + &Дастгирӣ + + + + Open issues and bug-tracking website + Сомонаи масъалаҳо ва пайгирии нуқсонҳоро кушоед + + + + &Known issues + &Масъалаҳои маълум + + + + Open release notes website + Сомонаро бо қайдҳои нашр кушоед + + + + &Release notes + &Қайдҳои нашр + + + + <h1>Welcome to the Calamares setup program for %1.</h1> + <h1>Хуш омадед ба барномаи танзимкунии Calamares барои %1.</h1> + + + + <h1>Welcome to %1 setup.</h1> + <h1>Хуш омадед ба танзимкунии %1.</h1> + + + + <h1>Welcome to the Calamares installer for %1.</h1> + <h1>Хуш омадед ба насбкунандаи Calamares барои %1.</h1> + + + + <h1>Welcome to the %1 installer.</h1> + <h1>Хуш омадед ба насбкунандаи %1.</h1> + + + + %1 support + Дастгирии %1 + + + + About %1 setup + Дар бораи танзими %1 + + + + About %1 installer + Дар бораи насбкунандаи %1 + + + + <h1>%1</h1><br/><strong>%2<br/>for %3</strong><br/><br/>Copyright 2014-2017 Teo Mrnjavac &lt;teo@kde.org&gt;<br/>Copyright 2017-2020 Adriaan de Groot &lt;groot@kde.org&gt;<br/>Thanks to <a href="https://calamares.io/team/">the Calamares team</a> and the <a href="https://www.transifex.com/calamares/calamares/">Calamares translators team</a>.<br/><br/><a href="https://calamares.io/">Calamares</a> development is sponsored by <br/><a href="http://www.blue-systems.com/">Blue Systems</a> - Liberating Software. + + + + + WelcomeQmlViewStep + + + Welcome + Хуш омадед + + + + WelcomeViewStep + + + Welcome + Хуш омадед + + + + about + + + <h1>%1</h1><br/> + <strong>%2<br/> + for %3</strong><br/><br/> + Copyright 2014-2017 Teo Mrnjavac &lt;teo@kde.org&gt;<br/> + Copyright 2017-2020 Adriaan de Groot &lt;groot@kde.org&gt;<br/> + Thanks to <a href='https://calamares.io/team/'>the Calamares team</a> + and the <a href='https://www.transifex.com/calamares/calamares/'>Calamares + translators team</a>.<br/><br/> + <a href='https://calamares.io/'>Calamares</a> + development is sponsored by <br/> + <a href='http://www.blue-systems.com/'>Blue Systems</a> - + Liberating Software. + + + + + Back + Ба қафо + + + + i18n + + + <h1>Languages</h1> </br> + The system locale setting affects the language and character set for some command line user interface elements. The current setting is <strong>%1</strong>. + <h1>Забонҳо</h1> </br> + Танзими маҳаллигардонии низом ба забон ва маҷмӯаи аломатҳо барои баъзеи унсурҳои интерфейси корбарӣ дар сатри фармондиҳӣ таъсир мерасонад. Танзими ҷорӣ: <strong>%1</strong>. + + + + <h1>Locales</h1> </br> + The system locale setting affects the language and character set for some command line user interface elements. The current setting is <strong>%1</strong>. + <h1>Маҳаллигардонӣ</h1> </br> + Танзими маҳаллигардонии низом ба забон ва маҷмӯаи аломатҳо барои баъзеи унсурҳои интерфейси корбарӣ дар сатри фармондиҳӣ таъсир мерасонад. Танзими ҷорӣ: <strong>%1</strong>. + + + + Back + Ба қафо + + + + keyboardq + + + Keyboard Model + Намунаи клавиатура + + + + Pick your preferred keyboard model or use the default one based on the detected hardware + + + + + Refresh + Навсозӣ кардан + + + + + Layouts + Тарҳбандиҳо + + + + + Keyboard Layout + Тарҳбандии клавиатура + + + + Models + Намунаҳо + + + + Variants + Имконот + + + + Test your keyboard + Клавиатураи худро санҷед + + + + localeq + + + System language set to %1 + Забони низом ба %1 танзим шуд + + + + Numbers and dates locale set to %1 + Низоми рақамҳо ва санаҳо ба %1 танзим шуд + + + + Change + Тағйир додан + + + + notesqml + + + <h3>%1</h3> + <p>These are example release notes.</p> + <h3>%1</h3> + <p>Матни намунавии қайдҳои нашр.</p> + + + + release_notes + + + <h3>%1</h3> + <p>This an example QML file, showing options in RichText with Flickable content.</p> + + <p>QML with RichText can use HTML tags, Flickable content is useful for touchscreens.</p> + + <p><b>This is bold text</b></p> + <p><i>This is italic text</i></p> + <p><u>This is underlined text</u></p> + <p><center>This text will be center-aligned.</center></p> + <p><s>This is strikethrough</s></p> + + <p>Code example: + <code>ls -l /home</code></p> + + <p><b>Lists:</b></p> + <ul> + <li>Intel CPU systems</li> + <li>AMD CPU systems</li> + </ul> + + <p>The vertical scrollbar is adjustable, current width set to 10.</p> + + + + + Back + Ба қафо + + + + welcomeq + + + <h3>Welcome to the %1 <quote>%2</quote> installer</h3> + <p>This program will ask you some questions and set up %1 on your computer.</p> + <h3>Хуш омадед ба насбкунандаи <quote>%2</quote> барои %1</h3> + <p>Ин барнома аз Шумо якчанд савол мепурсад ва %1-ро дар компютери шумо танзим мекунад.</p> + + + + About + Дар бораи барнома + + + + Support + Дастгирӣ + + + + Known issues + Масъалаҳои маълум + + + + Release notes + Қайдҳои нашр + + + + Donate + Саҳмгузорӣ + + + From d5d2d2a1f10bce859e025e2dc520b4b7dea5af72 Mon Sep 17 00:00:00 2001 From: Calamares CI Date: Sun, 26 Jul 2020 11:02:36 +0200 Subject: [PATCH 065/113] i18n: [desktop] Automatic merge of Transifex translations --- calamares.desktop | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/calamares.desktop b/calamares.desktop index 78c943ddb..a70f377cf 100644 --- a/calamares.desktop +++ b/calamares.desktop @@ -94,6 +94,10 @@ Name[hr]=Instaliraj sustav Icon[hr]=calamares GenericName[hr]=Instalacija sustava Comment[hr]=Calamares — Instalacija sustava +Name[ie]=Installar li sistema +Icon[ie]=calamares +GenericName[ie]=Installator del sistema +Comment[ie]=Calamares — Installator del sistema Name[hu]=Rendszer telepítése Icon[hu]=calamares GenericName[hu]=Rendszertelepítő @@ -184,6 +188,10 @@ Name[sv]=Installera system Icon[sv]=calamares GenericName[sv]=Systeminstallerare Comment[sv]=Calamares — Systeminstallerare +Name[tg]=Насбкунии низом +Icon[tg]=calamares +GenericName[tg]=Насбкунандаи низом +Comment[tg]=Calamares — Насбкунандаи низом Name[th]=ติดตั้งระบบ Name[uk]=Встановити Систему Icon[uk]=calamares From d0cdc8169e6bc5de34039d68e3fdc49e32968673 Mon Sep 17 00:00:00 2001 From: Calamares CI Date: Sun, 26 Jul 2020 11:02:37 +0200 Subject: [PATCH 066/113] i18n: [python] Automatic merge of Transifex translations --- lang/python/ie/LC_MESSAGES/python.mo | Bin 0 -> 384 bytes lang/python/ie/LC_MESSAGES/python.po | 339 ++++++++++++++++++++++++++ lang/python/tg/LC_MESSAGES/python.mo | Bin 0 -> 661 bytes lang/python/tg/LC_MESSAGES/python.po | 343 +++++++++++++++++++++++++++ 4 files changed, 682 insertions(+) create mode 100644 lang/python/ie/LC_MESSAGES/python.mo create mode 100644 lang/python/ie/LC_MESSAGES/python.po create mode 100644 lang/python/tg/LC_MESSAGES/python.mo create mode 100644 lang/python/tg/LC_MESSAGES/python.po diff --git a/lang/python/ie/LC_MESSAGES/python.mo b/lang/python/ie/LC_MESSAGES/python.mo new file mode 100644 index 0000000000000000000000000000000000000000..1f1e0d4d55d5df0ea52fe62ec7f76d4c7780189f GIT binary patch literal 384 zcmYL@!A=4(5QZ^&+M{O=HSqw^(k>cZO7?)bn2jPET)Ed}SZa3LOdh*>u9U*7P1#*mBAv;CL7>9kVJI`L3H6KuTnl9)ZtW!n{k_|^s!^eco zli6!JeFW1G#U>#fvIEn(X&Ow9^e$y!=)%;TD4JQQ-d`zQ*Z}-&_EKPJ_7MMkl=w11J kR6!I3S+%yZqGMxgCx~wTjxO#E$bTERH93z-*ck+^Uy##m^Z)<= literal 0 HcmV?d00001 diff --git a/lang/python/ie/LC_MESSAGES/python.po b/lang/python/ie/LC_MESSAGES/python.po new file mode 100644 index 000000000..115a704d7 --- /dev/null +++ b/lang/python/ie/LC_MESSAGES/python.po @@ -0,0 +1,339 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-06-18 15:42+0200\n" +"PO-Revision-Date: 2017-08-09 10:34+0000\n" +"Language-Team: Interlingue (https://www.transifex.com/calamares/teams/20061/ie/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ie\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: src/modules/grubcfg/main.py:37 +msgid "Configure GRUB." +msgstr "" + +#: src/modules/mount/main.py:38 +msgid "Mounting partitions." +msgstr "" + +#: src/modules/mount/main.py:150 src/modules/initcpiocfg/main.py:205 +#: src/modules/initcpiocfg/main.py:209 +#: src/modules/luksopenswaphookcfg/main.py:95 +#: src/modules/luksopenswaphookcfg/main.py:99 src/modules/rawfs/main.py:173 +#: src/modules/initramfscfg/main.py:94 src/modules/initramfscfg/main.py:98 +#: src/modules/openrcdmcryptcfg/main.py:78 +#: src/modules/openrcdmcryptcfg/main.py:82 src/modules/fstab/main.py:332 +#: src/modules/fstab/main.py:338 src/modules/localecfg/main.py:144 +#: src/modules/networkcfg/main.py:48 +msgid "Configuration Error" +msgstr "" + +#: src/modules/mount/main.py:151 src/modules/initcpiocfg/main.py:206 +#: src/modules/luksopenswaphookcfg/main.py:96 src/modules/rawfs/main.py:174 +#: src/modules/initramfscfg/main.py:95 src/modules/openrcdmcryptcfg/main.py:79 +#: src/modules/fstab/main.py:333 +msgid "No partitions are defined for
{!s}
to use." +msgstr "" + +#: src/modules/services-systemd/main.py:35 +msgid "Configure systemd services" +msgstr "" + +#: src/modules/services-systemd/main.py:68 +#: src/modules/services-openrc/main.py:102 +msgid "Cannot modify service" +msgstr "" + +#: src/modules/services-systemd/main.py:69 +msgid "" +"systemctl {arg!s} call in chroot returned error code {num!s}." +msgstr "" + +#: src/modules/services-systemd/main.py:72 +#: src/modules/services-systemd/main.py:76 +msgid "Cannot enable systemd service {name!s}." +msgstr "" + +#: src/modules/services-systemd/main.py:74 +msgid "Cannot enable systemd target {name!s}." +msgstr "" + +#: src/modules/services-systemd/main.py:78 +msgid "Cannot disable systemd target {name!s}." +msgstr "" + +#: src/modules/services-systemd/main.py:80 +msgid "Cannot mask systemd unit {name!s}." +msgstr "" + +#: src/modules/services-systemd/main.py:82 +msgid "" +"Unknown systemd commands {command!s} and " +"{suffix!s} for unit {name!s}." +msgstr "" + +#: src/modules/umount/main.py:40 +msgid "Unmount file systems." +msgstr "" + +#: src/modules/unpackfs/main.py:44 +msgid "Filling up filesystems." +msgstr "" + +#: src/modules/unpackfs/main.py:257 +msgid "rsync failed with error code {}." +msgstr "" + +#: src/modules/unpackfs/main.py:302 +msgid "Unpacking image {}/{}, file {}/{}" +msgstr "" + +#: src/modules/unpackfs/main.py:317 +msgid "Starting to unpack {}" +msgstr "" + +#: src/modules/unpackfs/main.py:326 src/modules/unpackfs/main.py:448 +msgid "Failed to unpack image \"{}\"" +msgstr "" + +#: src/modules/unpackfs/main.py:415 +msgid "No mount point for root partition" +msgstr "" + +#: src/modules/unpackfs/main.py:416 +msgid "globalstorage does not contain a \"rootMountPoint\" key, doing nothing" +msgstr "" + +#: src/modules/unpackfs/main.py:421 +msgid "Bad mount point for root partition" +msgstr "" + +#: src/modules/unpackfs/main.py:422 +msgid "rootMountPoint is \"{}\", which does not exist, doing nothing" +msgstr "" + +#: src/modules/unpackfs/main.py:438 src/modules/unpackfs/main.py:442 +#: src/modules/unpackfs/main.py:462 +msgid "Bad unsquash configuration" +msgstr "" + +#: src/modules/unpackfs/main.py:439 +msgid "The filesystem for \"{}\" ({}) is not supported by your current kernel" +msgstr "" + +#: src/modules/unpackfs/main.py:443 +msgid "The source filesystem \"{}\" does not exist" +msgstr "" + +#: src/modules/unpackfs/main.py:449 +msgid "" +"Failed to find unsquashfs, make sure you have the squashfs-tools package " +"installed" +msgstr "" + +#: src/modules/unpackfs/main.py:463 +msgid "The destination \"{}\" in the target system is not a directory" +msgstr "" + +#: src/modules/displaymanager/main.py:523 +msgid "Cannot write KDM configuration file" +msgstr "" + +#: src/modules/displaymanager/main.py:524 +msgid "KDM config file {!s} does not exist" +msgstr "" + +#: src/modules/displaymanager/main.py:585 +msgid "Cannot write LXDM configuration file" +msgstr "" + +#: src/modules/displaymanager/main.py:586 +msgid "LXDM config file {!s} does not exist" +msgstr "" + +#: src/modules/displaymanager/main.py:669 +msgid "Cannot write LightDM configuration file" +msgstr "" + +#: src/modules/displaymanager/main.py:670 +msgid "LightDM config file {!s} does not exist" +msgstr "" + +#: src/modules/displaymanager/main.py:744 +msgid "Cannot configure LightDM" +msgstr "" + +#: src/modules/displaymanager/main.py:745 +msgid "No LightDM greeter installed." +msgstr "" + +#: src/modules/displaymanager/main.py:776 +msgid "Cannot write SLIM configuration file" +msgstr "" + +#: src/modules/displaymanager/main.py:777 +msgid "SLIM config file {!s} does not exist" +msgstr "" + +#: src/modules/displaymanager/main.py:903 +msgid "No display managers selected for the displaymanager module." +msgstr "" + +#: src/modules/displaymanager/main.py:904 +msgid "" +"The displaymanagers list is empty or undefined in bothglobalstorage and " +"displaymanager.conf." +msgstr "" + +#: src/modules/displaymanager/main.py:986 +msgid "Display manager configuration was incomplete" +msgstr "" + +#: src/modules/initcpiocfg/main.py:37 +msgid "Configuring mkinitcpio." +msgstr "" + +#: src/modules/initcpiocfg/main.py:210 +#: src/modules/luksopenswaphookcfg/main.py:100 +#: src/modules/initramfscfg/main.py:99 src/modules/openrcdmcryptcfg/main.py:83 +#: src/modules/fstab/main.py:339 src/modules/localecfg/main.py:145 +#: src/modules/networkcfg/main.py:49 +msgid "No root mount point is given for
{!s}
to use." +msgstr "" + +#: src/modules/luksopenswaphookcfg/main.py:35 +msgid "Configuring encrypted swap." +msgstr "" + +#: src/modules/rawfs/main.py:35 +msgid "Installing data." +msgstr "" + +#: src/modules/services-openrc/main.py:38 +msgid "Configure OpenRC services" +msgstr "" + +#: src/modules/services-openrc/main.py:66 +msgid "Cannot add service {name!s} to run-level {level!s}." +msgstr "" + +#: src/modules/services-openrc/main.py:68 +msgid "Cannot remove service {name!s} from run-level {level!s}." +msgstr "" + +#: src/modules/services-openrc/main.py:70 +msgid "" +"Unknown service-action {arg!s} for service {name!s} in run-" +"level {level!s}." +msgstr "" + +#: src/modules/services-openrc/main.py:103 +msgid "" +"rc-update {arg!s} call in chroot returned error code {num!s}." +msgstr "" + +#: src/modules/services-openrc/main.py:110 +msgid "Target runlevel does not exist" +msgstr "" + +#: src/modules/services-openrc/main.py:111 +msgid "" +"The path for runlevel {level!s} is {path!s}, which does not " +"exist." +msgstr "" + +#: src/modules/services-openrc/main.py:119 +msgid "Target service does not exist" +msgstr "" + +#: src/modules/services-openrc/main.py:120 +msgid "" +"The path for service {name!s} is {path!s}, which does not " +"exist." +msgstr "" + +#: src/modules/plymouthcfg/main.py:36 +msgid "Configure Plymouth theme" +msgstr "" + +#: src/modules/packages/main.py:59 src/modules/packages/main.py:68 +#: src/modules/packages/main.py:78 +msgid "Install packages." +msgstr "" + +#: src/modules/packages/main.py:66 +#, python-format +msgid "Processing packages (%(count)d / %(total)d)" +msgstr "" + +#: src/modules/packages/main.py:71 +#, python-format +msgid "Installing one package." +msgid_plural "Installing %(num)d packages." +msgstr[0] "" +msgstr[1] "" + +#: src/modules/packages/main.py:74 +#, python-format +msgid "Removing one package." +msgid_plural "Removing %(num)d packages." +msgstr[0] "" +msgstr[1] "" + +#: src/modules/bootloader/main.py:51 +msgid "Install bootloader." +msgstr "" + +#: src/modules/hwclock/main.py:35 +msgid "Setting hardware clock." +msgstr "" + +#: src/modules/dracut/main.py:36 +msgid "Creating initramfs with dracut." +msgstr "" + +#: src/modules/dracut/main.py:58 +msgid "Failed to run dracut on the target" +msgstr "" + +#: src/modules/dracut/main.py:59 +msgid "The exit code was {}" +msgstr "" + +#: src/modules/initramfscfg/main.py:41 +msgid "Configuring initramfs." +msgstr "" + +#: src/modules/openrcdmcryptcfg/main.py:34 +msgid "Configuring OpenRC dmcrypt service." +msgstr "" + +#: src/modules/fstab/main.py:38 +msgid "Writing fstab." +msgstr "" + +#: src/modules/dummypython/main.py:44 +msgid "Dummy python job." +msgstr "" + +#: src/modules/dummypython/main.py:46 src/modules/dummypython/main.py:102 +#: src/modules/dummypython/main.py:103 +msgid "Dummy python step {}" +msgstr "" + +#: src/modules/localecfg/main.py:39 +msgid "Configuring locales." +msgstr "" + +#: src/modules/networkcfg/main.py:37 +msgid "Saving network configuration." +msgstr "" diff --git a/lang/python/tg/LC_MESSAGES/python.mo b/lang/python/tg/LC_MESSAGES/python.mo new file mode 100644 index 0000000000000000000000000000000000000000..1b88d83072b32a1b3a75791b2f5df45ad88b6f02 GIT binary patch literal 661 zcmYk3O>fgc5QY~h7hjNo0|z9ATdT6R6A+|MQ}u(iiXb!!X--_W&c@wh@0#6BQjUQ7 z2aq^%199P02_&M@_BU+Ho!`PZp_Gvxy`J&BvtPfj&wL;lGstaZ5xI)IK#Itb8^|l< z8nTbvL<-aXB|^TU-a*34gxo@Hq0XZURQye@AjND|$CfsFw9B=@Vg#*~wI?eZb?DaH&l{W1yf)93wzO#y z(W9$_#L$k4z-w*NHXn+@LmxBCBPfTZM>MR^Z~;nTbpOG8h~YhD#?p>v(xl8P9YI&b zsLt7$TDaOE~RtmPOEEyW%Cx?pT|fC%F3zGs6x zWkOaV-eKBsTifca)5=9(u~eUHT9>iv;gv>E*%tQSCUHjMwNiYJl&&kCnFwS)NlmR> zh3RK)R>IvHl;)})abMiQ>05W=#(;9*j@=J8c1P~;^qo8Y^_jR&{}oTLW&HO|-Fq~? baX+V>=o!P=M>odO(b>1jJokN)9{S`DZAjl9 literal 0 HcmV?d00001 diff --git a/lang/python/tg/LC_MESSAGES/python.po b/lang/python/tg/LC_MESSAGES/python.po new file mode 100644 index 000000000..a5ff5ce5e --- /dev/null +++ b/lang/python/tg/LC_MESSAGES/python.po @@ -0,0 +1,343 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +# Translators: +# Victor Ibragimov , 2020 +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-06-18 15:42+0200\n" +"PO-Revision-Date: 2017-08-09 10:34+0000\n" +"Last-Translator: Victor Ibragimov , 2020\n" +"Language-Team: Tajik (https://www.transifex.com/calamares/teams/20061/tg/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: tg\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: src/modules/grubcfg/main.py:37 +msgid "Configure GRUB." +msgstr "Танзимоти GRUB." + +#: src/modules/mount/main.py:38 +msgid "Mounting partitions." +msgstr "Васлкунии қисмҳои диск." + +#: src/modules/mount/main.py:150 src/modules/initcpiocfg/main.py:205 +#: src/modules/initcpiocfg/main.py:209 +#: src/modules/luksopenswaphookcfg/main.py:95 +#: src/modules/luksopenswaphookcfg/main.py:99 src/modules/rawfs/main.py:173 +#: src/modules/initramfscfg/main.py:94 src/modules/initramfscfg/main.py:98 +#: src/modules/openrcdmcryptcfg/main.py:78 +#: src/modules/openrcdmcryptcfg/main.py:82 src/modules/fstab/main.py:332 +#: src/modules/fstab/main.py:338 src/modules/localecfg/main.py:144 +#: src/modules/networkcfg/main.py:48 +msgid "Configuration Error" +msgstr "Хатои танзимкунӣ" + +#: src/modules/mount/main.py:151 src/modules/initcpiocfg/main.py:206 +#: src/modules/luksopenswaphookcfg/main.py:96 src/modules/rawfs/main.py:174 +#: src/modules/initramfscfg/main.py:95 src/modules/openrcdmcryptcfg/main.py:79 +#: src/modules/fstab/main.py:333 +msgid "No partitions are defined for
{!s}
to use." +msgstr "" + +#: src/modules/services-systemd/main.py:35 +msgid "Configure systemd services" +msgstr "" + +#: src/modules/services-systemd/main.py:68 +#: src/modules/services-openrc/main.py:102 +msgid "Cannot modify service" +msgstr "" + +#: src/modules/services-systemd/main.py:69 +msgid "" +"systemctl {arg!s} call in chroot returned error code {num!s}." +msgstr "" + +#: src/modules/services-systemd/main.py:72 +#: src/modules/services-systemd/main.py:76 +msgid "Cannot enable systemd service {name!s}." +msgstr "" + +#: src/modules/services-systemd/main.py:74 +msgid "Cannot enable systemd target {name!s}." +msgstr "" + +#: src/modules/services-systemd/main.py:78 +msgid "Cannot disable systemd target {name!s}." +msgstr "" + +#: src/modules/services-systemd/main.py:80 +msgid "Cannot mask systemd unit {name!s}." +msgstr "" + +#: src/modules/services-systemd/main.py:82 +msgid "" +"Unknown systemd commands {command!s} and " +"{suffix!s} for unit {name!s}." +msgstr "" + +#: src/modules/umount/main.py:40 +msgid "Unmount file systems." +msgstr "" + +#: src/modules/unpackfs/main.py:44 +msgid "Filling up filesystems." +msgstr "" + +#: src/modules/unpackfs/main.py:257 +msgid "rsync failed with error code {}." +msgstr "" + +#: src/modules/unpackfs/main.py:302 +msgid "Unpacking image {}/{}, file {}/{}" +msgstr "" + +#: src/modules/unpackfs/main.py:317 +msgid "Starting to unpack {}" +msgstr "" + +#: src/modules/unpackfs/main.py:326 src/modules/unpackfs/main.py:448 +msgid "Failed to unpack image \"{}\"" +msgstr "" + +#: src/modules/unpackfs/main.py:415 +msgid "No mount point for root partition" +msgstr "" + +#: src/modules/unpackfs/main.py:416 +msgid "globalstorage does not contain a \"rootMountPoint\" key, doing nothing" +msgstr "" + +#: src/modules/unpackfs/main.py:421 +msgid "Bad mount point for root partition" +msgstr "" + +#: src/modules/unpackfs/main.py:422 +msgid "rootMountPoint is \"{}\", which does not exist, doing nothing" +msgstr "" + +#: src/modules/unpackfs/main.py:438 src/modules/unpackfs/main.py:442 +#: src/modules/unpackfs/main.py:462 +msgid "Bad unsquash configuration" +msgstr "" + +#: src/modules/unpackfs/main.py:439 +msgid "The filesystem for \"{}\" ({}) is not supported by your current kernel" +msgstr "" + +#: src/modules/unpackfs/main.py:443 +msgid "The source filesystem \"{}\" does not exist" +msgstr "" + +#: src/modules/unpackfs/main.py:449 +msgid "" +"Failed to find unsquashfs, make sure you have the squashfs-tools package " +"installed" +msgstr "" + +#: src/modules/unpackfs/main.py:463 +msgid "The destination \"{}\" in the target system is not a directory" +msgstr "" + +#: src/modules/displaymanager/main.py:523 +msgid "Cannot write KDM configuration file" +msgstr "" + +#: src/modules/displaymanager/main.py:524 +msgid "KDM config file {!s} does not exist" +msgstr "" + +#: src/modules/displaymanager/main.py:585 +msgid "Cannot write LXDM configuration file" +msgstr "" + +#: src/modules/displaymanager/main.py:586 +msgid "LXDM config file {!s} does not exist" +msgstr "" + +#: src/modules/displaymanager/main.py:669 +msgid "Cannot write LightDM configuration file" +msgstr "" + +#: src/modules/displaymanager/main.py:670 +msgid "LightDM config file {!s} does not exist" +msgstr "" + +#: src/modules/displaymanager/main.py:744 +msgid "Cannot configure LightDM" +msgstr "" + +#: src/modules/displaymanager/main.py:745 +msgid "No LightDM greeter installed." +msgstr "" + +#: src/modules/displaymanager/main.py:776 +msgid "Cannot write SLIM configuration file" +msgstr "" + +#: src/modules/displaymanager/main.py:777 +msgid "SLIM config file {!s} does not exist" +msgstr "" + +#: src/modules/displaymanager/main.py:903 +msgid "No display managers selected for the displaymanager module." +msgstr "" + +#: src/modules/displaymanager/main.py:904 +msgid "" +"The displaymanagers list is empty or undefined in bothglobalstorage and " +"displaymanager.conf." +msgstr "" + +#: src/modules/displaymanager/main.py:986 +msgid "Display manager configuration was incomplete" +msgstr "" + +#: src/modules/initcpiocfg/main.py:37 +msgid "Configuring mkinitcpio." +msgstr "" + +#: src/modules/initcpiocfg/main.py:210 +#: src/modules/luksopenswaphookcfg/main.py:100 +#: src/modules/initramfscfg/main.py:99 src/modules/openrcdmcryptcfg/main.py:83 +#: src/modules/fstab/main.py:339 src/modules/localecfg/main.py:145 +#: src/modules/networkcfg/main.py:49 +msgid "No root mount point is given for
{!s}
to use." +msgstr "" + +#: src/modules/luksopenswaphookcfg/main.py:35 +msgid "Configuring encrypted swap." +msgstr "" + +#: src/modules/rawfs/main.py:35 +msgid "Installing data." +msgstr "" + +#: src/modules/services-openrc/main.py:38 +msgid "Configure OpenRC services" +msgstr "" + +#: src/modules/services-openrc/main.py:66 +msgid "Cannot add service {name!s} to run-level {level!s}." +msgstr "" + +#: src/modules/services-openrc/main.py:68 +msgid "Cannot remove service {name!s} from run-level {level!s}." +msgstr "" + +#: src/modules/services-openrc/main.py:70 +msgid "" +"Unknown service-action {arg!s} for service {name!s} in run-" +"level {level!s}." +msgstr "" + +#: src/modules/services-openrc/main.py:103 +msgid "" +"rc-update {arg!s} call in chroot returned error code {num!s}." +msgstr "" + +#: src/modules/services-openrc/main.py:110 +msgid "Target runlevel does not exist" +msgstr "" + +#: src/modules/services-openrc/main.py:111 +msgid "" +"The path for runlevel {level!s} is {path!s}, which does not " +"exist." +msgstr "" + +#: src/modules/services-openrc/main.py:119 +msgid "Target service does not exist" +msgstr "" + +#: src/modules/services-openrc/main.py:120 +msgid "" +"The path for service {name!s} is {path!s}, which does not " +"exist." +msgstr "" + +#: src/modules/plymouthcfg/main.py:36 +msgid "Configure Plymouth theme" +msgstr "" + +#: src/modules/packages/main.py:59 src/modules/packages/main.py:68 +#: src/modules/packages/main.py:78 +msgid "Install packages." +msgstr "" + +#: src/modules/packages/main.py:66 +#, python-format +msgid "Processing packages (%(count)d / %(total)d)" +msgstr "" + +#: src/modules/packages/main.py:71 +#, python-format +msgid "Installing one package." +msgid_plural "Installing %(num)d packages." +msgstr[0] "" +msgstr[1] "" + +#: src/modules/packages/main.py:74 +#, python-format +msgid "Removing one package." +msgid_plural "Removing %(num)d packages." +msgstr[0] "" +msgstr[1] "" + +#: src/modules/bootloader/main.py:51 +msgid "Install bootloader." +msgstr "" + +#: src/modules/hwclock/main.py:35 +msgid "Setting hardware clock." +msgstr "" + +#: src/modules/dracut/main.py:36 +msgid "Creating initramfs with dracut." +msgstr "" + +#: src/modules/dracut/main.py:58 +msgid "Failed to run dracut on the target" +msgstr "" + +#: src/modules/dracut/main.py:59 +msgid "The exit code was {}" +msgstr "" + +#: src/modules/initramfscfg/main.py:41 +msgid "Configuring initramfs." +msgstr "" + +#: src/modules/openrcdmcryptcfg/main.py:34 +msgid "Configuring OpenRC dmcrypt service." +msgstr "" + +#: src/modules/fstab/main.py:38 +msgid "Writing fstab." +msgstr "" + +#: src/modules/dummypython/main.py:44 +msgid "Dummy python job." +msgstr "" + +#: src/modules/dummypython/main.py:46 src/modules/dummypython/main.py:102 +#: src/modules/dummypython/main.py:103 +msgid "Dummy python step {}" +msgstr "" + +#: src/modules/localecfg/main.py:39 +msgid "Configuring locales." +msgstr "" + +#: src/modules/networkcfg/main.py:37 +msgid "Saving network configuration." +msgstr "" From d22f392609954dcbe5a3624dbada580a28016401 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Sun, 26 Jul 2020 11:09:45 +0200 Subject: [PATCH 067/113] CMake: update language lists - welcome Tajik - welcome Interlingue --- CMakeLists.txt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fb3ccf699..4b6f03fea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -147,14 +147,15 @@ set( CALAMARES_DESCRIPTION_SUMMARY # copy these four lines to four backup lines, add "p", and then update # the original four lines with the current translations). # -# Total 66 languages -set( _tx_complete az az_AZ ca he hi hr ja sq tr_TR uk zh_TW ) -set( _tx_good as ast be cs_CZ da de es fi_FI fr hu it_IT ko lt ml - nl pt_BR pt_PT ru sk sv zh_CN ) +# Total 68 languages +set( _tx_complete az az_AZ ca da fi_FI he hi hr ja pt_BR sq tr_TR + uk zh_TW ) +set( _tx_good as ast be cs_CZ de es fr hu it_IT ko lt ml nl pt_PT + ru sk sv zh_CN ) set( _tx_ok ar bg el en_GB es_MX es_PR et eu fa gl id is mr nb pl - ro sl sr sr@latin th ) -set( _tx_incomplete bn ca@valencia eo fr_CH gu kk kn lo lv mk ne_NP - ur uz ) + ro sl sr sr@latin tg th ) +set( _tx_incomplete bn ca@valencia eo fr_CH gu ie kk kn lo lv mk + ne_NP ur uz ) ### Required versions # From 5e35bcc8302512e988fa9f62c76650378fe830c3 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 27 Jul 2020 10:42:45 +0200 Subject: [PATCH 068/113] Changes: document new features, translations --- CHANGES | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index ad374e40a..27501358f 100644 --- a/CHANGES +++ b/CHANGES @@ -6,11 +6,16 @@ website will have to do for older versions. # 3.2.28 (unreleased) # This release contains contributions from (alphabetically by first name): - - No external contributors yet + - Anke Boersma + - apt-ghetto ## Core ## - A new object *Network* is available to QML modules in `io.calamares.core`. It exposes network status through the *hasInternet* property. + - Welcome to Tajik translations. This has reached sufficient completion + to be included in the drop-down list of languages on the welcome page. + - Welcome to [Interlingue](https://en.wikipedia.org/wiki/Interlingue). + The translation is at an early stage. ## Modules ## - The *locale* module has been completely redone on the inside. @@ -21,7 +26,11 @@ This release contains contributions from (alphabetically by first name): the work of figuring out maps. Note that the map uses several GeoIP and GeoData providers and you may need to configure the URLs with suitable usernames for those services. #1426 - + - Both *locale* and *localeq* can now be configured to use the system's + timezone setting -- this can be useful to avoid both hard-coding an + initial zone and doing extra GeoIP lookups, in the case where the + live system already does so. #1391 + - The *users* module no longer accepts `root` as a username. #1462 # 3.2.27 (2020-07-11) # From 1babcd2aa486ead1127c21fff15abee4c48a2c33 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 27 Jul 2020 10:57:15 +0200 Subject: [PATCH 069/113] [libcalamares] Put Permissions in CalamaresUtils namespace - most of the things in utils/ are in the CalamaresUtils namespace, let Permissions follow suit. Chase the name change in the *preservefiles* module. - add an `apply()` function for doing the most basic of chmod. Note that we don't use `QFile::setPermissions()` because the **values** used are different (0755 for chmod is 0x755 in the enum value passed to `setPermissions()`). --- src/libcalamares/utils/Permissions.cpp | 14 ++++++++++++++ src/libcalamares/utils/Permissions.h | 8 ++++++++ src/modules/preservefiles/PreserveFiles.cpp | 4 ++-- src/modules/preservefiles/PreserveFiles.h | 2 +- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/libcalamares/utils/Permissions.cpp b/src/libcalamares/utils/Permissions.cpp index 3166d840a..3f5350e86 100644 --- a/src/libcalamares/utils/Permissions.cpp +++ b/src/libcalamares/utils/Permissions.cpp @@ -8,9 +8,14 @@ #include "Permissions.h" +#include + #include #include +namespace CalamaresUtils +{ + Permissions::Permissions() : m_username() , m_group() @@ -64,3 +69,12 @@ Permissions::parsePermissions( QString const& p ) return; } + +bool +Permissions::apply( const QString& path, int mode ) +{ + int r = chmod( path.toUtf8().constData(), mode ); + return r == 0; +} + +} // namespace CalamaresUtils diff --git a/src/libcalamares/utils/Permissions.h b/src/libcalamares/utils/Permissions.h index b6e2d3a44..913d037d5 100644 --- a/src/libcalamares/utils/Permissions.h +++ b/src/libcalamares/utils/Permissions.h @@ -13,6 +13,9 @@ #include +namespace CalamaresUtils +{ + /** * @brief The Permissions class takes a QString @p in the form of * ::, checks it for validity, and makes the three @@ -56,6 +59,9 @@ public: */ QString octal() const { return QString::number( value(), 8 ); } + /// chmod(path, mode), returns true on success + static bool apply( const QString& path, int mode ); + private: void parsePermissions( QString const& p ); @@ -65,4 +71,6 @@ private: bool m_valid; }; +} // namespace CalamaresUtils + #endif // LIBCALAMARES_PERMISSIONS_H diff --git a/src/modules/preservefiles/PreserveFiles.cpp b/src/modules/preservefiles/PreserveFiles.cpp index 3e34024e7..57c2c5422 100644 --- a/src/modules/preservefiles/PreserveFiles.cpp +++ b/src/modules/preservefiles/PreserveFiles.cpp @@ -220,7 +220,7 @@ PreserveFiles::setConfigurationMap( const QVariantMap& configurationMap ) { QString filename = li.toString(); if ( !filename.isEmpty() ) - m_items.append( Item { filename, filename, Permissions( defaultPermissions ), ItemType::Path } ); + m_items.append( Item { filename, filename, CalamaresUtils::Permissions( defaultPermissions ), ItemType::Path } ); else { cDebug() << "Empty filename for preservefiles, item" << c; @@ -248,7 +248,7 @@ PreserveFiles::setConfigurationMap( const QVariantMap& configurationMap ) } else { - m_items.append( Item { QString(), dest, Permissions( perm ), t } ); + m_items.append( Item { QString(), dest, CalamaresUtils::Permissions( perm ), t } ); } } else diff --git a/src/modules/preservefiles/PreserveFiles.h b/src/modules/preservefiles/PreserveFiles.h index 214ff0df8..fdba933fa 100644 --- a/src/modules/preservefiles/PreserveFiles.h +++ b/src/modules/preservefiles/PreserveFiles.h @@ -34,7 +34,7 @@ class PLUGINDLLEXPORT PreserveFiles : public Calamares::CppJob { QString source; QString dest; - Permissions perm; + CalamaresUtils::Permissions perm; ItemType type; }; From 389e36303f5027cf2ca4d9a6e56c15265c4f0325 Mon Sep 17 00:00:00 2001 From: demmm Date: Mon, 27 Jul 2020 11:17:00 +0200 Subject: [PATCH 070/113] Changes: document keyboardq changes --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 27501358f..3d930c30d 100644 --- a/CHANGES +++ b/CHANGES @@ -31,6 +31,8 @@ This release contains contributions from (alphabetically by first name): initial zone and doing extra GeoIP lookups, in the case where the live system already does so. #1391 - The *users* module no longer accepts `root` as a username. #1462 + - The *keyboardq* module is now more inline with the look of the rest + of the Calamares modules, use of a background image is removed. # 3.2.27 (2020-07-11) # From 59dff815fcec2247276ddd8264bda029656f2451 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 27 Jul 2020 11:13:35 +0200 Subject: [PATCH 071/113] [libcalamares] Additional apply() methods for Permissions --- src/libcalamares/utils/Permissions.cpp | 48 ++++++++++++++++++++++++-- src/libcalamares/utils/Permissions.h | 20 +++++++++-- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/libcalamares/utils/Permissions.cpp b/src/libcalamares/utils/Permissions.cpp index 3f5350e86..a7afbc5fc 100644 --- a/src/libcalamares/utils/Permissions.cpp +++ b/src/libcalamares/utils/Permissions.cpp @@ -8,11 +8,14 @@ #include "Permissions.h" -#include +#include "Logger.h" +#include #include #include +#include + namespace CalamaresUtils { @@ -73,8 +76,49 @@ Permissions::parsePermissions( QString const& p ) bool Permissions::apply( const QString& path, int mode ) { - int r = chmod( path.toUtf8().constData(), mode ); + // We **don't** use QFile::setPermissions() here because it takes + // a Qt flags object that subtlely does not align with POSIX bits. + // The Qt flags are **hex** based, so 0x755 for rwxr-xr-x, while + // our integer (mode_t) stores **octal** based flags. + // + // Call chmod(2) directly, that's what Qt would be doing underneath + // anyway. + int r = chmod( path.toUtf8().constData(), mode_t( mode ) ); + if ( r ) + { + cDebug() << Logger::SubEntry << "Could not set permissions of" << path << "to" << QString::number( mode, 8 ); + } return r == 0; } +bool +Permissions::apply( const QString& path, const CalamaresUtils::Permissions& p ) +{ + if ( !p.isValid() ) + { + return false; + } + bool r = apply( path, p.value() ); + if ( r ) + { + // We don't use chgrp(2) or chown(2) here because then we need to + // go through the users list (which one, target or source?) to get + // uid_t and gid_t values to pass to that system call. + // + // Do a lame cop-out and let the chown(8) utility do the heavy lifting. + if ( QProcess::execute( "chown", { p.username() + ':' + p.group(), path } ) ) + { + r = false; + cDebug() << Logger::SubEntry << "Could not set owner of" << path << "to" + << ( p.username() + ':' + p.group() ); + } + } + if ( r ) + { + /* NOTUSED */ apply( path, p.value() ); + } + return r; +} + + } // namespace CalamaresUtils diff --git a/src/libcalamares/utils/Permissions.h b/src/libcalamares/utils/Permissions.h index 913d037d5..1f0dd38da 100644 --- a/src/libcalamares/utils/Permissions.h +++ b/src/libcalamares/utils/Permissions.h @@ -49,7 +49,7 @@ public: * * Bear in mind that input is in octal, but integers are just integers; * naively printing them will get decimal results (e.g. 493 from the - * input of "root:wheel:755"). + * input of "root:wheel:755"). This is suitable to pass to apply(). */ int value() const { return m_value; } /** @brief The value (file permission) as octal string @@ -59,8 +59,24 @@ public: */ QString octal() const { return QString::number( value(), 8 ); } - /// chmod(path, mode), returns true on success + /** @brief Sets the file-access @p mode of @p path + * + * Pass a path that is relative (or absolute) in the **host** system. + */ static bool apply( const QString& path, int mode ); + /** @brief Do both chmod and chown on @p path + * + * Note that interpreting user- and group- names for applying the + * permissions can be different between the host system and the target + * system; the target might not have a "live" user, for instance, and + * the host won't have the user-entered username for the installation. + * + * For this call, the names are interpreted in the **host** system. + * Pass a path that is relative (or absolute) in the **host** system. + */ + static bool apply( const QString& path, const Permissions& p ); + /// Convenience method for apply(const QString&, const Permissions& ) + bool apply( const QString& path ) const { return apply( path, *this ); } private: void parsePermissions( QString const& p ); From 90a0605f38d5268b262998270426b293b0c946e0 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 27 Jul 2020 12:25:50 +0200 Subject: [PATCH 072/113] [preservefiles] [users] Use the Permissions methods - don't call out to tools (executables) when we have an API for it (which might call out to those tools, but that's abstracted) --- src/modules/preservefiles/PreserveFiles.cpp | 21 ++------------------- src/modules/users/CreateUserJob.cpp | 3 ++- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/src/modules/preservefiles/PreserveFiles.cpp b/src/modules/preservefiles/PreserveFiles.cpp index 57c2c5422..8352e861d 100644 --- a/src/modules/preservefiles/PreserveFiles.cpp +++ b/src/modules/preservefiles/PreserveFiles.cpp @@ -157,26 +157,9 @@ PreserveFiles::exec() { if ( it.perm.isValid() ) { - auto s_p = CalamaresUtils::System::instance(); - - int r; - - r = s_p->targetEnvCall( QStringList { "chown", it.perm.username(), bare_dest } ); - if ( r ) + if ( !it.perm.apply( CalamaresUtils::System::instance()->targetPath( bare_dest ) ) ) { - cWarning() << "Could not chown target" << bare_dest; - } - - r = s_p->targetEnvCall( QStringList { "chgrp", it.perm.group(), bare_dest } ); - if ( r ) - { - cWarning() << "Could not chgrp target" << bare_dest; - } - - r = s_p->targetEnvCall( QStringList { "chmod", it.perm.octal(), bare_dest } ); - if ( r ) - { - cWarning() << "Could not chmod target" << bare_dest; + cWarning() << "Could not set attributes of" << bare_dest; } } diff --git a/src/modules/users/CreateUserJob.cpp b/src/modules/users/CreateUserJob.cpp index f0b1dca88..84a42437a 100644 --- a/src/modules/users/CreateUserJob.cpp +++ b/src/modules/users/CreateUserJob.cpp @@ -12,6 +12,7 @@ #include "JobQueue.h" #include "utils/CalamaresUtilsSystem.h" #include "utils/Logger.h" +#include "utils/Permissions.h" #include #include @@ -103,7 +104,7 @@ CreateUserJob::exec() if ( fileResult ) { - if ( QProcess::execute( "chmod", { "440", fileResult.path() } ) ) + if ( CalamaresUtils::Permissions::apply( fileResult.path(), 0440 ) ) { return Calamares::JobResult::error( tr( "Cannot chmod sudoers file." ) ); } From b99b87f787c0e32e1eadb095fafdd45bf8a4f6be Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 27 Jul 2020 12:37:04 +0200 Subject: [PATCH 073/113] [users] Explain some weird internals --- src/modules/users/CreateUserJob.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/users/CreateUserJob.cpp b/src/modules/users/CreateUserJob.cpp index 84a42437a..2031aa029 100644 --- a/src/modules/users/CreateUserJob.cpp +++ b/src/modules/users/CreateUserJob.cpp @@ -131,7 +131,8 @@ CreateUserJob::exec() } } - // If we're looking to reuse the contents of an existing /home + // If we're looking to reuse the contents of an existing /home. + // This GS setting comes from the **partitioning** module. if ( gs->value( "reuseHome" ).toBool() ) { QString shellFriendlyHome = "/home/" + m_userName; @@ -141,6 +142,7 @@ CreateUserJob::exec() QString backupDirName = "dotfiles_backup_" + QDateTime::currentDateTime().toString( "yyyy-MM-dd_HH-mm-ss" ); existingHome.mkdir( backupDirName ); + // We need the extra `sh -c` here to ensure that we can expand the shell globs CalamaresUtils::System::instance()->targetEnvCall( { "sh", "-c", "mv -f " + shellFriendlyHome + "/.* " + shellFriendlyHome + "/" + backupDirName } ); } From 1fddf723fe30cc2ba35d96a1c11069050c312e00 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 27 Jul 2020 13:18:09 +0200 Subject: [PATCH 074/113] [users] FreeBSD support creating groups --- src/modules/users/CreateUserJob.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/modules/users/CreateUserJob.cpp b/src/modules/users/CreateUserJob.cpp index 2031aa029..8927af8ab 100644 --- a/src/modules/users/CreateUserJob.cpp +++ b/src/modules/users/CreateUserJob.cpp @@ -81,7 +81,11 @@ ensureGroupsExistInTarget( const QStringList& wantedGroups, const QStringList& a { if ( !availableGroups.contains( group ) ) { +#ifdef __FreeBSD__ + CalamaresUtils::System::instance()->targetEnvCall( { "pw", "groupadd", "-n", group } ); +#else CalamaresUtils::System::instance()->targetEnvCall( { "groupadd", group } ); +#endif } } } From 26b8c826301aa26386e239c4d346f706a60a9e88 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 27 Jul 2020 13:29:51 +0200 Subject: [PATCH 075/113] [users] Refactor user-creation and user-group-setting into methods - This is prep-work for handling other tools for user- and group- creation as well. --- src/modules/users/CreateUserJob.cpp | 74 ++++++++++++++++++----------- 1 file changed, 45 insertions(+), 29 deletions(-) diff --git a/src/modules/users/CreateUserJob.cpp b/src/modules/users/CreateUserJob.cpp index 8927af8ab..e052fa266 100644 --- a/src/modules/users/CreateUserJob.cpp +++ b/src/modules/users/CreateUserJob.cpp @@ -90,6 +90,40 @@ ensureGroupsExistInTarget( const QStringList& wantedGroups, const QStringList& a } } +static Calamares::JobResult +createUser( const QString& loginName, const QString& fullName, const QString& shell ) +{ + QStringList useradd { "useradd", "-m", "-U" }; + if ( !shell.isEmpty() ) + { + useradd << "-s" << shell; + } + useradd << "-c" << fullName; + useradd << loginName; + + auto commandResult = CalamaresUtils::System::instance()->targetEnvCommand( useradd ); + if ( commandResult.getExitCode() ) + { + cError() << "useradd failed" << commandResult.getExitCode(); + return commandResult.explainProcess( useradd, std::chrono::seconds( 10 ) /* bogus timeout */ ); + } + return Calamares::JobResult::ok(); +} + +static Calamares::JobResult +setUserGroups( const QString& loginName, const QStringList& groups ) +{ + auto commandResult + = CalamaresUtils::System::instance()->targetEnvCommand( { "usermod", "-aG", groups.join( ',' ), loginName } ); + if ( commandResult.getExitCode() ) + { + cError() << "usermod failed" << commandResult.getExitCode(); + return commandResult.explainProcess( "usermod", std::chrono::seconds( 10 ) /* bogus timeout */ ); + } + return Calamares::JobResult::ok(); +} + + Calamares::JobResult CreateUserJob::exec() { @@ -122,18 +156,12 @@ CreateUserJob::exec() cDebug() << "[CREATEUSER]: preparing groups"; QStringList availableGroups = groupsInTargetSystem( destDir ); - ensureGroupsExistInTarget( m_defaultGroups, availableGroups ); - - QString defaultGroups = m_defaultGroups.join( ',' ); - if ( m_autologin ) + QStringList groupsForThisUser = m_defaultGroups; + if ( m_autologin && gs->contains( "autologinGroup" ) && !gs->value( "autologinGroup" ).toString().isEmpty() ) { - if ( gs->contains( "autologinGroup" ) && !gs->value( "autologinGroup" ).toString().isEmpty() ) - { - QString autologinGroup = gs->value( "autologinGroup" ).toString(); - ensureGroupsExistInTarget( QStringList { autologinGroup }, availableGroups ); - defaultGroups.append( QString( ",%1" ).arg( autologinGroup ) ); - } + groupsForThisUser << gs->value( "autologinGroup" ).toString(); } + ensureGroupsExistInTarget( m_defaultGroups, availableGroups ); // If we're looking to reuse the contents of an existing /home. // This GS setting comes from the **partitioning** module. @@ -154,33 +182,21 @@ CreateUserJob::exec() cDebug() << "[CREATEUSER]: creating user"; - QStringList useradd { "useradd", "-m", "-U" }; - QString shell = gs->value( "userShell" ).toString(); - if ( !shell.isEmpty() ) + auto useraddResult = createUser( m_userName, m_fullName, gs->value( "userShell" ).toString() ); + if ( !useraddResult ) { - useradd << "-s" << shell; - } - useradd << "-c" << m_fullName; - useradd << m_userName; - - auto commandResult = CalamaresUtils::System::instance()->targetEnvCommand( useradd ); - if ( commandResult.getExitCode() ) - { - cError() << "useradd failed" << commandResult.getExitCode(); - return commandResult.explainProcess( useradd, std::chrono::seconds( 10 ) /* bogus timeout */ ); + return useraddResult; } - commandResult - = CalamaresUtils::System::instance()->targetEnvCommand( { "usermod", "-aG", defaultGroups, m_userName } ); - if ( commandResult.getExitCode() ) + auto usergroupsResult = setUserGroups( m_userName, groupsForThisUser ); + if ( !usergroupsResult ) { - cError() << "usermod failed" << commandResult.getExitCode(); - return commandResult.explainProcess( "usermod", std::chrono::seconds( 10 ) /* bogus timeout */ ); + return usergroupsResult; } QString userGroup = QString( "%1:%2" ).arg( m_userName ).arg( m_userName ); QString homeDir = QString( "/home/%1" ).arg( m_userName ); - commandResult = CalamaresUtils::System::instance()->targetEnvCommand( { "chown", "-R", userGroup, homeDir } ); + auto commandResult = CalamaresUtils::System::instance()->targetEnvCommand( { "chown", "-R", userGroup, homeDir } ); if ( commandResult.getExitCode() ) { cError() << "chown failed" << commandResult.getExitCode(); From 8a6e4af511b0a5294e7236f4309cf2ab1c41cb4d Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 27 Jul 2020 13:45:00 +0200 Subject: [PATCH 076/113] [users] FreeBSD support creating user - call pw useradd and pw usermod as needed; the code paths are basically the same in invoking a program in the target system to do the work. --- src/modules/users/CreateUserJob.cpp | 35 ++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/modules/users/CreateUserJob.cpp b/src/modules/users/CreateUserJob.cpp index e052fa266..0fe931fa8 100644 --- a/src/modules/users/CreateUserJob.cpp +++ b/src/modules/users/CreateUserJob.cpp @@ -93,19 +93,33 @@ ensureGroupsExistInTarget( const QStringList& wantedGroups, const QStringList& a static Calamares::JobResult createUser( const QString& loginName, const QString& fullName, const QString& shell ) { - QStringList useradd { "useradd", "-m", "-U" }; + QStringList useraddCommand; +#ifdef __FreeBSD__ + useraddCommand << "pw" + << "useradd" + << "-n" << loginName << "-m" + << "-c" << fullName; + if ( !shell.isEmpty() ) + { + useraddCommand << "-s" << shell; + } +#else + useraddCommand << "useradd" + << "-m" + << "-U"; if ( !shell.isEmpty() ) { useradd << "-s" << shell; } useradd << "-c" << fullName; useradd << loginName; +#endif - auto commandResult = CalamaresUtils::System::instance()->targetEnvCommand( useradd ); + auto commandResult = CalamaresUtils::System::instance()->targetEnvCommand( useraddCommand ); if ( commandResult.getExitCode() ) { cError() << "useradd failed" << commandResult.getExitCode(); - return commandResult.explainProcess( useradd, std::chrono::seconds( 10 ) /* bogus timeout */ ); + return commandResult.explainProcess( useraddCommand, std::chrono::seconds( 10 ) /* bogus timeout */ ); } return Calamares::JobResult::ok(); } @@ -113,12 +127,21 @@ createUser( const QString& loginName, const QString& fullName, const QString& sh static Calamares::JobResult setUserGroups( const QString& loginName, const QStringList& groups ) { - auto commandResult - = CalamaresUtils::System::instance()->targetEnvCommand( { "usermod", "-aG", groups.join( ',' ), loginName } ); + QStringList setgroupsCommand; +#ifdef __FreeBSD__ + setgroupsCommand << "pw" + << "usermod" + << "-n" << loginName << "-G" << groups.join( ',' ); +#else + setgroupsCommand << "usermod" + << "-aG" << groups.join( ',' ) << loginName; +#endif + + auto commandResult = CalamaresUtils::System::instance()->targetEnvCommand( setgroupsCommand ); if ( commandResult.getExitCode() ) { cError() << "usermod failed" << commandResult.getExitCode(); - return commandResult.explainProcess( "usermod", std::chrono::seconds( 10 ) /* bogus timeout */ ); + return commandResult.explainProcess( setgroupsCommand, std::chrono::seconds( 10 ) /* bogus timeout */ ); } return Calamares::JobResult::ok(); } From 8ce7457023aa49bfce2d3291f70612cb2d86958e Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 27 Jul 2020 15:00:14 +0200 Subject: [PATCH 077/113] [users] Add test for create-users code - just one test for groups-file loading - while here fix bug that blank and comment lines were being kept as valid group names --- src/modules/users/CMakeLists.txt | 7 +++++++ src/modules/users/CreateUserJob.cpp | 16 ++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/modules/users/CMakeLists.txt b/src/modules/users/CMakeLists.txt index 1d969a93b..a449d39ac 100644 --- a/src/modules/users/CMakeLists.txt +++ b/src/modules/users/CMakeLists.txt @@ -49,6 +49,13 @@ calamares_add_test( ${CRYPT_LIBRARIES} ) +calamares_add_test( + userscreatetest + SOURCES + CreateUserTests.cpp + CreateUserJob.cpp +) + calamares_add_test( userstest SOURCES diff --git a/src/modules/users/CreateUserJob.cpp b/src/modules/users/CreateUserJob.cpp index 0fe931fa8..beb454ac0 100644 --- a/src/modules/users/CreateUserJob.cpp +++ b/src/modules/users/CreateUserJob.cpp @@ -55,7 +55,7 @@ CreateUserJob::prettyStatusMessage() const return tr( "Creating user %1." ).arg( m_userName ); } -static QStringList +STATICTEST QStringList groupsInTargetSystem( const QDir& targetRoot ) { QFileInfo groupsFi( targetRoot.absoluteFilePath( "etc/group" ) ); @@ -66,10 +66,22 @@ groupsInTargetSystem( const QDir& targetRoot ) } QString groupsData = QString::fromLocal8Bit( groupsFile.readAll() ); QStringList groupsLines = groupsData.split( '\n' ); - for ( QStringList::iterator it = groupsLines.begin(); it != groupsLines.end(); ++it ) + QStringList::iterator it = groupsLines.begin(); + while ( it != groupsLines.end() ) { + if ( it->startsWith( '#' ) ) + { + it = groupsLines.erase( it ); + continue; + } int indexOfFirstToDrop = it->indexOf( ':' ); + if ( indexOfFirstToDrop < 1 ) + { + it = groupsLines.erase( it ); + continue; + } it->truncate( indexOfFirstToDrop ); + ++it; } return groupsLines; } From 1e08ee084f096b3cdf5cfd081abe9ad5d5b1eca6 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 27 Jul 2020 15:35:24 +0200 Subject: [PATCH 078/113] [users] Actually add the test file --- src/modules/users/CreateUserTests.cpp | 80 +++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/modules/users/CreateUserTests.cpp diff --git a/src/modules/users/CreateUserTests.cpp b/src/modules/users/CreateUserTests.cpp new file mode 100644 index 000000000..b351109b3 --- /dev/null +++ b/src/modules/users/CreateUserTests.cpp @@ -0,0 +1,80 @@ +/* === This file is part of Calamares - === + * + * SPDX-FileCopyrightText: 2020 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later + * + * 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 . + */ + +#include "CreateUserJob.h" + +#include "utils/Logger.h" + +#include +#include + +// Implementation details +extern QStringList groupsInTargetSystem( const QDir& targetRoot ); + + +class CreateUserTests : public QObject +{ + Q_OBJECT +public: + CreateUserTests(); + virtual ~CreateUserTests() {} + +private Q_SLOTS: + void initTestCase(); + + void testReadGroup(); +}; + +CreateUserTests::CreateUserTests() {} + +void +CreateUserTests::initTestCase() +{ + Logger::setupLogLevel( Logger::LOGDEBUG ); + cDebug() << "Users test started."; +} + +void +CreateUserTests::testReadGroup() +{ + QDir root( "/" ); + QVERIFY( root.exists() ); + + // Get the groups in the host system + QStringList groups = groupsInTargetSystem( root ); + QVERIFY( groups.count() > 2 ); +#ifdef __FreeBSD__ + QVERIFY( groups.contains( QStringLiteral( "wheel" ) ) ); +#else + QVERIFY( groups.contains( QStringLiteral( "root" ) ) ); +#endif + QVERIFY( groups.contains( QStringLiteral( "sys" ) ) ); + + for ( const QString& s : groups ) + { + QVERIFY( !s.isEmpty() ); + QVERIFY( !s.contains( '#' ) ); + } +} + +QTEST_GUILESS_MAIN( CreateUserTests ) + +#include "utils/moc-warnings.h" + +#include "CreateUserTests.moc" From dab831b2ffe994fba4f07ae3b0f426080e8d00ae Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Sat, 25 Jul 2020 12:47:01 +0200 Subject: [PATCH 079/113] [users] Introduce a (stub) Config object --- src/modules/users/CMakeLists.txt | 1 + src/modules/users/Config.cpp | 33 ++++++++++++++++++++++++++++ src/modules/users/Config.h | 37 ++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 src/modules/users/Config.cpp create mode 100644 src/modules/users/Config.h diff --git a/src/modules/users/CMakeLists.txt b/src/modules/users/CMakeLists.txt index a449d39ac..bf93eb26a 100644 --- a/src/modules/users/CMakeLists.txt +++ b/src/modules/users/CMakeLists.txt @@ -28,6 +28,7 @@ calamares_add_plugin( users UsersPage.cpp SetHostNameJob.cpp CheckPWQuality.cpp + Config.cpp UI page_usersetup.ui RESOURCES diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp new file mode 100644 index 000000000..fb69298f6 --- /dev/null +++ b/src/modules/users/Config.cpp @@ -0,0 +1,33 @@ +/* === This file is part of Calamares - === + * + * SPDX-FileCopyrightText: 2020 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later + * License-Filename: LICENSE + * + * 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 . + */ + +#include "Config.h" + +Config::Config( QObject* parent ) + : QObject( parent ) +{ +} + +Config::~Config() {} + +void +Config::setConfigurationMap( const QVariantMap& ) +{ +} diff --git a/src/modules/users/Config.h b/src/modules/users/Config.h new file mode 100644 index 000000000..95c8c33bd --- /dev/null +++ b/src/modules/users/Config.h @@ -0,0 +1,37 @@ +/* === This file is part of Calamares - === + * + * SPDX-FileCopyrightText: 2020 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later + * License-Filename: LICENSE + * + * 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 . + */ + +#ifndef USERS_CONFIG_H +#define USERS_CONFIG_H + +#include +#include + +class Config : public QObject +{ +public: + Config( QObject* parent = nullptr ); + ~Config(); + + // Currently, config does nothing + void setConfigurationMap( const QVariantMap& ); +}; + +#endif From 4d85a64e4f5e130562aaa1b6100a7c9a53eb00b4 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 27 Jul 2020 17:14:06 +0200 Subject: [PATCH 080/113] [users] Fix build on Linux --- src/modules/users/CreateUserJob.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/users/CreateUserJob.cpp b/src/modules/users/CreateUserJob.cpp index beb454ac0..a6812dd53 100644 --- a/src/modules/users/CreateUserJob.cpp +++ b/src/modules/users/CreateUserJob.cpp @@ -121,10 +121,10 @@ createUser( const QString& loginName, const QString& fullName, const QString& sh << "-U"; if ( !shell.isEmpty() ) { - useradd << "-s" << shell; + useraddCommand << "-s" << shell; } - useradd << "-c" << fullName; - useradd << loginName; + useraddCommand << "-c" << fullName; + useraddCommand << loginName; #endif auto commandResult = CalamaresUtils::System::instance()->targetEnvCommand( useraddCommand ); From f9b114a67ad7d6b27bd90148493b82575c838460 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Sat, 25 Jul 2020 15:21:04 +0200 Subject: [PATCH 081/113] [users] Pass the Config object to the Page - delay construction of the Page (widget) until it's needed - hand the Config object to the Page on construction This is prep-work for putting the configuration information into the Config object, rather than in the UI elements. --- src/modules/users/UsersPage.cpp | 7 ++++--- src/modules/users/UsersPage.h | 5 ++++- src/modules/users/UsersViewStep.cpp | 28 +++++++++++++++++++++------- src/modules/users/UsersViewStep.h | 3 +++ 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/modules/users/UsersPage.cpp b/src/modules/users/UsersPage.cpp index 5c649d622..62b8920e7 100644 --- a/src/modules/users/UsersPage.cpp +++ b/src/modules/users/UsersPage.cpp @@ -79,9 +79,10 @@ labelOk( QLabel* pix, QLabel* label ) pix->setPixmap( CalamaresUtils::defaultPixmap( CalamaresUtils::Yes, CalamaresUtils::Original, label->size() ) ); } -UsersPage::UsersPage( QWidget* parent ) +UsersPage::UsersPage( Config* config, QWidget* parent ) : QWidget( parent ) , ui( new Ui::Page_UserSetup ) + , m_config( config ) , m_readyFullName( false ) , m_readyUsername( false ) , m_readyHostname( false ) @@ -99,12 +100,12 @@ UsersPage::UsersPage( QWidget* parent ) connect( ui->textBoxUserVerifiedPassword, &QLineEdit::textChanged, this, &UsersPage::onPasswordTextChanged ); connect( ui->textBoxRootPassword, &QLineEdit::textChanged, this, &UsersPage::onRootPasswordTextChanged ); connect( ui->textBoxVerifiedRootPassword, &QLineEdit::textChanged, this, &UsersPage::onRootPasswordTextChanged ); - connect( ui->checkBoxValidatePassword, &QCheckBox::stateChanged, this, [ this ]( int ) { + connect( ui->checkBoxValidatePassword, &QCheckBox::stateChanged, this, [this]( int ) { onPasswordTextChanged( ui->textBoxUserPassword->text() ); onRootPasswordTextChanged( ui->textBoxRootPassword->text() ); checkReady( isReady() ); } ); - connect( ui->checkBoxReusePassword, &QCheckBox::stateChanged, this, [ this ]( int checked ) { + connect( ui->checkBoxReusePassword, &QCheckBox::stateChanged, this, [this]( int checked ) { /* When "reuse" is checked, hide the fields for explicitly * entering the root password. However, if we're going to * disable the root password anyway, hide them all regardless of diff --git a/src/modules/users/UsersPage.h b/src/modules/users/UsersPage.h index 3382e9335..a13886de6 100644 --- a/src/modules/users/UsersPage.h +++ b/src/modules/users/UsersPage.h @@ -29,6 +29,8 @@ #include +class Config; + class QLabel; namespace Ui @@ -40,7 +42,7 @@ class UsersPage : public QWidget { Q_OBJECT public: - explicit UsersPage( QWidget* parent = nullptr ); + explicit UsersPage( Config* config, QWidget* parent = nullptr ); virtual ~UsersPage(); bool isReady(); @@ -95,6 +97,7 @@ private: void retranslate(); Ui::Page_UserSetup* ui; + Config* m_config; PasswordCheckList m_passwordChecks; bool m_passwordChecksChanged = false; diff --git a/src/modules/users/UsersViewStep.cpp b/src/modules/users/UsersViewStep.cpp index fe633b1c2..3fc86eb91 100644 --- a/src/modules/users/UsersViewStep.cpp +++ b/src/modules/users/UsersViewStep.cpp @@ -20,17 +20,17 @@ #include "UsersViewStep.h" +#include "Config.h" #include "SetHostNameJob.h" #include "SetPasswordJob.h" #include "UsersPage.h" +#include "GlobalStorage.h" +#include "JobQueue.h" #include "utils/Logger.h" #include "utils/NamedEnum.h" #include "utils/Variant.h" -#include "GlobalStorage.h" -#include "JobQueue.h" - CALAMARES_PLUGIN_FACTORY_DEFINITION( UsersViewStepFactory, registerPlugin< UsersViewStep >(); ) static const NamedEnumTable< SetHostNameJob::Action >& @@ -53,11 +53,11 @@ hostnameActions() UsersViewStep::UsersViewStep( QObject* parent ) : Calamares::ViewStep( parent ) - , m_widget( new UsersPage() ) + , m_widget( nullptr ) , m_actions( SetHostNameJob::Action::None ) + , m_config( new Config( this ) ) { emit nextStatusChanged( true ); - connect( m_widget, &UsersPage::checkReady, this, &UsersViewStep::nextStatusChanged ); } @@ -80,6 +80,11 @@ UsersViewStep::prettyName() const QWidget* UsersViewStep::widget() { + if ( !m_widget ) + { + m_widget = new UsersPage( m_config ); + connect( m_widget, &UsersPage::checkReady, this, &UsersViewStep::nextStatusChanged ); + } return m_widget; } @@ -87,7 +92,7 @@ UsersViewStep::widget() bool UsersViewStep::isNextEnabled() const { - return m_widget->isReady(); + return m_widget ? m_widget->isReady() : true; } @@ -122,7 +127,10 @@ UsersViewStep::jobs() const void UsersViewStep::onActivate() { - m_widget->onActivate(); + if ( m_widget ) + { + m_widget->onActivate(); + } } @@ -130,6 +138,10 @@ void UsersViewStep::onLeave() { m_jobs.clear(); + if ( !m_widget ) + { + return; + } m_jobs.append( m_widget->createJobs( m_defaultGroups ) ); Calamares::Job* j; @@ -222,4 +234,6 @@ UsersViewStep::setConfigurationMap( const QVariantMap& configurationMap ) Action hostsfileAction = getBool( configurationMap, "writeHostsFile", true ) ? Action::WriteEtcHosts : Action::None; m_actions = hostsfileAction | hostnameAction; + + m_config->setConfigurationMap( configurationMap ); } diff --git a/src/modules/users/UsersViewStep.h b/src/modules/users/UsersViewStep.h index 03cc83819..aacf11f2a 100644 --- a/src/modules/users/UsersViewStep.h +++ b/src/modules/users/UsersViewStep.h @@ -29,6 +29,7 @@ #include #include +class Config; class UsersPage; class PLUGINDLLEXPORT UsersViewStep : public Calamares::ViewStep @@ -62,6 +63,8 @@ private: QStringList m_defaultGroups; SetHostNameJob::Actions m_actions; + + Config* m_config; }; CALAMARES_PLUGIN_FACTORY_DECLARATION( UsersViewStepFactory ) From 8497aad7a173500d67fa3e28dbf609bf42caa9b6 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Sat, 25 Jul 2020 15:27:41 +0200 Subject: [PATCH 082/113] [users] Apply coding style --- src/modules/users/CheckPWQuality.cpp | 8 ++++---- src/modules/users/PasswordTests.cpp | 4 ++-- src/modules/users/Tests.cpp | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/modules/users/CheckPWQuality.cpp b/src/modules/users/CheckPWQuality.cpp index ab3eed84f..f06137c9b 100644 --- a/src/modules/users/CheckPWQuality.cpp +++ b/src/modules/users/CheckPWQuality.cpp @@ -55,7 +55,7 @@ DEFINE_CHECK_FUNC( minLength ) { cDebug() << Logger::SubEntry << "minLength set to" << minLength; checks.push_back( PasswordCheck( []() { return QCoreApplication::translate( "PWQ", "Password is too short" ); }, - [ minLength ]( const QString& s ) { return s.length() >= minLength; }, + [minLength]( const QString& s ) { return s.length() >= minLength; }, PasswordCheck::Weight( 10 ) ) ); } } @@ -71,7 +71,7 @@ DEFINE_CHECK_FUNC( maxLength ) { cDebug() << Logger::SubEntry << "maxLength set to" << maxLength; checks.push_back( PasswordCheck( []() { return QCoreApplication::translate( "PWQ", "Password is too long" ); }, - [ maxLength ]( const QString& s ) { return s.length() <= maxLength; }, + [maxLength]( const QString& s ) { return s.length() <= maxLength; }, PasswordCheck::Weight( 10 ) ) ); } } @@ -349,8 +349,8 @@ DEFINE_CHECK_FUNC( libpwquality ) /* Something actually added? */ if ( requirement_count ) { - checks.push_back( PasswordCheck( [ settings ]() { return settings->explanation(); }, - [ settings ]( const QString& s ) { + checks.push_back( PasswordCheck( [settings]() { return settings->explanation(); }, + [settings]( const QString& s ) { int r = settings->check( s ); if ( r < 0 ) { diff --git a/src/modules/users/PasswordTests.cpp b/src/modules/users/PasswordTests.cpp index b33526162..0c1b4bffc 100644 --- a/src/modules/users/PasswordTests.cpp +++ b/src/modules/users/PasswordTests.cpp @@ -24,9 +24,9 @@ QTEST_GUILESS_MAIN( PasswordTests ) -PasswordTests::PasswordTests() { } +PasswordTests::PasswordTests() {} -PasswordTests::~PasswordTests() { } +PasswordTests::~PasswordTests() {} void PasswordTests::initTestCase() diff --git a/src/modules/users/Tests.cpp b/src/modules/users/Tests.cpp index 75c5e6d5f..196fd9d68 100644 --- a/src/modules/users/Tests.cpp +++ b/src/modules/users/Tests.cpp @@ -37,7 +37,7 @@ class UsersTests : public QObject Q_OBJECT public: UsersTests(); - virtual ~UsersTests() { } + virtual ~UsersTests() {} private Q_SLOTS: void initTestCase(); From 2f786079f30e75b4c999e8f109e4e650750777af Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Sat, 25 Jul 2020 15:39:19 +0200 Subject: [PATCH 083/113] [users] Move shell settings to the Config object - this is a set-only property (as far as the current ViewStep is concerned) and is passed around in GS for non-obvious reasons. --- src/modules/users/Config.cpp | 27 ++++++++++++++++++++++++++- src/modules/users/Config.h | 29 ++++++++++++++++++++++++++++- src/modules/users/UsersViewStep.cpp | 9 --------- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp index fb69298f6..58e2b76ac 100644 --- a/src/modules/users/Config.cpp +++ b/src/modules/users/Config.cpp @@ -20,6 +20,11 @@ #include "Config.h" +#include "GlobalStorage.h" +#include "JobQueue.h" +#include "utils/Logger.h" +#include "utils/Variant.h" + Config::Config( QObject* parent ) : QObject( parent ) { @@ -28,6 +33,26 @@ Config::Config( QObject* parent ) Config::~Config() {} void -Config::setConfigurationMap( const QVariantMap& ) +Config::setUserShell( const QString& shell ) { + if ( !shell.isEmpty() && !shell.startsWith( '/' ) ) + { + cWarning() << "User shell" << shell << "is not an absolute path."; + return; + } + // The shell is put into GS because the CreateUser job expects it there + Calamares::JobQueue::instance()->globalStorage()->insert( "userShell", shell ); +} + + +void +Config::setConfigurationMap( const QVariantMap& configurationMap ) +{ + QString shell( QLatin1String( "/bin/bash" ) ); // as if it's not set at all + if ( configurationMap.contains( "userShell" ) ) + { + shell = CalamaresUtils::getString( configurationMap, "userShell" ); + } + // Now it might be explicitly set to empty, which is ok + setUserShell( shell ); } diff --git a/src/modules/users/Config.h b/src/modules/users/Config.h index 95c8c33bd..27416e88d 100644 --- a/src/modules/users/Config.h +++ b/src/modules/users/Config.h @@ -26,12 +26,39 @@ class Config : public QObject { + Q_OBJECT + + Q_PROPERTY( QString userShell READ userShell WRITE setUserShell NOTIFY userShellChanged ) + public: Config( QObject* parent = nullptr ); ~Config(); - // Currently, config does nothing void setConfigurationMap( const QVariantMap& ); + + /** @brief Full path to the user's shell executable + * + * Typically this will be /bin/bash, but it can be set from + * the config file with the *userShell* setting. + */ + QString userShell() const { return m_userShell; } + +public Q_SLOTS: + /** @brief Sets the user's shell if possible + * + * If the path is empty, that's ok: no shell will be explicitly set, + * so the user will get whatever shell is set to default in the target. + * + * The given non-empty @p path must be an absolute path (for use inside + * the target system!); if it is not, the shell is not changed. + */ + void setUserShell( const QString& path ); + +signals: + void userShellChanged( const QString& ); + +private: + QString m_userShell; }; #endif diff --git a/src/modules/users/UsersViewStep.cpp b/src/modules/users/UsersViewStep.cpp index 3fc86eb91..e037902ab 100644 --- a/src/modules/users/UsersViewStep.cpp +++ b/src/modules/users/UsersViewStep.cpp @@ -209,15 +209,6 @@ UsersViewStep::setConfigurationMap( const QVariantMap& configurationMap ) m_widget->setPasswordCheckboxVisible( getBool( configurationMap, "allowWeakPasswords", false ) ); m_widget->setValidatePasswordDefault( !getBool( configurationMap, "allowWeakPasswordsDefault", false ) ); - QString shell( QLatin1String( "/bin/bash" ) ); // as if it's not set at all - if ( configurationMap.contains( "userShell" ) ) - { - shell = CalamaresUtils::getString( configurationMap, "userShell" ); - } - // Now it might be explicitly set to empty, which is ok - - Calamares::JobQueue::instance()->globalStorage()->insert( "userShell", shell ); - using Action = SetHostNameJob::Action; QString hostnameActionString = CalamaresUtils::getString( configurationMap, "setHostname" ); From 35916eb20ff8ea351cba4047932ce52aeec62e15 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Sat, 25 Jul 2020 16:39:13 +0200 Subject: [PATCH 084/113] [users] Move autologin and sudoers groups to Config --- src/modules/users/Config.cpp | 28 ++++++++++++++++++++++++++++ src/modules/users/Config.h | 17 +++++++++++++++++ src/modules/users/UsersViewStep.cpp | 14 -------------- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp index 58e2b76ac..a071da8fc 100644 --- a/src/modules/users/Config.cpp +++ b/src/modules/users/Config.cpp @@ -44,6 +44,31 @@ Config::setUserShell( const QString& shell ) Calamares::JobQueue::instance()->globalStorage()->insert( "userShell", shell ); } +static inline void +setGS( const QString& key, const QString& group ) +{ + auto* gs = Calamares::JobQueue::instance()->globalStorage(); + if ( !gs || group.isEmpty() ) + { + return; + } + gs->insert( key, group ); +} + +void +Config::setAutologinGroup( const QString& group ) +{ + setGS( QStringLiteral( "autologinGroup" ), group ); + emit autologinGroupChanged( group ); +} + +void +Config::setSudoersGroup( const QString& group ) +{ + setGS( QStringLiteral( "sudoersGroup" ), group ); + emit sudoersGroupChanged( group ); +} + void Config::setConfigurationMap( const QVariantMap& configurationMap ) @@ -55,4 +80,7 @@ Config::setConfigurationMap( const QVariantMap& configurationMap ) } // Now it might be explicitly set to empty, which is ok setUserShell( shell ); + + setAutologinGroup( CalamaresUtils::getString( configurationMap, "autologinGroup" ) ); + setSudoersGroup( CalamaresUtils::getString( configurationMap, "sudoersGroup" ) ); } diff --git a/src/modules/users/Config.h b/src/modules/users/Config.h index 27416e88d..b399c6141 100644 --- a/src/modules/users/Config.h +++ b/src/modules/users/Config.h @@ -30,6 +30,9 @@ class Config : public QObject Q_PROPERTY( QString userShell READ userShell WRITE setUserShell NOTIFY userShellChanged ) + Q_PROPERTY( QString autologinGroup READ autologinGroup WRITE setAutologinGroup NOTIFY autologinGroupChanged ) + Q_PROPERTY( QString sudoersGroup READ sudoersGroup WRITE setSudoersGroup NOTIFY sudoersGroupChanged ) + public: Config( QObject* parent = nullptr ); ~Config(); @@ -43,6 +46,11 @@ public: */ QString userShell() const { return m_userShell; } + /// The group of which auto-login users must be a member + QString autologinGroup() const { return m_autologinGroup; } + /// The group of which users who can "sudo" must be a member + QString sudoersGroup() const { return m_sudoersGroup; } + public Q_SLOTS: /** @brief Sets the user's shell if possible * @@ -54,11 +62,20 @@ public Q_SLOTS: */ void setUserShell( const QString& path ); + /// Sets the autologin group; empty is ignored + void setAutologinGroup( const QString& group ); + /// Sets the sudoer group; empty is ignored + void setSudoersGroup( const QString& group ); + signals: void userShellChanged( const QString& ); + void autologinGroupChanged( const QString& ); + void sudoersGroupChanged( const QString& ); private: QString m_userShell; + QString m_autologinGroup; + QString m_sudoersGroup; }; #endif diff --git a/src/modules/users/UsersViewStep.cpp b/src/modules/users/UsersViewStep.cpp index e037902ab..745163c2c 100644 --- a/src/modules/users/UsersViewStep.cpp +++ b/src/modules/users/UsersViewStep.cpp @@ -174,20 +174,6 @@ UsersViewStep::setConfigurationMap( const QVariantMap& configurationMap ) m_defaultGroups = QStringList { "lp", "video", "network", "storage", "wheel", "audio" }; } - if ( configurationMap.contains( "autologinGroup" ) - && configurationMap.value( "autologinGroup" ).type() == QVariant::String ) - { - Calamares::JobQueue::instance()->globalStorage()->insert( - "autologinGroup", configurationMap.value( "autologinGroup" ).toString() ); - } - - if ( configurationMap.contains( "sudoersGroup" ) - && configurationMap.value( "sudoersGroup" ).type() == QVariant::String ) - { - Calamares::JobQueue::instance()->globalStorage()->insert( "sudoersGroup", - configurationMap.value( "sudoersGroup" ).toString() ); - } - bool setRootPassword = getBool( configurationMap, "setRootPassword", true ); Calamares::JobQueue::instance()->globalStorage()->insert( "setRootPassword", setRootPassword ); From 66ae1823a5ca7c99572d4f1f5cf27a143d050bce Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Sat, 25 Jul 2020 16:54:15 +0200 Subject: [PATCH 085/113] [users] Give Config object a user and login name - This is incomplete, because the business logic of guessing a login from the username is not here. --- src/modules/users/Config.cpp | 21 +++++++++++++++++++++ src/modules/users/Config.h | 17 +++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp index a071da8fc..856d27684 100644 --- a/src/modules/users/Config.cpp +++ b/src/modules/users/Config.cpp @@ -70,6 +70,27 @@ Config::setSudoersGroup( const QString& group ) } +void +Config::setLoginName( const QString& login ) +{ + if ( login != m_loginName ) + { + m_loginName = login; + emit loginNameChanged( login ); + } +} + +void +Config::setUserName( const QString& name ) +{ + if ( name != m_fullName ) + { + m_fullName = name; + emit userNameChanged( name ); + } +} + + void Config::setConfigurationMap( const QVariantMap& configurationMap ) { diff --git a/src/modules/users/Config.h b/src/modules/users/Config.h index b399c6141..32e51a309 100644 --- a/src/modules/users/Config.h +++ b/src/modules/users/Config.h @@ -33,6 +33,9 @@ class Config : public QObject Q_PROPERTY( QString autologinGroup READ autologinGroup WRITE setAutologinGroup NOTIFY autologinGroupChanged ) Q_PROPERTY( QString sudoersGroup READ sudoersGroup WRITE setSudoersGroup NOTIFY sudoersGroupChanged ) + Q_PROPERTY( QString userName READ userName WRITE setUserName NOTIFY userNameChanged ) + Q_PROPERTY( QString loginName READ loginName WRITE setLoginName NOTIFY loginNameChanged ) + public: Config( QObject* parent = nullptr ); ~Config(); @@ -51,6 +54,11 @@ public: /// The group of which users who can "sudo" must be a member QString sudoersGroup() const { return m_sudoersGroup; } + /// The full (GECOS) name of the user + QString userName() const { return m_fullName; } + /// The login name of the user + QString loginName() const { return m_loginName; } + public Q_SLOTS: /** @brief Sets the user's shell if possible * @@ -67,15 +75,24 @@ public Q_SLOTS: /// Sets the sudoer group; empty is ignored void setSudoersGroup( const QString& group ); + /// Sets the full name, may guess a loginName + void setUserName( const QString& name ); + /// Sets the login name + void setLoginName( const QString& login ); + signals: void userShellChanged( const QString& ); void autologinGroupChanged( const QString& ); void sudoersGroupChanged( const QString& ); + void userNameChanged( const QString& ); + void loginNameChanged( const QString& ); private: QString m_userShell; QString m_autologinGroup; QString m_sudoersGroup; + QString m_fullName; + QString m_loginName; }; #endif From 411a202ba5be1546e83e56faaefcff9e59444a30 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 27 Jul 2020 15:34:59 +0200 Subject: [PATCH 086/113] [users] Do some login-name guessing --- src/modules/users/Config.cpp | 41 ++++++++++++++++++++++++++++++++++++ src/modules/users/Config.h | 3 ++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp index 856d27684..ac01f9933 100644 --- a/src/modules/users/Config.cpp +++ b/src/modules/users/Config.cpp @@ -23,8 +23,11 @@ #include "GlobalStorage.h" #include "JobQueue.h" #include "utils/Logger.h" +#include "utils/String.h" #include "utils/Variant.h" +#include + Config::Config( QObject* parent ) : QObject( parent ) { @@ -75,11 +78,34 @@ Config::setLoginName( const QString& login ) { if ( login != m_loginName ) { + m_customLoginName = !login.isEmpty(); m_loginName = login; emit loginNameChanged( login ); } } +static const QRegExp USERNAME_RX( "^[a-z_][a-z0-9_-]*[$]?$" ); + +static QString +makeLoginNameSuggestion( const QStringList& parts ) +{ + if ( parts.isEmpty() ) + { + return QString(); + } + + QString usernameSuggestion = parts.first(); + for ( int i = 1; i < parts.length(); ++i ) + { + if ( !parts.value( i ).isEmpty() ) + { + usernameSuggestion.append( parts.value( i ).at( 0 ) ); + } + } + + return USERNAME_RX.indexIn( usernameSuggestion ) != -1 ? usernameSuggestion : QString(); +} + void Config::setUserName( const QString& name ) { @@ -87,6 +113,21 @@ Config::setUserName( const QString& name ) { m_fullName = name; emit userNameChanged( name ); + + // Build login and hostname, if needed + QRegExp rx( "[^a-zA-Z0-9 ]", Qt::CaseInsensitive ); + QString cleanName = CalamaresUtils::removeDiacritics( name ).toLower().replace( rx, " " ).simplified(); + QStringList cleanParts = cleanName.split( ' ' ); + + if ( !m_customLoginName ) + { + QString login = makeLoginNameSuggestion( cleanParts ); + if ( !login.isEmpty() && login != m_loginName ) + { + m_loginName = login; + emit loginNameChanged( login ); + } + } } } diff --git a/src/modules/users/Config.h b/src/modules/users/Config.h index 32e51a309..6aed5840f 100644 --- a/src/modules/users/Config.h +++ b/src/modules/users/Config.h @@ -77,7 +77,7 @@ public Q_SLOTS: /// Sets the full name, may guess a loginName void setUserName( const QString& name ); - /// Sets the login name + /// Sets the login name (flags it as "custom") void setLoginName( const QString& login ); signals: @@ -93,6 +93,7 @@ private: QString m_sudoersGroup; QString m_fullName; QString m_loginName; + bool m_customLoginName = false; }; #endif From 5ffa09000a0b880d6ebcb808cc91733d0a21e8af Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 27 Jul 2020 15:54:52 +0200 Subject: [PATCH 087/113] [users] Add hostname guessing to Config --- src/modules/users/Config.cpp | 73 ++++++++++++++++++++++++++++++++++-- src/modules/users/Config.h | 11 ++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp index ac01f9933..9deaef81f 100644 --- a/src/modules/users/Config.cpp +++ b/src/modules/users/Config.cpp @@ -26,6 +26,7 @@ #include "utils/String.h" #include "utils/Variant.h" +#include #include Config::Config( QObject* parent ) @@ -84,12 +85,56 @@ Config::setLoginName( const QString& login ) } } -static const QRegExp USERNAME_RX( "^[a-z_][a-z0-9_-]*[$]?$" ); +void +Config::setHostName( const QString& host ) +{ + if ( host != m_hostName ) + { + m_customHostName = !host.isEmpty(); + m_hostName = host; + emit hostNameChanged( host ); + } +} + + +/** @brief Guess the machine's name + * + * If there is DMI data, use that; otherwise, just call the machine "-pc". + * Reads the DMI data just once. + */ +static QString +guessProductName() +{ + static bool tried = false; + static QString dmiProduct; + + if ( !tried ) + { + // yes validateHostnameText() but these files can be a mess + QRegExp dmirx( "[^a-zA-Z0-9]", Qt::CaseInsensitive ); + QFile dmiFile( QStringLiteral( "/sys/devices/virtual/dmi/id/product_name" ) ); + + if ( dmiFile.exists() && dmiFile.open( QIODevice::ReadOnly ) ) + { + dmiProduct = QString::fromLocal8Bit( dmiFile.readAll().simplified().data() ) + .toLower() + .replace( dmirx, " " ) + .remove( ' ' ); + } + if ( dmiProduct.isEmpty() ) + { + dmiProduct = QStringLiteral( "-pc" ); + } + tried = true; + } + return dmiProduct; +} static QString makeLoginNameSuggestion( const QStringList& parts ) { - if ( parts.isEmpty() ) + static const QRegExp USERNAME_RX( "^[a-z_][a-z0-9_-]*[$]?$" ); + if ( parts.isEmpty() || parts.first().isEmpty() ) { return QString(); } @@ -106,6 +151,20 @@ makeLoginNameSuggestion( const QStringList& parts ) return USERNAME_RX.indexIn( usernameSuggestion ) != -1 ? usernameSuggestion : QString(); } +static QString +makeHostnameSuggestion( const QStringList& parts ) +{ + static const QRegExp HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" ); + if ( parts.isEmpty() || parts.first().isEmpty() ) + { + return QString(); + } + + QString productName = guessProductName(); + QString hostnameSuggestion = QStringLiteral( "%1-%2" ).arg( parts.first() ).arg( productName ); + return HOSTNAME_RX.indexIn( hostnameSuggestion ) != -1 ? hostnameSuggestion : QString(); +} + void Config::setUserName( const QString& name ) { @@ -128,10 +187,18 @@ Config::setUserName( const QString& name ) emit loginNameChanged( login ); } } + if ( !m_customHostName ) + { + QString hostname = makeHostnameSuggestion( cleanParts ); + if ( !hostname.isEmpty() && hostname != m_hostName ) + { + m_hostName = hostname; + emit hostNameChanged( hostname ); + } + } } } - void Config::setConfigurationMap( const QVariantMap& configurationMap ) { diff --git a/src/modules/users/Config.h b/src/modules/users/Config.h index 6aed5840f..59f0e8d68 100644 --- a/src/modules/users/Config.h +++ b/src/modules/users/Config.h @@ -36,6 +36,8 @@ class Config : public QObject Q_PROPERTY( QString userName READ userName WRITE setUserName NOTIFY userNameChanged ) Q_PROPERTY( QString loginName READ loginName WRITE setLoginName NOTIFY loginNameChanged ) + Q_PROPERTY( QString hostName READ hostName WRITE setHostName NOTIFY hostNameChanged ) + public: Config( QObject* parent = nullptr ); ~Config(); @@ -59,6 +61,9 @@ public: /// The login name of the user QString loginName() const { return m_loginName; } + /// The host name (name for the system) + QString hostName() const { return m_hostName; } + public Q_SLOTS: /** @brief Sets the user's shell if possible * @@ -80,12 +85,16 @@ public Q_SLOTS: /// Sets the login name (flags it as "custom") void setLoginName( const QString& login ); + /// Sets the host name (flags it as "custom") + void setHostName( const QString& host ); + signals: void userShellChanged( const QString& ); void autologinGroupChanged( const QString& ); void sudoersGroupChanged( const QString& ); void userNameChanged( const QString& ); void loginNameChanged( const QString& ); + void hostNameChanged( const QString& ); private: QString m_userShell; @@ -93,7 +102,9 @@ private: QString m_sudoersGroup; QString m_fullName; QString m_loginName; + QString m_hostName; bool m_customLoginName = false; + bool m_customHostName = false; }; #endif From 8a14cc7ffc1616dd254456190ab53e23465c6809 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 27 Jul 2020 16:09:29 +0200 Subject: [PATCH 088/113] [users] Move some configuration from Page to Config object - make the HostName textbox just a view on the Config's HostName - make the username and login textboxes view onto Config - query the Config rather than the UI for job data --- src/modules/users/Config.cpp | 2 + src/modules/users/UsersPage.cpp | 135 ++++------------------------ src/modules/users/UsersPage.h | 7 -- src/modules/users/UsersViewStep.cpp | 2 +- src/modules/users/page_usersetup.ui | 4 +- 5 files changed, 24 insertions(+), 126 deletions(-) diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp index 9deaef81f..24b14faf1 100644 --- a/src/modules/users/Config.cpp +++ b/src/modules/users/Config.cpp @@ -168,6 +168,8 @@ makeHostnameSuggestion( const QStringList& parts ) void Config::setUserName( const QString& name ) { + // TODO: handle "empty" case + // TODO: rename to "FullName" if ( name != m_fullName ) { m_fullName = name; diff --git a/src/modules/users/UsersPage.cpp b/src/modules/users/UsersPage.cpp index 62b8920e7..60afa1bb3 100644 --- a/src/modules/users/UsersPage.cpp +++ b/src/modules/users/UsersPage.cpp @@ -24,9 +24,9 @@ */ #include "UsersPage.h" - #include "ui_page_usersetup.h" +#include "Config.h" #include "CreateUserJob.h" #include "SetHostNameJob.h" #include "SetPasswordJob.h" @@ -34,7 +34,6 @@ #include "GlobalStorage.h" #include "JobQueue.h" #include "Settings.h" - #include "utils/CalamaresUtilsGui.h" #include "utils/Logger.h" #include "utils/Retranslator.h" @@ -94,8 +93,6 @@ UsersPage::UsersPage( Config* config, QWidget* parent ) // Connect signals and slots connect( ui->textBoxFullName, &QLineEdit::textEdited, this, &UsersPage::onFullNameTextEdited ); - connect( ui->textBoxUsername, &QLineEdit::textEdited, this, &UsersPage::onUsernameTextEdited ); - connect( ui->textBoxHostname, &QLineEdit::textEdited, this, &UsersPage::onHostnameTextEdited ); connect( ui->textBoxUserPassword, &QLineEdit::textChanged, this, &UsersPage::onPasswordTextChanged ); connect( ui->textBoxUserVerifiedPassword, &QLineEdit::textChanged, this, &UsersPage::onPasswordTextChanged ); connect( ui->textBoxRootPassword, &QLineEdit::textChanged, this, &UsersPage::onRootPasswordTextChanged ); @@ -124,8 +121,13 @@ UsersPage::UsersPage( Config* config, QWidget* parent ) checkReady( isReady() ); } ); - m_customUsername = false; - m_customHostname = false; + connect( ui->textBoxHostName, &QLineEdit::textEdited, config, &Config::setHostName); + connect( config, &Config::hostNameChanged, ui->textBoxHostName, &QLineEdit::setText ); + connect( config, &Config::hostNameChanged, this, &UsersPage::validateHostnameText ); + + connect( ui->textBoxLoginName, &QLineEdit::textEdited, config, &Config::setLoginName ); + connect( config, &Config::loginNameChanged, ui->textBoxLoginName, &QLineEdit::setText ); + connect( config, &Config::loginNameChanged, this, &UsersPage::validateUsernameText ); setWriteRootPassword( true ); ui->checkBoxReusePassword->setChecked( true ); @@ -147,13 +149,13 @@ UsersPage::retranslate() ui->retranslateUi( this ); if ( Calamares::Settings::instance()->isSetupMode() ) { - ui->textBoxUsername->setToolTip( tr( "If more than one person will " + ui->textBoxLoginName->setToolTip( tr( "If more than one person will " "use this computer, you can create multiple " "accounts after setup." ) ); } else { - ui->textBoxUsername->setToolTip( tr( "If more than one person will " + ui->textBoxLoginName->setToolTip( tr( "If more than one person will " "use this computer, you can create multiple " "accounts after installation." ) ); } @@ -177,12 +179,6 @@ UsersPage::isReady() return readyFields && m_readyRootPassword; } -QString -UsersPage::getHostname() const -{ - return ui->textBoxHostname->text(); -} - QString UsersPage::getRootPassword() const { @@ -206,7 +202,7 @@ UsersPage::getRootPassword() const QPair< QString, QString > UsersPage::getUserPassword() const { - return QPair< QString, QString >( ui->textBoxUsername->text(), ui->textBoxUserPassword->text() ); + return QPair< QString, QString >( m_config->loginName(), ui->textBoxUserPassword->text() ); } QList< Calamares::job_ptr > @@ -221,9 +217,9 @@ UsersPage::createJobs( const QStringList& defaultGroupsList ) Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); Calamares::Job* j; - j = new CreateUserJob( ui->textBoxUsername->text(), - ui->textBoxFullName->text().isEmpty() ? ui->textBoxUsername->text() - : ui->textBoxFullName->text(), + j = new CreateUserJob( m_config->loginName(), + m_config->userName().isEmpty() ? m_config->loginName() + : m_config->userName(), ui->checkBoxAutoLogin->isChecked(), defaultGroupsList ); list.append( Calamares::job_ptr( j ) ); @@ -232,13 +228,13 @@ UsersPage::createJobs( const QStringList& defaultGroupsList ) { gs->insert( "reuseRootPassword", ui->checkBoxReusePassword->isChecked() ); } - gs->insert( "hostname", ui->textBoxHostname->text() ); + gs->insert( "hostname", m_config->hostName() ); if ( ui->checkBoxAutoLogin->isChecked() ) { - gs->insert( "autologinUser", ui->textBoxUsername->text() ); + gs->insert( "autologinUser", m_config->loginName() ); } - gs->insert( "username", ui->textBoxUsername->text() ); + gs->insert( "username", m_config->loginName() ); gs->insert( "password", CalamaresUtils::obscure( ui->textBoxUserPassword->text() ) ); return list; @@ -269,6 +265,7 @@ UsersPage::onFullNameTextEdited( const QString& textRef ) { ui->labelFullNameError->clear(); ui->labelFullName->clear(); +#if 0 if ( !m_customUsername ) { ui->textBoxUsername->clear(); @@ -277,6 +274,7 @@ UsersPage::onFullNameTextEdited( const QString& textRef ) { ui->textBoxHostname->clear(); } +#endif m_readyFullName = false; } else @@ -284,97 +282,10 @@ UsersPage::onFullNameTextEdited( const QString& textRef ) ui->labelFullName->setPixmap( CalamaresUtils::defaultPixmap( CalamaresUtils::Yes, CalamaresUtils::Original, ui->labelFullName->size() ) ); m_readyFullName = true; - fillSuggestions(); } checkReady( isReady() ); } -/** @brief Guess the machine's name - * - * If there is DMI data, use that; otherwise, just call the machine "-pc". - * Reads the DMI data just once. - */ -static QString -guessProductName() -{ - static bool tried = false; - static QString dmiProduct; - - if ( !tried ) - { - // yes validateHostnameText() but these files can be a mess - QRegExp dmirx( "[^a-zA-Z0-9]", Qt::CaseInsensitive ); - QFile dmiFile( QStringLiteral( "/sys/devices/virtual/dmi/id/product_name" ) ); - - if ( dmiFile.exists() && dmiFile.open( QIODevice::ReadOnly ) ) - { - dmiProduct = QString::fromLocal8Bit( dmiFile.readAll().simplified().data() ) - .toLower() - .replace( dmirx, " " ) - .remove( ' ' ); - } - if ( dmiProduct.isEmpty() ) - { - dmiProduct = QStringLiteral( "-pc" ); - } - tried = true; - } - return dmiProduct; -} - -void -UsersPage::fillSuggestions() -{ - QString fullName = ui->textBoxFullName->text(); - QRegExp rx( "[^a-zA-Z0-9 ]", Qt::CaseInsensitive ); - QString cleanName = CalamaresUtils::removeDiacritics( fullName ).toLower().replace( rx, " " ).simplified(); - QStringList cleanParts = cleanName.split( ' ' ); - - if ( !m_customUsername ) - { - if ( !cleanParts.isEmpty() && !cleanParts.first().isEmpty() ) - { - QString usernameSuggestion = cleanParts.first(); - for ( int i = 1; i < cleanParts.length(); ++i ) - { - if ( !cleanParts.value( i ).isEmpty() ) - { - usernameSuggestion.append( cleanParts.value( i ).at( 0 ) ); - } - } - if ( USERNAME_RX.indexIn( usernameSuggestion ) != -1 ) - { - ui->textBoxUsername->setText( usernameSuggestion ); - validateUsernameText( usernameSuggestion ); - m_customUsername = false; - } - } - } - - if ( !m_customHostname ) - { - if ( !cleanParts.isEmpty() && !cleanParts.first().isEmpty() ) - { - QString hostnameSuggestion; - QString productName = guessProductName(); - hostnameSuggestion = QString( "%1-%2" ).arg( cleanParts.first() ).arg( productName ); - if ( HOSTNAME_RX.indexIn( hostnameSuggestion ) != -1 ) - { - ui->textBoxHostname->setText( hostnameSuggestion ); - validateHostnameText( hostnameSuggestion ); - m_customHostname = false; - } - } - } -} - - -void -UsersPage::onUsernameTextEdited( const QString& textRef ) -{ - m_customUsername = true; - validateUsernameText( textRef ); -} void @@ -427,14 +338,6 @@ UsersPage::validateUsernameText( const QString& textRef ) } -void -UsersPage::onHostnameTextEdited( const QString& textRef ) -{ - m_customHostname = true; - validateHostnameText( textRef ); -} - - void UsersPage::validateHostnameText( const QString& textRef ) { diff --git a/src/modules/users/UsersPage.h b/src/modules/users/UsersPage.h index a13886de6..ac5701b2d 100644 --- a/src/modules/users/UsersPage.h +++ b/src/modules/users/UsersPage.h @@ -65,8 +65,6 @@ public: */ void addPasswordCheck( const QString& key, const QVariant& value ); - ///@brief Hostname as entered / auto-filled - QString getHostname() const; ///@brief Root password, depends on settings, may be empty QString getRootPassword() const; ///@brief User name and password @@ -74,10 +72,7 @@ public: protected slots: void onFullNameTextEdited( const QString& ); - void fillSuggestions(); - void onUsernameTextEdited( const QString& ); void validateUsernameText( const QString& ); - void onHostnameTextEdited( const QString& ); void validateHostnameText( const QString& ); void onPasswordTextChanged( const QString& ); void onRootPasswordTextChanged( const QString& ); @@ -104,9 +99,7 @@ private: bool m_readyFullName; bool m_readyUsername; - bool m_customUsername; bool m_readyHostname; - bool m_customHostname; bool m_readyPassword; bool m_readyRootPassword; diff --git a/src/modules/users/UsersViewStep.cpp b/src/modules/users/UsersViewStep.cpp index 745163c2c..be6d61878 100644 --- a/src/modules/users/UsersViewStep.cpp +++ b/src/modules/users/UsersViewStep.cpp @@ -153,7 +153,7 @@ UsersViewStep::onLeave() j = new SetPasswordJob( "root", m_widget->getRootPassword() ); m_jobs.append( Calamares::job_ptr( j ) ); - j = new SetHostNameJob( m_widget->getHostname(), m_actions ); + j = new SetHostNameJob( m_config->hostName(), m_actions ); m_jobs.append( Calamares::job_ptr( j ) ); } diff --git a/src/modules/users/page_usersetup.ui b/src/modules/users/page_usersetup.ui index b778647d8..4aefa9981 100644 --- a/src/modules/users/page_usersetup.ui +++ b/src/modules/users/page_usersetup.ui @@ -127,7 +127,7 @@ - + 0 @@ -226,7 +226,7 @@ - + 0 From 630a50804973d103f5ed70cfeb2d2a604bc0fa82 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 27 Jul 2020 17:26:07 +0200 Subject: [PATCH 089/113] [users] Hack - create the widget anyway - since the configuration is in the UI parts, we need the widget still to load the whole configuration (until the config object is complete). Create the widget before doing configuration; this is wrong. But now we don't hit nullptr derefs all over. --- src/modules/users/UsersViewStep.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/users/UsersViewStep.cpp b/src/modules/users/UsersViewStep.cpp index be6d61878..03256d419 100644 --- a/src/modules/users/UsersViewStep.cpp +++ b/src/modules/users/UsersViewStep.cpp @@ -161,6 +161,8 @@ UsersViewStep::onLeave() void UsersViewStep::setConfigurationMap( const QVariantMap& configurationMap ) { + // Create the widget, after all .. as long as writing configuration to the UI is needed + (void)this->widget(); using CalamaresUtils::getBool; if ( configurationMap.contains( "defaultGroups" ) From d4a784f52186e5310440095a91506a9aae5ccf68 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 27 Jul 2020 17:52:46 +0200 Subject: [PATCH 090/113] [users] Hook up full name to Config --- src/modules/users/Config.cpp | 20 +++++++++++++++---- src/modules/users/Config.h | 8 ++++---- src/modules/users/UsersPage.cpp | 34 +++++++++++---------------------- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp index 24b14faf1..d4fdb16c5 100644 --- a/src/modules/users/Config.cpp +++ b/src/modules/users/Config.cpp @@ -166,14 +166,26 @@ makeHostnameSuggestion( const QStringList& parts ) } void -Config::setUserName( const QString& name ) +Config::setFullName( const QString& name ) { - // TODO: handle "empty" case - // TODO: rename to "FullName" + if ( name.isEmpty() && !m_fullName.isEmpty() ) + { + if ( !m_customHostName ) + { + setHostName( name ); + } + if ( !m_customLoginName ) + { + setLoginName( name ); + } + m_fullName = name; + emit fullNameChanged( name ); + } + if ( name != m_fullName ) { m_fullName = name; - emit userNameChanged( name ); + emit fullNameChanged( name ); // Build login and hostname, if needed QRegExp rx( "[^a-zA-Z0-9 ]", Qt::CaseInsensitive ); diff --git a/src/modules/users/Config.h b/src/modules/users/Config.h index 59f0e8d68..6a5ff0105 100644 --- a/src/modules/users/Config.h +++ b/src/modules/users/Config.h @@ -33,7 +33,7 @@ class Config : public QObject Q_PROPERTY( QString autologinGroup READ autologinGroup WRITE setAutologinGroup NOTIFY autologinGroupChanged ) Q_PROPERTY( QString sudoersGroup READ sudoersGroup WRITE setSudoersGroup NOTIFY sudoersGroupChanged ) - Q_PROPERTY( QString userName READ userName WRITE setUserName NOTIFY userNameChanged ) + Q_PROPERTY( QString fullName READ fullName WRITE setFullName NOTIFY fullNameChanged ) Q_PROPERTY( QString loginName READ loginName WRITE setLoginName NOTIFY loginNameChanged ) Q_PROPERTY( QString hostName READ hostName WRITE setHostName NOTIFY hostNameChanged ) @@ -57,7 +57,7 @@ public: QString sudoersGroup() const { return m_sudoersGroup; } /// The full (GECOS) name of the user - QString userName() const { return m_fullName; } + QString fullName() const { return m_fullName; } /// The login name of the user QString loginName() const { return m_loginName; } @@ -81,7 +81,7 @@ public Q_SLOTS: void setSudoersGroup( const QString& group ); /// Sets the full name, may guess a loginName - void setUserName( const QString& name ); + void setFullName( const QString& name ); /// Sets the login name (flags it as "custom") void setLoginName( const QString& login ); @@ -92,7 +92,7 @@ signals: void userShellChanged( const QString& ); void autologinGroupChanged( const QString& ); void sudoersGroupChanged( const QString& ); - void userNameChanged( const QString& ); + void fullNameChanged( const QString& ); void loginNameChanged( const QString& ); void hostNameChanged( const QString& ); diff --git a/src/modules/users/UsersPage.cpp b/src/modules/users/UsersPage.cpp index 60afa1bb3..ee2719091 100644 --- a/src/modules/users/UsersPage.cpp +++ b/src/modules/users/UsersPage.cpp @@ -92,7 +92,6 @@ UsersPage::UsersPage( Config* config, QWidget* parent ) ui->setupUi( this ); // Connect signals and slots - connect( ui->textBoxFullName, &QLineEdit::textEdited, this, &UsersPage::onFullNameTextEdited ); connect( ui->textBoxUserPassword, &QLineEdit::textChanged, this, &UsersPage::onPasswordTextChanged ); connect( ui->textBoxUserVerifiedPassword, &QLineEdit::textChanged, this, &UsersPage::onPasswordTextChanged ); connect( ui->textBoxRootPassword, &QLineEdit::textChanged, this, &UsersPage::onRootPasswordTextChanged ); @@ -121,7 +120,10 @@ UsersPage::UsersPage( Config* config, QWidget* parent ) checkReady( isReady() ); } ); - connect( ui->textBoxHostName, &QLineEdit::textEdited, config, &Config::setHostName); + connect( ui->textBoxFullName, &QLineEdit::textEdited, config, &Config::setFullName ); + connect( config, &Config::fullNameChanged, this, &UsersPage::onFullNameTextEdited ); + + connect( ui->textBoxHostName, &QLineEdit::textEdited, config, &Config::setHostName ); connect( config, &Config::hostNameChanged, ui->textBoxHostName, &QLineEdit::setText ); connect( config, &Config::hostNameChanged, this, &UsersPage::validateHostnameText ); @@ -150,14 +152,14 @@ UsersPage::retranslate() if ( Calamares::Settings::instance()->isSetupMode() ) { ui->textBoxLoginName->setToolTip( tr( "If more than one person will " - "use this computer, you can create multiple " - "accounts after setup." ) ); + "use this computer, you can create multiple " + "accounts after setup." ) ); } else { ui->textBoxLoginName->setToolTip( tr( "If more than one person will " - "use this computer, you can create multiple " - "accounts after installation." ) ); + "use this computer, you can create multiple " + "accounts after installation." ) ); } // Re-do password checks (with output messages) as well. // .. the password-checking methods get their values from the text boxes, @@ -218,8 +220,7 @@ UsersPage::createJobs( const QStringList& defaultGroupsList ) Calamares::Job* j; j = new CreateUserJob( m_config->loginName(), - m_config->userName().isEmpty() ? m_config->loginName() - : m_config->userName(), + m_config->fullName().isEmpty() ? m_config->loginName() : m_config->fullName(), ui->checkBoxAutoLogin->isChecked(), defaultGroupsList ); list.append( Calamares::job_ptr( j ) ); @@ -265,16 +266,6 @@ UsersPage::onFullNameTextEdited( const QString& textRef ) { ui->labelFullNameError->clear(); ui->labelFullName->clear(); -#if 0 - if ( !m_customUsername ) - { - ui->textBoxUsername->clear(); - } - if ( !m_customHostname ) - { - ui->textBoxHostname->clear(); - } -#endif m_readyFullName = false; } else @@ -287,7 +278,6 @@ UsersPage::onFullNameTextEdited( const QString& textRef ) } - void UsersPage::validateUsernameText( const QString& textRef ) { @@ -321,11 +311,9 @@ UsersPage::validateUsernameText( const QString& textRef ) tr( "Only lowercase letters, numbers, underscore and hyphen are allowed." ) ); m_readyUsername = false; } - else if ( 0 == QString::compare("root", text, Qt::CaseSensitive ) ) + else if ( 0 == QString::compare( "root", text, Qt::CaseSensitive ) ) { - labelError( ui->labelUsername, - ui->labelUsernameError, - tr( "'root' is not allowed as user name." ) ); + labelError( ui->labelUsername, ui->labelUsernameError, tr( "'root' is not allowed as user name." ) ); m_readyUsername = false; } else From a564d7a753bafd95037cf5f75f0ca4326f3acc31 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Mon, 27 Jul 2020 17:14:06 +0200 Subject: [PATCH 091/113] [users] Fix build on Linux --- src/modules/users/CreateUserJob.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/users/CreateUserJob.cpp b/src/modules/users/CreateUserJob.cpp index beb454ac0..a6812dd53 100644 --- a/src/modules/users/CreateUserJob.cpp +++ b/src/modules/users/CreateUserJob.cpp @@ -121,10 +121,10 @@ createUser( const QString& loginName, const QString& fullName, const QString& sh << "-U"; if ( !shell.isEmpty() ) { - useradd << "-s" << shell; + useraddCommand << "-s" << shell; } - useradd << "-c" << fullName; - useradd << loginName; + useraddCommand << "-c" << fullName; + useraddCommand << loginName; #endif auto commandResult = CalamaresUtils::System::instance()->targetEnvCommand( useraddCommand ); From 40d7d1baac94703063d6ef57c4d515d17373fd68 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 28 Jul 2020 10:21:23 +0200 Subject: [PATCH 092/113] [users] Move login validation to Config object - add a loginNameStatus which is a QString (empty if things are ok) stating what's wrong with the loginName, if anything. --- src/modules/users/Config.cpp | 58 ++++++++++++++++++++++++++++++--- src/modules/users/Config.h | 6 ++++ src/modules/users/UsersPage.cpp | 57 +++++++++----------------------- src/modules/users/UsersPage.h | 2 +- 4 files changed, 77 insertions(+), 46 deletions(-) diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp index d4fdb16c5..03ae60b2f 100644 --- a/src/modules/users/Config.cpp +++ b/src/modules/users/Config.cpp @@ -28,6 +28,10 @@ #include #include +#include + +static const QRegExp USERNAME_RX( "^[a-z_][a-z0-9_-]*[$]?$" ); +static constexpr const int USERNAME_MAX_LENGTH = 31; Config::Config( QObject* parent ) : QObject( parent ) @@ -49,7 +53,7 @@ Config::setUserShell( const QString& shell ) } static inline void -setGS( const QString& key, const QString& group ) +insertInGlobalStorage( const QString& key, const QString& group ) { auto* gs = Calamares::JobQueue::instance()->globalStorage(); if ( !gs || group.isEmpty() ) @@ -62,14 +66,14 @@ setGS( const QString& key, const QString& group ) void Config::setAutologinGroup( const QString& group ) { - setGS( QStringLiteral( "autologinGroup" ), group ); + insertInGlobalStorage( QStringLiteral( "autologinGroup" ), group ); emit autologinGroupChanged( group ); } void Config::setSudoersGroup( const QString& group ) { - setGS( QStringLiteral( "sudoersGroup" ), group ); + insertInGlobalStorage( QStringLiteral( "sudoersGroup" ), group ); emit sudoersGroupChanged( group ); } @@ -82,9 +86,55 @@ Config::setLoginName( const QString& login ) m_customLoginName = !login.isEmpty(); m_loginName = login; emit loginNameChanged( login ); + emit loginNameStatusChanged( loginNameStatus() ); } } +const QStringList& +Config::forbiddenLoginNames() +{ + static QStringList forbidden { "root" }; + return forbidden; +} + +QString +Config::loginNameStatus() const +{ + // An empty login is "ok", even if it isn't really + if ( m_loginName.isEmpty() ) + { + return QString(); + } + + QRegExpValidator validateEntireLoginName( USERNAME_RX ); + QRegExpValidator validateFirstLetter( QRegExp( "[a-z_].*" ) ); // anchors are implicit in QRegExpValidator + int pos = -1; + + if ( m_loginName.length() > USERNAME_MAX_LENGTH ) + { + return tr( "Your username is too long." ); + } + QString login( m_loginName ); // make a copy because validate() doesn't take const& + if ( validateFirstLetter.validate( login, pos ) == QValidator::Invalid ) + { + return tr( "Your username must start with a lowercase letter or underscore." ); + } + if ( validateEntireLoginName.validate( login, pos ) == QValidator::Invalid ) + { + return tr( "Only lowercase letters, numbers, underscore and hyphen are allowed." ); + } + + for ( const QString& badName : forbiddenLoginNames() ) + { + if ( 0 == QString::compare( badName, m_loginName, Qt::CaseSensitive ) ) + { + return tr( "'%1' is not allowed as user name." ).arg( badName ); + } + } + + return QString(); +} + void Config::setHostName( const QString& host ) { @@ -133,7 +183,6 @@ guessProductName() static QString makeLoginNameSuggestion( const QStringList& parts ) { - static const QRegExp USERNAME_RX( "^[a-z_][a-z0-9_-]*[$]?$" ); if ( parts.isEmpty() || parts.first().isEmpty() ) { return QString(); @@ -199,6 +248,7 @@ Config::setFullName( const QString& name ) { m_loginName = login; emit loginNameChanged( login ); + emit loginNameStatusChanged( loginNameStatus() ); } } if ( !m_customHostName ) diff --git a/src/modules/users/Config.h b/src/modules/users/Config.h index 6a5ff0105..91a494619 100644 --- a/src/modules/users/Config.h +++ b/src/modules/users/Config.h @@ -35,6 +35,7 @@ class Config : public QObject Q_PROPERTY( QString fullName READ fullName WRITE setFullName NOTIFY fullNameChanged ) Q_PROPERTY( QString loginName READ loginName WRITE setLoginName NOTIFY loginNameChanged ) + Q_PROPERTY( QString loginNameStatus READ loginNameStatus NOTIFY loginNameStatusChanged ) Q_PROPERTY( QString hostName READ hostName WRITE setHostName NOTIFY hostNameChanged ) @@ -60,10 +61,14 @@ public: QString fullName() const { return m_fullName; } /// The login name of the user QString loginName() const { return m_loginName; } + /// Status message about login -- empty for "ok" + QString loginNameStatus() const; /// The host name (name for the system) QString hostName() const { return m_hostName; } + static const QStringList& forbiddenLoginNames(); + public Q_SLOTS: /** @brief Sets the user's shell if possible * @@ -94,6 +99,7 @@ signals: void sudoersGroupChanged( const QString& ); void fullNameChanged( const QString& ); void loginNameChanged( const QString& ); + void loginNameStatusChanged( const QString& ); void hostNameChanged( const QString& ); private: diff --git a/src/modules/users/UsersPage.cpp b/src/modules/users/UsersPage.cpp index ee2719091..cc940502e 100644 --- a/src/modules/users/UsersPage.cpp +++ b/src/modules/users/UsersPage.cpp @@ -46,7 +46,6 @@ #include #include -static const QRegExp USERNAME_RX( "^[a-z_][a-z0-9_-]*[$]?$" ); static const QRegExp HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" ); static constexpr const int USERNAME_MAX_LENGTH = 31; static constexpr const int HOSTNAME_MIN_LENGTH = 2; @@ -129,7 +128,7 @@ UsersPage::UsersPage( Config* config, QWidget* parent ) connect( ui->textBoxLoginName, &QLineEdit::textEdited, config, &Config::setLoginName ); connect( config, &Config::loginNameChanged, ui->textBoxLoginName, &QLineEdit::setText ); - connect( config, &Config::loginNameChanged, this, &UsersPage::validateUsernameText ); + connect( config, &Config::loginNameStatusChanged, this, &UsersPage::reportLoginNameStatus ); setWriteRootPassword( true ); ui->checkBoxReusePassword->setChecked( true ); @@ -277,55 +276,31 @@ UsersPage::onFullNameTextEdited( const QString& textRef ) checkReady( isReady() ); } - void -UsersPage::validateUsernameText( const QString& textRef ) +UsersPage::reportLoginNameStatus( const QString& status ) { - QString text( textRef ); - QRegExpValidator val_whole( USERNAME_RX ); - QRegExpValidator val_start( QRegExp( "[a-z_].*" ) ); // anchors are implicit in QRegExpValidator - int pos = -1; - - if ( text.isEmpty() ) + if ( status.isEmpty() ) { - ui->labelUsernameError->clear(); - ui->labelUsername->clear(); - m_readyUsername = false; - } - else if ( text.length() > USERNAME_MAX_LENGTH ) - { - labelError( ui->labelUsername, ui->labelUsernameError, tr( "Your username is too long." ) ); - m_readyUsername = false; - } - else if ( val_start.validate( text, pos ) == QValidator::Invalid ) - { - labelError( ui->labelUsername, - ui->labelUsernameError, - tr( "Your username must start with a lowercase letter or underscore." ) ); - m_readyUsername = false; - } - else if ( val_whole.validate( text, pos ) == QValidator::Invalid ) - { - labelError( ui->labelUsername, - ui->labelUsernameError, - tr( "Only lowercase letters, numbers, underscore and hyphen are allowed." ) ); - m_readyUsername = false; - } - else if ( 0 == QString::compare( "root", text, Qt::CaseSensitive ) ) - { - labelError( ui->labelUsername, ui->labelUsernameError, tr( "'root' is not allowed as user name." ) ); - m_readyUsername = false; + if ( m_config->loginName().isEmpty() ) + { + ui->labelUsernameError->clear(); + ui->labelUsername->clear(); + m_readyUsername = false; + } + else + { + labelOk( ui->labelUsername, ui->labelUsernameError ); + m_readyUsername = true; + } } else { - labelOk( ui->labelUsername, ui->labelUsernameError ); - m_readyUsername = true; + labelError( ui->labelUsername, ui->labelUsernameError, status ); + m_readyUsername = false; } - emit checkReady( isReady() ); } - void UsersPage::validateHostnameText( const QString& textRef ) { diff --git a/src/modules/users/UsersPage.h b/src/modules/users/UsersPage.h index ac5701b2d..7cf83100c 100644 --- a/src/modules/users/UsersPage.h +++ b/src/modules/users/UsersPage.h @@ -72,7 +72,7 @@ public: protected slots: void onFullNameTextEdited( const QString& ); - void validateUsernameText( const QString& ); + void reportLoginNameStatus( const QString& ); void validateHostnameText( const QString& ); void onPasswordTextChanged( const QString& ); void onRootPasswordTextChanged( const QString& ); From 9018913af53b97acb3bc6eb1a749ec78448b84e9 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 28 Jul 2020 10:45:38 +0200 Subject: [PATCH 093/113] [users] Move hostname validation to Config --- src/modules/users/Config.cpp | 73 ++++++++++++++++++++++----- src/modules/users/Config.h | 5 ++ src/modules/users/UsersPage.cpp | 89 +++++++++++---------------------- src/modules/users/UsersPage.h | 2 +- 4 files changed, 97 insertions(+), 72 deletions(-) diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp index 03ae60b2f..bc1113672 100644 --- a/src/modules/users/Config.cpp +++ b/src/modules/users/Config.cpp @@ -33,6 +33,10 @@ static const QRegExp USERNAME_RX( "^[a-z_][a-z0-9_-]*[$]?$" ); static constexpr const int USERNAME_MAX_LENGTH = 31; +static const QRegExp HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" ); +static constexpr const int HOSTNAME_MIN_LENGTH = 2; +static constexpr const int HOSTNAME_MAX_LENGTH = 63; + Config::Config( QObject* parent ) : QObject( parent ) { @@ -106,15 +110,22 @@ Config::loginNameStatus() const return QString(); } - QRegExpValidator validateEntireLoginName( USERNAME_RX ); - QRegExpValidator validateFirstLetter( QRegExp( "[a-z_].*" ) ); // anchors are implicit in QRegExpValidator - int pos = -1; - if ( m_loginName.length() > USERNAME_MAX_LENGTH ) { return tr( "Your username is too long." ); } + for ( const QString& badName : forbiddenLoginNames() ) + { + if ( 0 == QString::compare( badName, m_loginName, Qt::CaseSensitive ) ) + { + return tr( "'%1' is not allowed as username." ).arg( badName ); + } + } + QString login( m_loginName ); // make a copy because validate() doesn't take const& + QRegExpValidator validateEntireLoginName( USERNAME_RX ); + QRegExpValidator validateFirstLetter( QRegExp( "[a-z_].*" ) ); // anchors are implicit in QRegExpValidator + int pos = -1; if ( validateFirstLetter.validate( login, pos ) == QValidator::Invalid ) { return tr( "Your username must start with a lowercase letter or underscore." ); @@ -124,14 +135,6 @@ Config::loginNameStatus() const return tr( "Only lowercase letters, numbers, underscore and hyphen are allowed." ); } - for ( const QString& badName : forbiddenLoginNames() ) - { - if ( 0 == QString::compare( badName, m_loginName, Qt::CaseSensitive ) ) - { - return tr( "'%1' is not allowed as user name." ).arg( badName ); - } - } - return QString(); } @@ -143,9 +146,54 @@ Config::setHostName( const QString& host ) m_customHostName = !host.isEmpty(); m_hostName = host; emit hostNameChanged( host ); + emit hostNameStatusChanged( hostNameStatus() ); } } +const QStringList& +Config::forbiddenHostNames() +{ + static QStringList forbidden { "localhost" }; + return forbidden; +} + +QString +Config::hostNameStatus() const +{ + // An empty hostname is "ok", even if it isn't really + if ( m_hostName.isEmpty() ) + { + return QString(); + } + + if ( m_hostName.length() < HOSTNAME_MIN_LENGTH ) + { + return tr( "Your hostname is too short." ); + } + if ( m_hostName.length() > HOSTNAME_MAX_LENGTH ) + { + return tr( "Your hostname is too long." ); + } + for ( const QString& badName : forbiddenHostNames() ) + { + if ( 0 == QString::compare( badName, m_hostName, Qt::CaseSensitive ) ) + { + return tr( "'%1' is not allowed as hostname." ).arg( badName ); + } + } + + QString text = m_hostName; + QRegExpValidator val( HOSTNAME_RX ); + int pos = -1; + + if ( val.validate( text, pos ) == QValidator::Invalid ) + { + return tr( "Only letters, numbers, underscore and hyphen are allowed." ); + } + + return QString(); +} + /** @brief Guess the machine's name * @@ -258,6 +306,7 @@ Config::setFullName( const QString& name ) { m_hostName = hostname; emit hostNameChanged( hostname ); + emit hostNameStatusChanged( hostNameStatus() ); } } } diff --git a/src/modules/users/Config.h b/src/modules/users/Config.h index 91a494619..824b70ba8 100644 --- a/src/modules/users/Config.h +++ b/src/modules/users/Config.h @@ -38,6 +38,7 @@ class Config : public QObject Q_PROPERTY( QString loginNameStatus READ loginNameStatus NOTIFY loginNameStatusChanged ) Q_PROPERTY( QString hostName READ hostName WRITE setHostName NOTIFY hostNameChanged ) + Q_PROPERTY( QString hostNameStatus READ hostNameStatus NOTIFY hostNameStatusChanged ) public: Config( QObject* parent = nullptr ); @@ -66,8 +67,11 @@ public: /// The host name (name for the system) QString hostName() const { return m_hostName; } + /// Status message about hostname -- empty for "ok" + QString hostNameStatus() const; static const QStringList& forbiddenLoginNames(); + static const QStringList& forbiddenHostNames(); public Q_SLOTS: /** @brief Sets the user's shell if possible @@ -101,6 +105,7 @@ signals: void loginNameChanged( const QString& ); void loginNameStatusChanged( const QString& ); void hostNameChanged( const QString& ); + void hostNameStatusChanged( const QString& ); private: QString m_userShell; diff --git a/src/modules/users/UsersPage.cpp b/src/modules/users/UsersPage.cpp index cc940502e..7983e29ea 100644 --- a/src/modules/users/UsersPage.cpp +++ b/src/modules/users/UsersPage.cpp @@ -46,11 +46,6 @@ #include #include -static const QRegExp HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" ); -static constexpr const int USERNAME_MAX_LENGTH = 31; -static constexpr const int HOSTNAME_MIN_LENGTH = 2; -static constexpr const int HOSTNAME_MAX_LENGTH = 63; - /** @brief How bad is the error for labelError() ? */ enum class Badness { @@ -77,6 +72,32 @@ labelOk( QLabel* pix, QLabel* label ) pix->setPixmap( CalamaresUtils::defaultPixmap( CalamaresUtils::Yes, CalamaresUtils::Original, label->size() ) ); } +/** Indicate error, update @p ok based on @p status */ +static inline void +labelStatus( QLabel* pix, QLabel* label, const QString& value, const QString& status, bool& ok ) +{ + if ( status.isEmpty() ) + { + if ( value.isEmpty() ) + { + // This is different from labelOK() because no checkmark is shown + label->clear(); + pix->clear(); + ok = false; + } + else + { + labelOk( pix, label ); + ok = true; + } + } + else + { + labelError( pix, label, status ); + ok = false; + } +} + UsersPage::UsersPage( Config* config, QWidget* parent ) : QWidget( parent ) , ui( new Ui::Page_UserSetup ) @@ -124,7 +145,7 @@ UsersPage::UsersPage( Config* config, QWidget* parent ) connect( ui->textBoxHostName, &QLineEdit::textEdited, config, &Config::setHostName ); connect( config, &Config::hostNameChanged, ui->textBoxHostName, &QLineEdit::setText ); - connect( config, &Config::hostNameChanged, this, &UsersPage::validateHostnameText ); + connect( config, &Config::hostNameStatusChanged, this, &UsersPage::reportHostNameStatus ); connect( ui->textBoxLoginName, &QLineEdit::textEdited, config, &Config::setLoginName ); connect( config, &Config::loginNameChanged, ui->textBoxLoginName, &QLineEdit::setText ); @@ -279,64 +300,14 @@ UsersPage::onFullNameTextEdited( const QString& textRef ) void UsersPage::reportLoginNameStatus( const QString& status ) { - if ( status.isEmpty() ) - { - if ( m_config->loginName().isEmpty() ) - { - ui->labelUsernameError->clear(); - ui->labelUsername->clear(); - m_readyUsername = false; - } - else - { - labelOk( ui->labelUsername, ui->labelUsernameError ); - m_readyUsername = true; - } - } - else - { - labelError( ui->labelUsername, ui->labelUsernameError, status ); - m_readyUsername = false; - } + labelStatus( ui->labelUsername, ui->labelUsernameError, m_config->loginName(), status, m_readyUsername ); emit checkReady( isReady() ); } void -UsersPage::validateHostnameText( const QString& textRef ) +UsersPage::reportHostNameStatus( const QString& status ) { - QString text = textRef; - QRegExpValidator val( HOSTNAME_RX ); - int pos = -1; - - if ( text.isEmpty() ) - { - ui->labelHostnameError->clear(); - ui->labelHostname->clear(); - m_readyHostname = false; - } - else if ( text.length() < HOSTNAME_MIN_LENGTH ) - { - labelError( ui->labelHostname, ui->labelHostnameError, tr( "Your hostname is too short." ) ); - m_readyHostname = false; - } - else if ( text.length() > HOSTNAME_MAX_LENGTH ) - { - labelError( ui->labelHostname, ui->labelHostnameError, tr( "Your hostname is too long." ) ); - m_readyHostname = false; - } - else if ( val.validate( text, pos ) == QValidator::Invalid ) - { - labelError( ui->labelHostname, - ui->labelHostnameError, - tr( "Only letters, numbers, underscore and hyphen are allowed." ) ); - m_readyHostname = false; - } - else - { - labelOk( ui->labelHostname, ui->labelHostnameError ); - m_readyHostname = true; - } - + labelStatus( ui->labelHostname, ui->labelHostnameError, m_config->hostName(), status, m_readyHostname ); emit checkReady( isReady() ); } diff --git a/src/modules/users/UsersPage.h b/src/modules/users/UsersPage.h index 7cf83100c..7e0830dc0 100644 --- a/src/modules/users/UsersPage.h +++ b/src/modules/users/UsersPage.h @@ -73,7 +73,7 @@ public: protected slots: void onFullNameTextEdited( const QString& ); void reportLoginNameStatus( const QString& ); - void validateHostnameText( const QString& ); + void reportHostNameStatus( const QString& ); void onPasswordTextChanged( const QString& ); void onRootPasswordTextChanged( const QString& ); From 0813ec33278381cf3bc4bcd0f3508a3514699001 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 28 Jul 2020 10:49:12 +0200 Subject: [PATCH 094/113] [users] Misc cleanups - unused includes - avoid "my--pc" .. the dash is inserted by makeHostnameSuggestion() --- src/modules/users/Config.cpp | 2 +- src/modules/users/UsersPage.cpp | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp index bc1113672..9a9ebe693 100644 --- a/src/modules/users/Config.cpp +++ b/src/modules/users/Config.cpp @@ -221,7 +221,7 @@ guessProductName() } if ( dmiProduct.isEmpty() ) { - dmiProduct = QStringLiteral( "-pc" ); + dmiProduct = QStringLiteral( "pc" ); } tried = true; } diff --git a/src/modules/users/UsersPage.cpp b/src/modules/users/UsersPage.cpp index 7983e29ea..af5a1391e 100644 --- a/src/modules/users/UsersPage.cpp +++ b/src/modules/users/UsersPage.cpp @@ -43,8 +43,6 @@ #include #include #include -#include -#include /** @brief How bad is the error for labelError() ? */ enum class Badness From 6c930af5cbe760305fb559874043d6da4f7af02e Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 28 Jul 2020 11:18:07 +0200 Subject: [PATCH 095/113] [users] Use convenience method for labeling Full Name --- src/modules/users/UsersPage.cpp | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/modules/users/UsersPage.cpp b/src/modules/users/UsersPage.cpp index af5a1391e..0877bf0e3 100644 --- a/src/modules/users/UsersPage.cpp +++ b/src/modules/users/UsersPage.cpp @@ -278,20 +278,9 @@ UsersPage::setWriteRootPassword( bool write ) void -UsersPage::onFullNameTextEdited( const QString& textRef ) +UsersPage::onFullNameTextEdited( const QString& fullName ) { - if ( textRef.isEmpty() ) - { - ui->labelFullNameError->clear(); - ui->labelFullName->clear(); - m_readyFullName = false; - } - else - { - ui->labelFullName->setPixmap( - CalamaresUtils::defaultPixmap( CalamaresUtils::Yes, CalamaresUtils::Original, ui->labelFullName->size() ) ); - m_readyFullName = true; - } + labelStatus( ui->labelFullName, ui->labelFullNameError, fullName, QString(), m_readyFullName ); checkReady( isReady() ); } From 45b71c24e771d69d22699337bbd030590eb1ed4f Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 28 Jul 2020 11:41:52 +0200 Subject: [PATCH 096/113] [users] Move autologin setting to Config --- src/modules/users/Config.cpp | 12 ++++++++++++ src/modules/users/Config.h | 11 +++++++++++ src/modules/users/UsersPage.cpp | 16 +++++++--------- src/modules/users/UsersPage.h | 1 - src/modules/users/UsersViewStep.cpp | 1 - src/modules/users/page_usersetup.ui | 2 +- 6 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp index 9a9ebe693..f245aa866 100644 --- a/src/modules/users/Config.cpp +++ b/src/modules/users/Config.cpp @@ -312,6 +312,16 @@ Config::setFullName( const QString& name ) } } +void +Config::setAutoLogin( bool b ) +{ + if ( b != m_doAutoLogin ) + { + m_doAutoLogin = b; + emit autoLoginChanged( b ); + } +} + void Config::setConfigurationMap( const QVariantMap& configurationMap ) { @@ -325,4 +335,6 @@ Config::setConfigurationMap( const QVariantMap& configurationMap ) setAutologinGroup( CalamaresUtils::getString( configurationMap, "autologinGroup" ) ); setSudoersGroup( CalamaresUtils::getString( configurationMap, "sudoersGroup" ) ); + + m_doAutoLogin = CalamaresUtils::getBool( configurationMap, "doAutologin", false ); } diff --git a/src/modules/users/Config.h b/src/modules/users/Config.h index 824b70ba8..e7ab8a736 100644 --- a/src/modules/users/Config.h +++ b/src/modules/users/Config.h @@ -33,6 +33,8 @@ class Config : public QObject Q_PROPERTY( QString autologinGroup READ autologinGroup WRITE setAutologinGroup NOTIFY autologinGroupChanged ) Q_PROPERTY( QString sudoersGroup READ sudoersGroup WRITE setSudoersGroup NOTIFY sudoersGroupChanged ) + Q_PROPERTY( bool doAutoLogin READ doAutoLogin WRITE setAutoLogin NOTIFY autoLoginChanged ) + Q_PROPERTY( QString fullName READ fullName WRITE setFullName NOTIFY fullNameChanged ) Q_PROPERTY( QString loginName READ loginName WRITE setLoginName NOTIFY loginNameChanged ) Q_PROPERTY( QString loginNameStatus READ loginNameStatus NOTIFY loginNameStatusChanged ) @@ -70,6 +72,9 @@ public: /// Status message about hostname -- empty for "ok" QString hostNameStatus() const; + /// Should the user be automatically logged-in? + bool doAutoLogin() const { return m_doAutoLogin; } + static const QStringList& forbiddenLoginNames(); static const QStringList& forbiddenHostNames(); @@ -97,6 +102,9 @@ public Q_SLOTS: /// Sets the host name (flags it as "custom") void setHostName( const QString& host ); + /// Sets the autologin flag + void setAutoLogin( bool b ); + signals: void userShellChanged( const QString& ); void autologinGroupChanged( const QString& ); @@ -106,6 +114,7 @@ signals: void loginNameStatusChanged( const QString& ); void hostNameChanged( const QString& ); void hostNameStatusChanged( const QString& ); + void autoLoginChanged( bool ); private: QString m_userShell; @@ -114,6 +123,8 @@ private: QString m_fullName; QString m_loginName; QString m_hostName; + bool m_doAutoLogin = false; + bool m_customLoginName = false; bool m_customHostName = false; }; diff --git a/src/modules/users/UsersPage.cpp b/src/modules/users/UsersPage.cpp index 0877bf0e3..c9eb227e7 100644 --- a/src/modules/users/UsersPage.cpp +++ b/src/modules/users/UsersPage.cpp @@ -149,6 +149,11 @@ UsersPage::UsersPage( Config* config, QWidget* parent ) connect( config, &Config::loginNameChanged, ui->textBoxLoginName, &QLineEdit::setText ); connect( config, &Config::loginNameStatusChanged, this, &UsersPage::reportLoginNameStatus ); + connect( ui->checkBoxDoAutoLogin, &QCheckBox::stateChanged, this, [this]( int checked ) { + m_config->setAutoLogin( checked != Qt::Unchecked ); + } ); + connect( config, &Config::autoLoginChanged, ui->checkBoxDoAutoLogin, &QCheckBox::setChecked ); + setWriteRootPassword( true ); ui->checkBoxReusePassword->setChecked( true ); ui->checkBoxValidatePassword->setChecked( true ); @@ -239,7 +244,7 @@ UsersPage::createJobs( const QStringList& defaultGroupsList ) Calamares::Job* j; j = new CreateUserJob( m_config->loginName(), m_config->fullName().isEmpty() ? m_config->loginName() : m_config->fullName(), - ui->checkBoxAutoLogin->isChecked(), + m_config->doAutoLogin(), defaultGroupsList ); list.append( Calamares::job_ptr( j ) ); @@ -248,7 +253,7 @@ UsersPage::createJobs( const QStringList& defaultGroupsList ) gs->insert( "reuseRootPassword", ui->checkBoxReusePassword->isChecked() ); } gs->insert( "hostname", m_config->hostName() ); - if ( ui->checkBoxAutoLogin->isChecked() ) + if ( m_config->doAutoLogin() ) { gs->insert( "autologinUser", m_config->loginName() ); } @@ -378,13 +383,6 @@ UsersPage::setValidatePasswordDefault( bool checked ) emit checkReady( isReady() ); } -void -UsersPage::setAutologinDefault( bool checked ) -{ - ui->checkBoxAutoLogin->setChecked( checked ); - emit checkReady( isReady() ); -} - void UsersPage::setReusePasswordDefault( bool checked ) { diff --git a/src/modules/users/UsersPage.h b/src/modules/users/UsersPage.h index 7e0830dc0..7cd522498 100644 --- a/src/modules/users/UsersPage.h +++ b/src/modules/users/UsersPage.h @@ -54,7 +54,6 @@ public: void setWriteRootPassword( bool show ); void setPasswordCheckboxVisible( bool visible ); void setValidatePasswordDefault( bool checked ); - void setAutologinDefault( bool checked ); void setReusePasswordDefault( bool checked ); /** @brief Process entries in the passwordRequirements config entry diff --git a/src/modules/users/UsersViewStep.cpp b/src/modules/users/UsersViewStep.cpp index 03256d419..0826b8a28 100644 --- a/src/modules/users/UsersViewStep.cpp +++ b/src/modules/users/UsersViewStep.cpp @@ -180,7 +180,6 @@ UsersViewStep::setConfigurationMap( const QVariantMap& configurationMap ) Calamares::JobQueue::instance()->globalStorage()->insert( "setRootPassword", setRootPassword ); m_widget->setWriteRootPassword( setRootPassword ); - m_widget->setAutologinDefault( getBool( configurationMap, "doAutologin", false ) ); m_widget->setReusePasswordDefault( getBool( configurationMap, "doReusePassword", false ) ); if ( configurationMap.contains( "passwordRequirements" ) diff --git a/src/modules/users/page_usersetup.ui b/src/modules/users/page_usersetup.ui index 4aefa9981..d880673b8 100644 --- a/src/modules/users/page_usersetup.ui +++ b/src/modules/users/page_usersetup.ui @@ -456,7 +456,7 @@ - + Log in automatically without asking for the password. From 6a03bcb25e19285b0aa8432857c8d6f11db1ab27 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 28 Jul 2020 11:59:53 +0200 Subject: [PATCH 097/113] [users] Move setRootPassword to Config - this really controls whether a root password is written during installtion, so rename to writeRootPassword in the code. --- src/modules/users/Config.cpp | 3 +++ src/modules/users/Config.h | 3 +++ src/modules/users/UsersPage.cpp | 42 ++++++++++------------------- src/modules/users/UsersPage.h | 3 --- src/modules/users/UsersViewStep.cpp | 4 --- 5 files changed, 20 insertions(+), 35 deletions(-) diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp index f245aa866..1219c2a3c 100644 --- a/src/modules/users/Config.cpp +++ b/src/modules/users/Config.cpp @@ -337,4 +337,7 @@ Config::setConfigurationMap( const QVariantMap& configurationMap ) setSudoersGroup( CalamaresUtils::getString( configurationMap, "sudoersGroup" ) ); m_doAutoLogin = CalamaresUtils::getBool( configurationMap, "doAutologin", false ); + + m_writeRootPassword = CalamaresUtils::getBool( configurationMap, "setRootPassword", true ); + Calamares::JobQueue::instance()->globalStorage()->insert( "setRootPassword", m_writeRootPassword ); } diff --git a/src/modules/users/Config.h b/src/modules/users/Config.h index e7ab8a736..d32ddc8f2 100644 --- a/src/modules/users/Config.h +++ b/src/modules/users/Config.h @@ -74,6 +74,8 @@ public: /// Should the user be automatically logged-in? bool doAutoLogin() const { return m_doAutoLogin; } + /// Should the root password be written (if false, no password is set and the root account is disabled for login) + bool writeRootPassword() const { return m_writeRootPassword; } static const QStringList& forbiddenLoginNames(); static const QStringList& forbiddenHostNames(); @@ -124,6 +126,7 @@ private: QString m_loginName; QString m_hostName; bool m_doAutoLogin = false; + bool m_writeRootPassword = true; bool m_customLoginName = false; bool m_customHostName = false; diff --git a/src/modules/users/UsersPage.cpp b/src/modules/users/UsersPage.cpp index c9eb227e7..268e6099e 100644 --- a/src/modules/users/UsersPage.cpp +++ b/src/modules/users/UsersPage.cpp @@ -105,7 +105,6 @@ UsersPage::UsersPage( Config* config, QWidget* parent ) , m_readyHostname( false ) , m_readyPassword( false ) , m_readyRootPassword( false ) - , m_writeRootPassword( true ) { ui->setupUi( this ); @@ -119,22 +118,19 @@ UsersPage::UsersPage( Config* config, QWidget* parent ) onRootPasswordTextChanged( ui->textBoxRootPassword->text() ); checkReady( isReady() ); } ); - connect( ui->checkBoxReusePassword, &QCheckBox::stateChanged, this, [this]( int checked ) { + connect( ui->checkBoxReusePassword, &QCheckBox::stateChanged, this, [this]( const int checked ) { /* When "reuse" is checked, hide the fields for explicitly * entering the root password. However, if we're going to * disable the root password anyway, hide them all regardless of * the checkbox -- so when writeRoot is false, checked needs * to be true, to hide them all. */ - if ( !m_writeRootPassword ) - { - checked = true; - } - ui->labelChooseRootPassword->setVisible( !checked ); - ui->labelRootPassword->setVisible( !checked ); - ui->labelRootPasswordError->setVisible( !checked ); - ui->textBoxRootPassword->setVisible( !checked ); - ui->textBoxVerifiedRootPassword->setVisible( !checked ); + const bool visible = m_config->writeRootPassword() ? !checked : false; + ui->labelChooseRootPassword->setVisible( visible ); + ui->labelRootPassword->setVisible( visible ); + ui->labelRootPasswordError->setVisible( visible ); + ui->textBoxRootPassword->setVisible( visible ); + ui->textBoxVerifiedRootPassword->setVisible( visible ); checkReady( isReady() ); } ); @@ -154,7 +150,7 @@ UsersPage::UsersPage( Config* config, QWidget* parent ) } ); connect( config, &Config::autoLoginChanged, ui->checkBoxDoAutoLogin, &QCheckBox::setChecked ); - setWriteRootPassword( true ); + ui->checkBoxReusePassword->setVisible( m_config->writeRootPassword() ); ui->checkBoxReusePassword->setChecked( true ); ui->checkBoxValidatePassword->setChecked( true ); @@ -196,18 +192,16 @@ bool UsersPage::isReady() { bool readyFields = m_readyFullName && m_readyHostname && m_readyPassword && m_readyUsername; - if ( !m_writeRootPassword || ui->checkBoxReusePassword->isChecked() ) - { - return readyFields; - } - - return readyFields && m_readyRootPassword; + // If we're going to write a root password, we need a valid one (or reuse the user's password) + readyFields + &= m_config->writeRootPassword() ? ( m_readyRootPassword || ui->checkBoxReusePassword->isChecked() ) : true; + return readyFields; } QString UsersPage::getRootPassword() const { - if ( m_writeRootPassword ) + if ( m_config->writeRootPassword() ) { if ( ui->checkBoxReusePassword->isChecked() ) { @@ -248,7 +242,7 @@ UsersPage::createJobs( const QStringList& defaultGroupsList ) defaultGroupsList ); list.append( Calamares::job_ptr( j ) ); - if ( m_writeRootPassword ) + if ( m_config->writeRootPassword() ) { gs->insert( "reuseRootPassword", ui->checkBoxReusePassword->isChecked() ); } @@ -274,14 +268,6 @@ UsersPage::onActivate() } -void -UsersPage::setWriteRootPassword( bool write ) -{ - m_writeRootPassword = write; - ui->checkBoxReusePassword->setVisible( write ); -} - - void UsersPage::onFullNameTextEdited( const QString& fullName ) { diff --git a/src/modules/users/UsersPage.h b/src/modules/users/UsersPage.h index 7cd522498..35f7f0938 100644 --- a/src/modules/users/UsersPage.h +++ b/src/modules/users/UsersPage.h @@ -51,7 +51,6 @@ public: void onActivate(); - void setWriteRootPassword( bool show ); void setPasswordCheckboxVisible( bool visible ); void setValidatePasswordDefault( bool checked ); void setReusePasswordDefault( bool checked ); @@ -101,8 +100,6 @@ private: bool m_readyHostname; bool m_readyPassword; bool m_readyRootPassword; - - bool m_writeRootPassword; }; #endif // USERSPAGE_H diff --git a/src/modules/users/UsersViewStep.cpp b/src/modules/users/UsersViewStep.cpp index 0826b8a28..713811246 100644 --- a/src/modules/users/UsersViewStep.cpp +++ b/src/modules/users/UsersViewStep.cpp @@ -176,10 +176,6 @@ UsersViewStep::setConfigurationMap( const QVariantMap& configurationMap ) m_defaultGroups = QStringList { "lp", "video", "network", "storage", "wheel", "audio" }; } - bool setRootPassword = getBool( configurationMap, "setRootPassword", true ); - Calamares::JobQueue::instance()->globalStorage()->insert( "setRootPassword", setRootPassword ); - - m_widget->setWriteRootPassword( setRootPassword ); m_widget->setReusePasswordDefault( getBool( configurationMap, "doReusePassword", false ) ); if ( configurationMap.contains( "passwordRequirements" ) From cc2e3f79ff045cd577ebc28e8f89c19350aadd58 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 28 Jul 2020 12:16:03 +0200 Subject: [PATCH 098/113] [users] Move job creation from widget to viewstep - This is a half-step: the ViewStep shouldn't do job creation either, eventually it needs to be the Config object, but this is better than asking the widget (UI) to create some jobs. - When updating login- or host-name, or the autologin setting, set it in GS as well. This is a minor improvement over doing it only when leaving the page. - Since the Config object isn't complete, there are leftovers in the widget, which has a fillGlobalStorage() for the not-jobs-related bits previously in createJobs(). --- src/modules/users/Config.cpp | 29 ++++++++++++++++++++++++++++ src/modules/users/UsersPage.cpp | 30 +++++------------------------ src/modules/users/UsersPage.h | 5 ++--- src/modules/users/UsersViewStep.cpp | 10 ++++++++-- 4 files changed, 44 insertions(+), 30 deletions(-) diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp index 1219c2a3c..bb739cbd1 100644 --- a/src/modules/users/Config.cpp +++ b/src/modules/users/Config.cpp @@ -87,6 +87,16 @@ Config::setLoginName( const QString& login ) { if ( login != m_loginName ) { + Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); + if ( login.isEmpty() ) + { + gs->remove( "username" ); + } + else + { + gs->insert( "username", login ); + } + m_customLoginName = !login.isEmpty(); m_loginName = login; emit loginNameChanged( login ); @@ -143,6 +153,16 @@ Config::setHostName( const QString& host ) { if ( host != m_hostName ) { + Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); + if ( host.isEmpty() ) + { + gs->remove( "hostname" ); + } + else + { + gs->insert( "hostname", host ); + } + m_customHostName = !host.isEmpty(); m_hostName = host; emit hostNameChanged( host ); @@ -317,6 +337,15 @@ Config::setAutoLogin( bool b ) { if ( b != m_doAutoLogin ) { + Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); + if ( b ) + { + gs->insert( "autologinUser", loginName() ); + } + else + { + gs->remove( "autologinUser" ); + } m_doAutoLogin = b; emit autoLoginChanged( b ); } diff --git a/src/modules/users/UsersPage.cpp b/src/modules/users/UsersPage.cpp index 268e6099e..c4502256a 100644 --- a/src/modules/users/UsersPage.cpp +++ b/src/modules/users/UsersPage.cpp @@ -24,12 +24,9 @@ */ #include "UsersPage.h" -#include "ui_page_usersetup.h" #include "Config.h" -#include "CreateUserJob.h" -#include "SetHostNameJob.h" -#include "SetPasswordJob.h" +#include "ui_page_usersetup.h" #include "GlobalStorage.h" #include "JobQueue.h" @@ -189,7 +186,7 @@ UsersPage::retranslate() bool -UsersPage::isReady() +UsersPage::isReady() const { bool readyFields = m_readyFullName && m_readyHostname && m_readyPassword && m_readyUsername; // If we're going to write a root password, we need a valid one (or reuse the user's password) @@ -224,38 +221,21 @@ UsersPage::getUserPassword() const return QPair< QString, QString >( m_config->loginName(), ui->textBoxUserPassword->text() ); } -QList< Calamares::job_ptr > -UsersPage::createJobs( const QStringList& defaultGroupsList ) +void +UsersPage::fillGlobalStorage() const { - QList< Calamares::job_ptr > list; if ( !isReady() ) { - return list; + return; } Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); - Calamares::Job* j; - j = new CreateUserJob( m_config->loginName(), - m_config->fullName().isEmpty() ? m_config->loginName() : m_config->fullName(), - m_config->doAutoLogin(), - defaultGroupsList ); - list.append( Calamares::job_ptr( j ) ); - if ( m_config->writeRootPassword() ) { gs->insert( "reuseRootPassword", ui->checkBoxReusePassword->isChecked() ); } - gs->insert( "hostname", m_config->hostName() ); - if ( m_config->doAutoLogin() ) - { - gs->insert( "autologinUser", m_config->loginName() ); - } - - gs->insert( "username", m_config->loginName() ); gs->insert( "password", CalamaresUtils::obscure( ui->textBoxUserPassword->text() ) ); - - return list; } diff --git a/src/modules/users/UsersPage.h b/src/modules/users/UsersPage.h index 35f7f0938..b8cb0f06a 100644 --- a/src/modules/users/UsersPage.h +++ b/src/modules/users/UsersPage.h @@ -25,7 +25,6 @@ #define USERSPAGE_H #include "CheckPWQuality.h" -#include "Job.h" #include @@ -45,9 +44,9 @@ public: explicit UsersPage( Config* config, QWidget* parent = nullptr ); virtual ~UsersPage(); - bool isReady(); + bool isReady() const; - Calamares::JobList createJobs( const QStringList& defaultGroupsList ); + void fillGlobalStorage() const; void onActivate(); diff --git a/src/modules/users/UsersViewStep.cpp b/src/modules/users/UsersViewStep.cpp index 713811246..ddaf4837b 100644 --- a/src/modules/users/UsersViewStep.cpp +++ b/src/modules/users/UsersViewStep.cpp @@ -21,6 +21,7 @@ #include "UsersViewStep.h" #include "Config.h" +#include "CreateUserJob.h" #include "SetHostNameJob.h" #include "SetPasswordJob.h" #include "UsersPage.h" @@ -138,13 +139,16 @@ void UsersViewStep::onLeave() { m_jobs.clear(); - if ( !m_widget ) + if ( !m_widget || !m_widget->isReady() ) { return; } - m_jobs.append( m_widget->createJobs( m_defaultGroups ) ); Calamares::Job* j; + j = new CreateUserJob( m_config->loginName(), + m_config->fullName().isEmpty() ? m_config->loginName() : m_config->fullName(), + m_config->doAutoLogin(), + m_defaultGroups ); auto userPW = m_widget->getUserPassword(); j = new SetPasswordJob( userPW.first, userPW.second ); @@ -155,6 +159,8 @@ UsersViewStep::onLeave() j = new SetHostNameJob( m_config->hostName(), m_actions ); m_jobs.append( Calamares::job_ptr( j ) ); + + m_widget->fillGlobalStorage(); } From b06498194e177a3067cb2d7d22bda6e8c5a8eb5e Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 28 Jul 2020 14:46:56 +0200 Subject: [PATCH 099/113] [machineid] Fix up schema - schema didn't allow recent (2019) configuration entries - remove mention of deprecated key from example config --- src/modules/machineid/machineid.conf | 2 -- src/modules/machineid/machineid.schema.yaml | 10 +++++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/modules/machineid/machineid.conf b/src/modules/machineid/machineid.conf index 97bd10a06..fa42655fd 100644 --- a/src/modules/machineid/machineid.conf +++ b/src/modules/machineid/machineid.conf @@ -15,8 +15,6 @@ dbus: true # Whether /var/lib/dbus/machine-id should be a symlink to /etc/machine-id # (ignored if dbus is false, or if there is no /etc/machine-id to point to). dbus-symlink: true -# this is a deprecated form of *dbus-symlink* -symlink: true # Whether to create an entropy file entropy: false diff --git a/src/modules/machineid/machineid.schema.yaml b/src/modules/machineid/machineid.schema.yaml index 588a7fa4e..c5eb55b1b 100644 --- a/src/modules/machineid/machineid.schema.yaml +++ b/src/modules/machineid/machineid.schema.yaml @@ -4,6 +4,10 @@ $id: https://calamares.io/schemas/machineid additionalProperties: false type: object properties: - "systemd": { type: boolean, default: true } - "dbus": { type: boolean, default: true } - "symlink": { type: boolean, default: true } + systemd: { type: boolean, default: true } + dbus: { type: boolean, default: true } + "dbus-symlink": { type: boolean, default: true } + entropy: { type: boolean, default: false } + "entropy-copy": { type: boolean, default: false } + # Deprecated properties + symlink: { type: boolean, default: true } From 9568fc082ff8e76d56cd6cb8548a50cbb01fdc6f Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 28 Jul 2020 14:59:36 +0200 Subject: [PATCH 100/113] [calamares] Try to reduce compile-churn with version header - Very rarely do we need the full-git-version of Calamares, so split that into a separate header with a little trickery. - In the "normal" version header, drop the full-git-version values. --- CMakeLists.txt | 6 +++++- src/calamares/CalamaresVersion.h.in | 2 +- src/calamares/CalamaresVersionX.h.in | 13 +++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 src/calamares/CalamaresVersionX.h.in diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b6f03fea..43ca1bc56 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -537,7 +537,11 @@ if( CALAMARES_VERSION_RC ) set( CALAMARES_VERSION ${CALAMARES_VERSION}rc${CALAMARES_VERSION_RC} ) endif() -# additional info for non-release builds +# Additional info for non-release builds. The "extended" version information +# with date and git information (commit, dirty status) is used only +# by CalamaresVersionX.h, which is included by consumers that need a full +# version number with all that information; normal consumers can include +# CalamaresVersion.h with more stable numbers. if( NOT BUILD_RELEASE AND EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.git/" ) include( CMakeDateStamp ) set( CALAMARES_VERSION_DATE "${CMAKE_DATESTAMP_YEAR}${CMAKE_DATESTAMP_MONTH}${CMAKE_DATESTAMP_DAY}" ) diff --git a/src/calamares/CalamaresVersion.h.in b/src/calamares/CalamaresVersion.h.in index 4ac7ee1d1..54a44888a 100644 --- a/src/calamares/CalamaresVersion.h.in +++ b/src/calamares/CalamaresVersion.h.in @@ -4,7 +4,7 @@ #cmakedefine CALAMARES_ORGANIZATION_NAME "${CALAMARES_ORGANIZATION_NAME}" #cmakedefine CALAMARES_ORGANIZATION_DOMAIN "${CALAMARES_ORGANIZATION_DOMAIN}" #cmakedefine CALAMARES_APPLICATION_NAME "${CALAMARES_APPLICATION_NAME}" -#cmakedefine CALAMARES_VERSION "${CALAMARES_VERSION}" +#cmakedefine CALAMARES_VERSION "${CALAMARES_VERSION_SHORT}" #cmakedefine CALAMARES_VERSION_SHORT "${CALAMARES_VERSION_SHORT}" #cmakedefine CALAMARES_VERSION_MAJOR "${CALAMARES_VERSION_MAJOR}" diff --git a/src/calamares/CalamaresVersionX.h.in b/src/calamares/CalamaresVersionX.h.in new file mode 100644 index 000000000..75b124c11 --- /dev/null +++ b/src/calamares/CalamaresVersionX.h.in @@ -0,0 +1,13 @@ +// Same as CalamaresVersion.h, but with a full-git-extended VERSION +// rather than the short (vM.m.p) semantic version. +#ifndef CALAMARES_VERSION_H + +// On purpose, do not define the guard, but let CalamaresVersion.h do it +// #define CALAMARES_VERSION_H + +#include "CalamaresVersion.h" + +#undef CALAMARES_VERSION +#cmakedefine CALAMARES_VERSION "${CALAMARES_VERSION}" + +#endif // CALAMARES_VERSION_H From 38b347f8f2c1d36063db564c6fdf12dd9c0d557a Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 28 Jul 2020 14:58:43 +0200 Subject: [PATCH 101/113] [libcalamares] Take ownership of the versioning headers - The sources were in src/calamares but processed and generated in libcalamares, which is weird at best. - Generate an "extended" version header. - Use the extended version in the logger and nowhere else. - While here, minor coding style cleanups The overall change here means that after running CMake, only Logger.cpp needs to be rebuilt (if the extended version has changed) and not a handful of other files that don't need the full version number, but do happen to include CalamaresVersion.h --- src/libcalamares/CMakeLists.txt | 4 +++- .../CalamaresVersion.h.in | 0 .../CalamaresVersionX.h.in | 0 src/libcalamares/utils/Logger.cpp | 14 ++++++-------- 4 files changed, 9 insertions(+), 9 deletions(-) rename src/{calamares => libcalamares}/CalamaresVersion.h.in (100%) rename src/{calamares => libcalamares}/CalamaresVersionX.h.in (100%) diff --git a/src/libcalamares/CMakeLists.txt b/src/libcalamares/CMakeLists.txt index 56bacb32a..8e209f8a3 100644 --- a/src/libcalamares/CMakeLists.txt +++ b/src/libcalamares/CMakeLists.txt @@ -27,8 +27,10 @@ include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR} ) configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/CalamaresConfig.h.in ${CMAKE_CURRENT_BINARY_DIR}/CalamaresConfig.h ) -configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/../calamares/CalamaresVersion.h.in +configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/CalamaresVersion.h.in ${CMAKE_CURRENT_BINARY_DIR}/CalamaresVersion.h ) +configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/CalamaresVersionX.h.in + ${CMAKE_CURRENT_BINARY_DIR}/CalamaresVersionX.h ) set( OPTIONAL_PRIVATE_LIBRARIES "" ) set( OPTIONAL_PUBLIC_LIBRARIES "" ) diff --git a/src/calamares/CalamaresVersion.h.in b/src/libcalamares/CalamaresVersion.h.in similarity index 100% rename from src/calamares/CalamaresVersion.h.in rename to src/libcalamares/CalamaresVersion.h.in diff --git a/src/calamares/CalamaresVersionX.h.in b/src/libcalamares/CalamaresVersionX.h.in similarity index 100% rename from src/calamares/CalamaresVersionX.h.in rename to src/libcalamares/CalamaresVersionX.h.in diff --git a/src/libcalamares/utils/Logger.cpp b/src/libcalamares/utils/Logger.cpp index 72885d53f..5a2149f44 100644 --- a/src/libcalamares/utils/Logger.cpp +++ b/src/libcalamares/utils/Logger.cpp @@ -3,6 +3,7 @@ * SPDX-FileCopyrightText: 2010-2011 Christian Muehlhaeuser * SPDX-FileCopyrightText: 2014 Teo Mrnjavac * SPDX-FileCopyrightText: 2017 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later * * * Calamares is free software: you can redistribute it and/or modify @@ -18,15 +19,12 @@ * You should have received a copy of the GNU General Public License * along with Calamares. If not, see . * - * SPDX-License-Identifier: GPL-3.0-or-later - * License-Filename: LICENSE - * */ #include "Logger.h" -#include -#include +#include "CalamaresVersionX.h" +#include "utils/Dirs.h" #include #include @@ -35,10 +33,10 @@ #include #include -#include "CalamaresVersion.h" -#include "utils/Dirs.h" +#include +#include -#define LOGFILE_SIZE 1024 * 256 +static constexpr const int LOGFILE_SIZE = 1024 * 256; static std::ofstream logfile; static unsigned int s_threshold = From bfa1f618c7aade57bd2c51b11293ecf4931c765e Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Tue, 28 Jul 2020 17:36:10 +0200 Subject: [PATCH 102/113] CMake: Improve RCC version-checking Previously, we check for RCC support every single time CMake runs. This is slightly wasteful, and it wasn't being done right anyway. But it's moot because: - Calamares supports back to Qt 5.9 - Qt 5.9's version of rcc (at least, 5.9.7) **does** support the command-line argument `--format-version 1` - Everything newer does too. Simplify translations a little, too: just use autorcc rather than building things by hand. --- CMakeModules/CalamaresAddTranslations.cmake | 36 ++------------------- src/calamares/CMakeLists.txt | 4 +-- 2 files changed, 4 insertions(+), 36 deletions(-) diff --git a/CMakeModules/CalamaresAddTranslations.cmake b/CMakeModules/CalamaresAddTranslations.cmake index bb15fb122..88992faf5 100644 --- a/CMakeModules/CalamaresAddTranslations.cmake +++ b/CMakeModules/CalamaresAddTranslations.cmake @@ -22,46 +22,14 @@ include( CMakeParseArguments ) -if( NOT _rcc_version_support_checked ) - set( _rcc_version_support_checked TRUE ) - - # Extract the executable name - get_property( _rcc_executable - TARGET ${Qt5Core_RCC_EXECUTABLE} - PROPERTY IMPORTED_LOCATION - ) - if( NOT _rcc_executable ) - # Weird, probably now uses Qt5::rcc which is wrong too - set( _rcc_executable ${Qt5Core_RCC_EXECUTABLE} ) - endif() - - # Try an empty RCC file with explicit format-version - execute_process( - COMMAND echo "" - COMMAND ${Qt5Core_RCC_EXECUTABLE} --format-version 1 --list - - RESULT_VARIABLE _rcc_version_rv - ERROR_VARIABLE _rcc_version_dump - ) - if ( _rcc_version_rv EQUAL 0 ) - # Supported: force to the reproducible version - set( _rcc_version_support --format-version 1 ) - else() - # Older Qt versions (5.7, 5.8) don't support setting the - # rcc format-version, so won't be reproducible if they - # default to version 2. - set( _rcc_version_support "" ) - endif() - unset( _rcc_version_rv ) - unset( _rcc_version_dump ) -endif() - - # Internal macro for adding the C++ / Qt translations to the # build and install tree. Should be called only once, from # src/calamares/CMakeLists.txt. macro(add_calamares_translations language) list( APPEND CALAMARES_LANGUAGES ${ARGV} ) + set( _rcc_version_support --format-version 1 ) + set( calamares_i18n_qrc_content "\n" ) # calamares and qt language files diff --git a/src/calamares/CMakeLists.txt b/src/calamares/CMakeLists.txt index b632567b8..ff91904e2 100644 --- a/src/calamares/CMakeLists.txt +++ b/src/calamares/CMakeLists.txt @@ -34,9 +34,8 @@ include_directories( # Translations include( CalamaresAddTranslations ) add_calamares_translations( ${CALAMARES_TRANSLATION_LANGUAGES} ) -qt5_add_resources( calamaresRc calamares.qrc ) -add_executable( calamares_bin ${calamaresSources} ${calamaresRc} ${trans_outfile} ) +add_executable( calamares_bin ${calamaresSources} calamares.qrc ${trans_outfile} ) target_include_directories( calamares_bin PRIVATE ${CMAKE_SOURCE_DIR} ) set_target_properties(calamares_bin PROPERTIES @@ -45,6 +44,7 @@ set_target_properties(calamares_bin ) calamares_automoc( calamares_bin ) calamares_autouic( calamares_bin ) +calamares_autorcc( calamares_bin ) target_link_libraries( calamares_bin PRIVATE From afb0b36f586e262824ea9754300f903b0e93a808 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 29 Jul 2020 10:01:49 +0200 Subject: [PATCH 103/113] CMake: simplify QRC generation Use configure_file() to avoid stomping on timestamps: if the list of translations doesn't change, we don't need to rebuild the translated QRC. --- CMakeModules/CalamaresAddTranslations.cmake | 25 +++++---------------- lang/calamares_i18n.qrc.in | 5 +++++ 2 files changed, 10 insertions(+), 20 deletions(-) create mode 100644 lang/calamares_i18n.qrc.in diff --git a/CMakeModules/CalamaresAddTranslations.cmake b/CMakeModules/CalamaresAddTranslations.cmake index 88992faf5..5015301d2 100644 --- a/CMakeModules/CalamaresAddTranslations.cmake +++ b/CMakeModules/CalamaresAddTranslations.cmake @@ -28,12 +28,9 @@ include( CMakeParseArguments ) macro(add_calamares_translations language) list( APPEND CALAMARES_LANGUAGES ${ARGV} ) - set( _rcc_version_support --format-version 1 ) - - set( calamares_i18n_qrc_content "\n" ) + set( calamares_i18n_qrc_content "" ) # calamares and qt language files - set( calamares_i18n_qrc_content "${calamares_i18n_qrc_content}\n" ) foreach( lang ${CALAMARES_LANGUAGES} ) foreach( tlsource "calamares_${lang}" "tz_${lang}" ) if( EXISTS "${CMAKE_SOURCE_DIR}/lang/${tlsource}.ts" ) @@ -43,31 +40,19 @@ macro(add_calamares_translations language) endforeach() endforeach() - set( calamares_i18n_qrc_content "${calamares_i18n_qrc_content}\n" ) - set( calamares_i18n_qrc_content "${calamares_i18n_qrc_content}\n" ) - - file( WRITE ${CMAKE_BINARY_DIR}/lang/calamares_i18n.qrc "${calamares_i18n_qrc_content}" ) - - qt5_add_translation(QM_FILES ${TS_FILES}) - - ## HACK HACK HACK - around rcc limitations to allow out of source-tree building set( trans_file calamares_i18n ) - set( trans_srcfile ${CMAKE_BINARY_DIR}/lang/${trans_file}.qrc ) set( trans_infile ${CMAKE_CURRENT_BINARY_DIR}/${trans_file}.qrc ) set( trans_outfile ${CMAKE_CURRENT_BINARY_DIR}/qrc_${trans_file}.cxx ) - # Copy the QRC file to the output directory - add_custom_command( - OUTPUT ${trans_infile} - COMMAND ${CMAKE_COMMAND} -E copy ${trans_srcfile} ${trans_infile} - MAIN_DEPENDENCY ${trans_srcfile} - ) + configure_file( ${CMAKE_SOURCE_DIR}/lang/calamares_i18n.qrc.in ${trans_infile} @ONLY ) + + qt5_add_translation(QM_FILES ${TS_FILES}) # Run the resource compiler (rcc_options should already be set) add_custom_command( OUTPUT ${trans_outfile} COMMAND "${Qt5Core_RCC_EXECUTABLE}" - ARGS ${rcc_options} ${_rcc_version_support} -name ${trans_file} -o ${trans_outfile} ${trans_infile} + ARGS ${rcc_options} --format-version 1 -name ${trans_file} -o ${trans_outfile} ${trans_infile} MAIN_DEPENDENCY ${trans_infile} DEPENDS ${QM_FILES} ) diff --git a/lang/calamares_i18n.qrc.in b/lang/calamares_i18n.qrc.in new file mode 100644 index 000000000..1f0ac1604 --- /dev/null +++ b/lang/calamares_i18n.qrc.in @@ -0,0 +1,5 @@ + + +@calamares_i18n_qrc_content@ + + From 33eab6e8697ab91b23879ab50ed8337a86ee1af1 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 29 Jul 2020 10:58:07 +0200 Subject: [PATCH 104/113] CMake: improve validator dependency-checking The configvalidator has some extra Python dependencies. Cache the restults of checking the dependencies (convenient for developers), and also explain what's going on if the feature is switched off. --- CMakeLists.txt | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 43ca1bc56..3dec2e55f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -356,23 +356,33 @@ set_package_properties( URL "https://python.org" PURPOSE "Python 3 interpreter for certain tests." ) + +set( _schema_explanation "" ) if ( PYTHONINTERP_FOUND ) - message(STATUS "Found Python 3 interpreter ${PYTHON_EXECUTABLE}") if ( BUILD_SCHEMA_TESTING ) # The configuration validator script has some dependencies, # and if they are not installed, don't run. If errors out # with exit(1) on missing dependencies. - exec_program( ${PYTHON_EXECUTABLE} ARGS "${CMAKE_SOURCE_DIR}/ci/configvalidator.py" -x RETURN_VALUE _validator_deps ) + if ( CALAMARES_CONFIGVALIDATOR_CHECKED ) + set( _validator_deps ${CALAMARES_CONFIGVALIDATOR_RESULT} ) + else() + exec_program( ${PYTHON_EXECUTABLE} ARGS "${CMAKE_SOURCE_DIR}/ci/configvalidator.py" -x RETURN_VALUE _validator_deps ) + set( CALAMARES_CONFIGVALIDATOR_CHECKED TRUE CACHE INTERNAL "Dependencies for configvalidator checked" ) + set( CALAMARES_CONFIGVALIDATOR_RESULT ${_validator_deps} CACHE INTERNAL "Result of configvalidator dependency check" ) + endif() # It should never succeed, but only returns 1 when the imports fail if ( _validator_deps EQUAL 1 ) - message(STATUS "BUILD_SCHEMA_TESTING dependencies are missing." ) + set( _schema_explanation " Missing dependencies for configvalidator.py." ) set( BUILD_SCHEMA_TESTING OFF ) endif() endif() else() # Can't run schema tests without Python3. + set( _schema_explanation " Missing Python3." ) set( BUILD_SCHEMA_TESTING OFF ) endif() +add_feature_info( yaml-schema BUILD_SCHEMA_TESTING "Validate YAML (config files) with schema.${_schema_explanation}" ) + find_package( PythonLibs ${PYTHONLIBS_VERSION} ) set_package_properties( PythonLibs PROPERTIES From b9372ba4328af677b6f5c132b4c6ae7ac6fb9dfe Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 29 Jul 2020 12:18:25 +0200 Subject: [PATCH 105/113] [users] Move default groups setting to Config - drop groups from the viewstep - note that the Config object should also be in charge of creating Jobs (but then the de-tangling needs to be completed) - add tests of default groups loading Doesn't compile because QRegExpValidator is a gui thing. --- src/modules/users/CMakeLists.txt | 1 + src/modules/users/Config.cpp | 16 +++++++ src/modules/users/Config.h | 3 ++ src/modules/users/CreateUserTests.cpp | 61 ++++++++++++++++++++++++++- src/modules/users/UsersViewStep.cpp | 14 +----- src/modules/users/UsersViewStep.h | 1 - 6 files changed, 81 insertions(+), 15 deletions(-) diff --git a/src/modules/users/CMakeLists.txt b/src/modules/users/CMakeLists.txt index bf93eb26a..e2d2e3117 100644 --- a/src/modules/users/CMakeLists.txt +++ b/src/modules/users/CMakeLists.txt @@ -55,6 +55,7 @@ calamares_add_test( SOURCES CreateUserTests.cpp CreateUserJob.cpp + Config.cpp ) calamares_add_test( diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp index bb739cbd1..060600894 100644 --- a/src/modules/users/Config.cpp +++ b/src/modules/users/Config.cpp @@ -351,6 +351,21 @@ Config::setAutoLogin( bool b ) } } +STATICTEST inline void +setConfigurationDefaultGroups( const QVariantMap& map, QStringList& defaultGroups ) +{ + // '#' is not a valid group name; use that to distinguish an empty-list + // in the configuration (which is a legitimate, if unusual, choice) + // from a bad or missing configuration value. + defaultGroups = CalamaresUtils::getStringList( map, QStringLiteral( "defaultGroups" ), QStringList { "#" } ); + if ( defaultGroups.contains( QStringLiteral( "#" ) ) ) + { + cWarning() << "Using fallback groups. Please check *defaultGroups* in users.conf"; + defaultGroups = QStringList { "lp", "video", "network", "storage", "wheel", "audio" }; + } +} + + void Config::setConfigurationMap( const QVariantMap& configurationMap ) { @@ -365,6 +380,7 @@ Config::setConfigurationMap( const QVariantMap& configurationMap ) setAutologinGroup( CalamaresUtils::getString( configurationMap, "autologinGroup" ) ); setSudoersGroup( CalamaresUtils::getString( configurationMap, "sudoersGroup" ) ); + setConfigurationDefaultGroups( configurationMap, m_defaultGroups ); m_doAutoLogin = CalamaresUtils::getBool( configurationMap, "doAutologin", false ); m_writeRootPassword = CalamaresUtils::getBool( configurationMap, "setRootPassword", true ); diff --git a/src/modules/users/Config.h b/src/modules/users/Config.h index d32ddc8f2..b84dc5aaf 100644 --- a/src/modules/users/Config.h +++ b/src/modules/users/Config.h @@ -77,6 +77,8 @@ public: /// Should the root password be written (if false, no password is set and the root account is disabled for login) bool writeRootPassword() const { return m_writeRootPassword; } + const QStringList& defaultGroups() const { return m_defaultGroups; } + static const QStringList& forbiddenLoginNames(); static const QStringList& forbiddenHostNames(); @@ -119,6 +121,7 @@ signals: void autoLoginChanged( bool ); private: + QStringList m_defaultGroups; QString m_userShell; QString m_autologinGroup; QString m_sudoersGroup; diff --git a/src/modules/users/CreateUserTests.cpp b/src/modules/users/CreateUserTests.cpp index b351109b3..50592a384 100644 --- a/src/modules/users/CreateUserTests.cpp +++ b/src/modules/users/CreateUserTests.cpp @@ -17,6 +17,7 @@ * along with Calamares. If not, see . */ +#include "Config.h" #include "CreateUserJob.h" #include "utils/Logger.h" @@ -25,8 +26,8 @@ #include // Implementation details -extern QStringList groupsInTargetSystem( const QDir& targetRoot ); - +extern QStringList groupsInTargetSystem( const QDir& targetRoot ); // CreateUserJob +extern void setConfigurationDefaultGroups( const QVariantMap& map, QStringList& defaultGroups ); class CreateUserTests : public QObject { @@ -39,6 +40,7 @@ private Q_SLOTS: void initTestCase(); void testReadGroup(); + void testDefaultGroups(); }; CreateUserTests::CreateUserTests() {} @@ -73,6 +75,61 @@ CreateUserTests::testReadGroup() } } +void +CreateUserTests::testDefaultGroups() +{ + { + QStringList groups; + QVariantMap hweelGroup; + QVERIFY( groups.isEmpty() ); + hweelGroup.insert( "defaultGroups", QStringList { "hweel" } ); + setConfigurationDefaultGroups( hweelGroup, groups ); + QCOMPARE( groups.count(), 1 ); + QVERIFY( groups.contains( "hweel" ) ); + } + + { + QStringList desired { "wheel", "root", "operator" }; + QStringList groups; + QVariantMap threeGroup; + QVERIFY( groups.isEmpty() ); + threeGroup.insert( "defaultGroups", desired ); + setConfigurationDefaultGroups( threeGroup, groups ); + QCOMPARE( groups.count(), 3 ); + QVERIFY( !groups.contains( "hweel" ) ); + QCOMPARE( groups, desired ); + } + + { + QStringList groups; + QVariantMap explicitEmpty; + QVERIFY( groups.isEmpty() ); + explicitEmpty.insert( "defaultGroups", QStringList() ); + setConfigurationDefaultGroups( explicitEmpty, groups ); + QCOMPARE( groups.count(), 0 ); + } + + { + QStringList groups; + QVariantMap missing; + QVERIFY( groups.isEmpty() ); + setConfigurationDefaultGroups( missing, groups ); + QCOMPARE( groups.count(), 6 ); // because of fallback! + QVERIFY( groups.contains( "lp" ) ); + } + + { + QStringList groups; + QVariantMap typeMismatch; + QVERIFY( groups.isEmpty() ); + typeMismatch.insert( "defaultGroups", 1 ); + setConfigurationDefaultGroups( typeMismatch, groups ); + QCOMPARE( groups.count(), 6 ); // because of fallback! + QVERIFY( groups.contains( "lp" ) ); + } +} + + QTEST_GUILESS_MAIN( CreateUserTests ) #include "utils/moc-warnings.h" diff --git a/src/modules/users/UsersViewStep.cpp b/src/modules/users/UsersViewStep.cpp index ddaf4837b..b9ea3ae17 100644 --- a/src/modules/users/UsersViewStep.cpp +++ b/src/modules/users/UsersViewStep.cpp @@ -145,10 +145,11 @@ UsersViewStep::onLeave() } Calamares::Job* j; + // TODO: Config object should create jobs, like this one, that depend only on config values j = new CreateUserJob( m_config->loginName(), m_config->fullName().isEmpty() ? m_config->loginName() : m_config->fullName(), m_config->doAutoLogin(), - m_defaultGroups ); + m_config->defaultGroups() ); auto userPW = m_widget->getUserPassword(); j = new SetPasswordJob( userPW.first, userPW.second ); @@ -171,17 +172,6 @@ UsersViewStep::setConfigurationMap( const QVariantMap& configurationMap ) (void)this->widget(); using CalamaresUtils::getBool; - if ( configurationMap.contains( "defaultGroups" ) - && configurationMap.value( "defaultGroups" ).type() == QVariant::List ) - { - m_defaultGroups = configurationMap.value( "defaultGroups" ).toStringList(); - } - else - { - cWarning() << "Using fallback groups. Please check defaultGroups in users.conf"; - m_defaultGroups = QStringList { "lp", "video", "network", "storage", "wheel", "audio" }; - } - m_widget->setReusePasswordDefault( getBool( configurationMap, "doReusePassword", false ) ); if ( configurationMap.contains( "passwordRequirements" ) diff --git a/src/modules/users/UsersViewStep.h b/src/modules/users/UsersViewStep.h index aacf11f2a..5c3674571 100644 --- a/src/modules/users/UsersViewStep.h +++ b/src/modules/users/UsersViewStep.h @@ -61,7 +61,6 @@ private: UsersPage* m_widget; QList< Calamares::job_ptr > m_jobs; - QStringList m_defaultGroups; SetHostNameJob::Actions m_actions; Config* m_config; From f75839340a0183873ee43214ba77e0428242d2c0 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 29 Jul 2020 12:27:56 +0200 Subject: [PATCH 106/113] [users] Drop QRegExpValidator - QREValidator is a GUI part, so to avoid a dependency on GUI for the (non-GUI) Config object, port to the simpler QRE (which we had available anyway) --- src/modules/users/Config.cpp | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp index 060600894..6c6bbe90a 100644 --- a/src/modules/users/Config.cpp +++ b/src/modules/users/Config.cpp @@ -28,7 +28,6 @@ #include #include -#include static const QRegExp USERNAME_RX( "^[a-z_][a-z0-9_-]*[$]?$" ); static constexpr const int USERNAME_MAX_LENGTH = 31; @@ -132,15 +131,12 @@ Config::loginNameStatus() const } } - QString login( m_loginName ); // make a copy because validate() doesn't take const& - QRegExpValidator validateEntireLoginName( USERNAME_RX ); - QRegExpValidator validateFirstLetter( QRegExp( "[a-z_].*" ) ); // anchors are implicit in QRegExpValidator - int pos = -1; - if ( validateFirstLetter.validate( login, pos ) == QValidator::Invalid ) + QRegExp validateFirstLetter( "^[a-z_]" ); + if ( validateFirstLetter.indexIn( m_loginName ) != 0 ) { return tr( "Your username must start with a lowercase letter or underscore." ); } - if ( validateEntireLoginName.validate( login, pos ) == QValidator::Invalid ) + if ( !USERNAME_RX.exactMatch( m_loginName ) ) { return tr( "Only lowercase letters, numbers, underscore and hyphen are allowed." ); } @@ -202,11 +198,7 @@ Config::hostNameStatus() const } } - QString text = m_hostName; - QRegExpValidator val( HOSTNAME_RX ); - int pos = -1; - - if ( val.validate( text, pos ) == QValidator::Invalid ) + if ( !HOSTNAME_RX.exactMatch( m_hostName ) ) { return tr( "Only letters, numbers, underscore and hyphen are allowed." ); } From cc1136fb0eb9d243a183c06b87c8aa4eb34cd07f Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 29 Jul 2020 13:23:41 +0200 Subject: [PATCH 107/113] [users] Untangle tests - name sources for tests consistently Test - chase some required source changes with the renaming - name test targets consistently too --- src/modules/users/CMakeLists.txt | 8 ++++---- .../users/{CreateUserTests.cpp => TestCreateUserJob.cpp} | 2 +- .../users/{PasswordTests.cpp => TestPasswordJob.cpp} | 2 +- src/modules/users/{PasswordTests.h => TestPasswordJob.h} | 0 src/modules/users/{Tests.cpp => TestSetHostNameJob.cpp} | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) rename src/modules/users/{CreateUserTests.cpp => TestCreateUserJob.cpp} (99%) rename src/modules/users/{PasswordTests.cpp => TestPasswordJob.cpp} (98%) rename src/modules/users/{PasswordTests.h => TestPasswordJob.h} (100%) rename src/modules/users/{Tests.cpp => TestSetHostNameJob.cpp} (99%) diff --git a/src/modules/users/CMakeLists.txt b/src/modules/users/CMakeLists.txt index e2d2e3117..a486e93c3 100644 --- a/src/modules/users/CMakeLists.txt +++ b/src/modules/users/CMakeLists.txt @@ -44,7 +44,7 @@ calamares_add_plugin( users calamares_add_test( userspasswordtest SOURCES - PasswordTests.cpp + TestPasswordJob.cpp SetPasswordJob.cpp LIBRARIES ${CRYPT_LIBRARIES} @@ -53,15 +53,15 @@ calamares_add_test( calamares_add_test( userscreatetest SOURCES - CreateUserTests.cpp + TestCreateUserJob.cpp CreateUserJob.cpp Config.cpp ) calamares_add_test( - userstest + usershostnametest SOURCES - Tests.cpp + TestSetHostNameJob.cpp SetHostNameJob.cpp LIBRARIES Qt5::DBus diff --git a/src/modules/users/CreateUserTests.cpp b/src/modules/users/TestCreateUserJob.cpp similarity index 99% rename from src/modules/users/CreateUserTests.cpp rename to src/modules/users/TestCreateUserJob.cpp index 50592a384..f8c28a235 100644 --- a/src/modules/users/CreateUserTests.cpp +++ b/src/modules/users/TestCreateUserJob.cpp @@ -134,4 +134,4 @@ QTEST_GUILESS_MAIN( CreateUserTests ) #include "utils/moc-warnings.h" -#include "CreateUserTests.moc" +#include "TestCreateUserJob.moc" diff --git a/src/modules/users/PasswordTests.cpp b/src/modules/users/TestPasswordJob.cpp similarity index 98% rename from src/modules/users/PasswordTests.cpp rename to src/modules/users/TestPasswordJob.cpp index 0c1b4bffc..7f38c109d 100644 --- a/src/modules/users/PasswordTests.cpp +++ b/src/modules/users/TestPasswordJob.cpp @@ -18,7 +18,7 @@ #include "SetPasswordJob.h" -#include "PasswordTests.h" +#include "TestPasswordJob.h" #include diff --git a/src/modules/users/PasswordTests.h b/src/modules/users/TestPasswordJob.h similarity index 100% rename from src/modules/users/PasswordTests.h rename to src/modules/users/TestPasswordJob.h diff --git a/src/modules/users/Tests.cpp b/src/modules/users/TestSetHostNameJob.cpp similarity index 99% rename from src/modules/users/Tests.cpp rename to src/modules/users/TestSetHostNameJob.cpp index 196fd9d68..9e08d4807 100644 --- a/src/modules/users/Tests.cpp +++ b/src/modules/users/TestSetHostNameJob.cpp @@ -142,4 +142,4 @@ QTEST_GUILESS_MAIN( UsersTests ) #include "utils/moc-warnings.h" -#include "Tests.moc" +#include "TestSetHostNameJob.moc" From 892e9798f4dd9400e3d97c7da81d8ed01af4cc5d Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 29 Jul 2020 13:31:39 +0200 Subject: [PATCH 108/113] [users] Sanitize tests - move the testing of config-object methods to its own tests - simplify file structure for the password job tests --- src/modules/users/CMakeLists.txt | 8 +- src/modules/users/TestCreateUserJob.cpp | 58 ------------ src/modules/users/TestPasswordJob.cpp | 23 ++++- src/modules/users/TestPasswordJob.h | 36 -------- src/modules/users/TestSetHostNameJob.cpp | 3 +- src/modules/users/Tests.cpp | 113 +++++++++++++++++++++++ 6 files changed, 141 insertions(+), 100 deletions(-) delete mode 100644 src/modules/users/TestPasswordJob.h create mode 100644 src/modules/users/Tests.cpp diff --git a/src/modules/users/CMakeLists.txt b/src/modules/users/CMakeLists.txt index a486e93c3..5fafcd4f3 100644 --- a/src/modules/users/CMakeLists.txt +++ b/src/modules/users/CMakeLists.txt @@ -55,7 +55,6 @@ calamares_add_test( SOURCES TestCreateUserJob.cpp CreateUserJob.cpp - Config.cpp ) calamares_add_test( @@ -66,3 +65,10 @@ calamares_add_test( LIBRARIES Qt5::DBus ) + +calamares_add_test( + userstest + SOURCES + Tests.cpp + Config.cpp +) diff --git a/src/modules/users/TestCreateUserJob.cpp b/src/modules/users/TestCreateUserJob.cpp index f8c28a235..610f84eef 100644 --- a/src/modules/users/TestCreateUserJob.cpp +++ b/src/modules/users/TestCreateUserJob.cpp @@ -17,7 +17,6 @@ * along with Calamares. If not, see . */ -#include "Config.h" #include "CreateUserJob.h" #include "utils/Logger.h" @@ -27,7 +26,6 @@ // Implementation details extern QStringList groupsInTargetSystem( const QDir& targetRoot ); // CreateUserJob -extern void setConfigurationDefaultGroups( const QVariantMap& map, QStringList& defaultGroups ); class CreateUserTests : public QObject { @@ -40,7 +38,6 @@ private Q_SLOTS: void initTestCase(); void testReadGroup(); - void testDefaultGroups(); }; CreateUserTests::CreateUserTests() {} @@ -75,61 +72,6 @@ CreateUserTests::testReadGroup() } } -void -CreateUserTests::testDefaultGroups() -{ - { - QStringList groups; - QVariantMap hweelGroup; - QVERIFY( groups.isEmpty() ); - hweelGroup.insert( "defaultGroups", QStringList { "hweel" } ); - setConfigurationDefaultGroups( hweelGroup, groups ); - QCOMPARE( groups.count(), 1 ); - QVERIFY( groups.contains( "hweel" ) ); - } - - { - QStringList desired { "wheel", "root", "operator" }; - QStringList groups; - QVariantMap threeGroup; - QVERIFY( groups.isEmpty() ); - threeGroup.insert( "defaultGroups", desired ); - setConfigurationDefaultGroups( threeGroup, groups ); - QCOMPARE( groups.count(), 3 ); - QVERIFY( !groups.contains( "hweel" ) ); - QCOMPARE( groups, desired ); - } - - { - QStringList groups; - QVariantMap explicitEmpty; - QVERIFY( groups.isEmpty() ); - explicitEmpty.insert( "defaultGroups", QStringList() ); - setConfigurationDefaultGroups( explicitEmpty, groups ); - QCOMPARE( groups.count(), 0 ); - } - - { - QStringList groups; - QVariantMap missing; - QVERIFY( groups.isEmpty() ); - setConfigurationDefaultGroups( missing, groups ); - QCOMPARE( groups.count(), 6 ); // because of fallback! - QVERIFY( groups.contains( "lp" ) ); - } - - { - QStringList groups; - QVariantMap typeMismatch; - QVERIFY( groups.isEmpty() ); - typeMismatch.insert( "defaultGroups", 1 ); - setConfigurationDefaultGroups( typeMismatch, groups ); - QCOMPARE( groups.count(), 6 ); // because of fallback! - QVERIFY( groups.contains( "lp" ) ); - } -} - - QTEST_GUILESS_MAIN( CreateUserTests ) #include "utils/moc-warnings.h" diff --git a/src/modules/users/TestPasswordJob.cpp b/src/modules/users/TestPasswordJob.cpp index 7f38c109d..8677f88c2 100644 --- a/src/modules/users/TestPasswordJob.cpp +++ b/src/modules/users/TestPasswordJob.cpp @@ -1,6 +1,7 @@ /* === This file is part of Calamares - === * - * Copyright 2017, Adriaan de Groot + * SPDX-FileCopyrightText: 2017 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later * * Calamares is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,11 +19,19 @@ #include "SetPasswordJob.h" -#include "TestPasswordJob.h" - #include -QTEST_GUILESS_MAIN( PasswordTests ) +class PasswordTests : public QObject +{ + Q_OBJECT +public: + PasswordTests(); + ~PasswordTests() override; + +private Q_SLOTS: + void initTestCase(); + void testSalt(); +}; PasswordTests::PasswordTests() {} @@ -48,3 +57,9 @@ PasswordTests::testSalt() QVERIFY( s.endsWith( '$' ) ); qDebug() << "Obtained salt" << s; } + +QTEST_GUILESS_MAIN( PasswordTests ) + +#include "utils/moc-warnings.h" + +#include "TestPasswordJob.moc" diff --git a/src/modules/users/TestPasswordJob.h b/src/modules/users/TestPasswordJob.h deleted file mode 100644 index 3b4b5d201..000000000 --- a/src/modules/users/TestPasswordJob.h +++ /dev/null @@ -1,36 +0,0 @@ -/* === This file is part of Calamares - === - * - * Copyright 2017, Adriaan de Groot - * - * 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 . - */ - -#ifndef PASSWORDTESTS_H -#define PASSWORDTESTS_H - -#include - -class PasswordTests : public QObject -{ - Q_OBJECT -public: - PasswordTests(); - ~PasswordTests() override; - -private Q_SLOTS: - void initTestCase(); - void testSalt(); -}; - -#endif diff --git a/src/modules/users/TestSetHostNameJob.cpp b/src/modules/users/TestSetHostNameJob.cpp index 9e08d4807..0e887a5f1 100644 --- a/src/modules/users/TestSetHostNameJob.cpp +++ b/src/modules/users/TestSetHostNameJob.cpp @@ -1,6 +1,7 @@ /* === This file is part of Calamares - === * - * Copyright 2020, Adriaan de Groot + * SPDX-FileCopyrightText: 2020 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later * * Calamares is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/modules/users/Tests.cpp b/src/modules/users/Tests.cpp new file mode 100644 index 000000000..a4ee45fe2 --- /dev/null +++ b/src/modules/users/Tests.cpp @@ -0,0 +1,113 @@ +/* === This file is part of Calamares - === + * + * SPDX-FileCopyrightText: 2020 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later + * + * 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 . + */ + +#include "Config.h" + +#include "utils/Logger.h" + +#include + +// Implementation details +extern void setConfigurationDefaultGroups( const QVariantMap& map, QStringList& defaultGroups ); + +/** @brief Test Config object methods and internals + * + */ +class UserTests : public QObject +{ + Q_OBJECT +public: + UserTests(); + virtual ~UserTests() {} + +private Q_SLOTS: + void initTestCase(); + + void testDefaultGroups(); +}; + +UserTests::UserTests() {} + +void +UserTests::initTestCase() +{ + Logger::setupLogLevel( Logger::LOGDEBUG ); + cDebug() << "Users test started."; +} + +void +UserTests::testDefaultGroups() +{ + { + QStringList groups; + QVariantMap hweelGroup; + QVERIFY( groups.isEmpty() ); + hweelGroup.insert( "defaultGroups", QStringList { "hweel" } ); + setConfigurationDefaultGroups( hweelGroup, groups ); + QCOMPARE( groups.count(), 1 ); + QVERIFY( groups.contains( "hweel" ) ); + } + + { + QStringList desired { "wheel", "root", "operator" }; + QStringList groups; + QVariantMap threeGroup; + QVERIFY( groups.isEmpty() ); + threeGroup.insert( "defaultGroups", desired ); + setConfigurationDefaultGroups( threeGroup, groups ); + QCOMPARE( groups.count(), 3 ); + QVERIFY( !groups.contains( "hweel" ) ); + QCOMPARE( groups, desired ); + } + + { + QStringList groups; + QVariantMap explicitEmpty; + QVERIFY( groups.isEmpty() ); + explicitEmpty.insert( "defaultGroups", QStringList() ); + setConfigurationDefaultGroups( explicitEmpty, groups ); + QCOMPARE( groups.count(), 0 ); + } + + { + QStringList groups; + QVariantMap missing; + QVERIFY( groups.isEmpty() ); + setConfigurationDefaultGroups( missing, groups ); + QCOMPARE( groups.count(), 6 ); // because of fallback! + QVERIFY( groups.contains( "lp" ) ); + } + + { + QStringList groups; + QVariantMap typeMismatch; + QVERIFY( groups.isEmpty() ); + typeMismatch.insert( "defaultGroups", 1 ); + setConfigurationDefaultGroups( typeMismatch, groups ); + QCOMPARE( groups.count(), 6 ); // because of fallback! + QVERIFY( groups.contains( "lp" ) ); + } +} + + +QTEST_GUILESS_MAIN( UserTests ) + +#include "utils/moc-warnings.h" + +#include "Tests.moc" From 43cd415d9adf3ea99216e2c6d709a41ac747fc9d Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 29 Jul 2020 13:42:56 +0200 Subject: [PATCH 109/113] [partition] Switch to 'modern' Error/ok icons --- src/modules/partition/gui/EncryptWidget.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/modules/partition/gui/EncryptWidget.cpp b/src/modules/partition/gui/EncryptWidget.cpp index 42a073db7..8fb9a7b30 100644 --- a/src/modules/partition/gui/EncryptWidget.cpp +++ b/src/modules/partition/gui/EncryptWidget.cpp @@ -141,15 +141,16 @@ EncryptWidget::onPassphraseEdited() m_ui->m_iconLabel->setToolTip( QString() ); if ( p1.isEmpty() && p2.isEmpty() ) { - m_ui->m_iconLabel->clear(); + applyPixmap( m_ui->m_iconLabel, CalamaresUtils::StatusWarning ); + m_ui->m_iconLabel->setToolTip( tr( "Please enter the same passphrase in both boxes." ) ); } else if ( p1 == p2 ) { - applyPixmap( m_ui->m_iconLabel, CalamaresUtils::Yes ); + applyPixmap( m_ui->m_iconLabel, CalamaresUtils::StatusOk ); } else { - applyPixmap( m_ui->m_iconLabel, CalamaresUtils::No ); + applyPixmap( m_ui->m_iconLabel, CalamaresUtils::StatusError ); m_ui->m_iconLabel->setToolTip( tr( "Please enter the same passphrase in both boxes." ) ); } From f1c4caba483c5ddadadfd85b72bdebb2f1857a5d Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 29 Jul 2020 13:59:06 +0200 Subject: [PATCH 110/113] [partition] Refactor checking next-enabled - move the calculations to an own method (so it can use early-return and log things to explain why next is disabled) --- src/modules/partition/gui/ChoicePage.cpp | 68 +++++++++++++++++------- src/modules/partition/gui/ChoicePage.h | 1 + 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/src/modules/partition/gui/ChoicePage.cpp b/src/modules/partition/gui/ChoicePage.cpp index 9e48e69ac..9f654b1bf 100644 --- a/src/modules/partition/gui/ChoicePage.cpp +++ b/src/modules/partition/gui/ChoicePage.cpp @@ -1442,46 +1442,74 @@ ChoicePage::currentChoice() const return m_choice; } - -void -ChoicePage::updateNextEnabled() +bool +ChoicePage::calculateNextEnabled() const { bool enabled = false; - auto sm_p = m_beforePartitionBarsView ? m_beforePartitionBarsView->selectionModel() : nullptr; switch ( m_choice ) { case NoChoice: - enabled = false; - break; + cDebug() << "No partitioning choice"; + return false; case Replace: case Alongside: - enabled = sm_p && sm_p->currentIndex().isValid(); + if ( !( sm_p && sm_p->currentIndex().isValid() ) ) + { + cDebug() << "No partition selected"; + return false; + } break; case Erase: case Manual: enabled = true; } - if ( m_isEfi && - ( m_choice == Alongside || - m_choice == Replace ) ) + if (!enabled) { - if ( m_core->efiSystemPartitions().count() == 0 ) - enabled = false; + cDebug() << "No valid choice made"; + return false; } - if ( m_choice != Manual && - m_encryptWidget->isVisible() && - m_encryptWidget->state() == EncryptWidget::Encryption::Unconfirmed ) - enabled = false; - if ( enabled == m_nextEnabled ) - return; + if ( m_isEfi && ( m_choice == Alongside || m_choice == Replace ) ) + { + if ( m_core->efiSystemPartitions().count() == 0 ) + { + cDebug() << "No EFI partition for alongside or replace"; + return false; + } + } - m_nextEnabled = enabled; - emit nextStatusChanged( enabled ); + if ( m_choice != Manual && m_encryptWidget->isVisible() ) + { + switch ( m_encryptWidget->state() ) + { + case EncryptWidget::Encryption::Unconfirmed: + cDebug() << "No passphrase provided"; + return false; + case EncryptWidget::Encryption::Disabled: + case EncryptWidget::Encryption::Confirmed: + // Checkbox not checked, **or** passphrases match + break; + } + } + + return true; +} + + +void +ChoicePage::updateNextEnabled() +{ + bool enabled = calculateNextEnabled(); + + if ( enabled != m_nextEnabled ) + { + m_nextEnabled = enabled; + emit nextStatusChanged( enabled ); + } } void diff --git a/src/modules/partition/gui/ChoicePage.h b/src/modules/partition/gui/ChoicePage.h index 1ff8f0d40..a28892011 100644 --- a/src/modules/partition/gui/ChoicePage.h +++ b/src/modules/partition/gui/ChoicePage.h @@ -126,6 +126,7 @@ private slots: void onEraseSwapChoiceChanged(); private: + bool calculateNextEnabled() const; void updateNextEnabled(); void setupChoices(); QComboBox* createBootloaderComboBox( QWidget* parentButton ); From 0eb1f002db2d61ea2414cc80736c2e53de818b13 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 29 Jul 2020 14:13:43 +0200 Subject: [PATCH 111/113] [partition] defuse is-next-enabled Both the KPMCore and the ChoicePage -- asynchronously -- were connected to the nextStatusChanged() signal. So if the core said next was true, that could end up communicated to the ViewManager, enabling the *next* button in the UI. Changing to the *erase* page generally triggers a KPMCore reload, which later emits a `hasRootMountPointChanged()` signal, once the layout is applied and the disk gets a root mount point. So we'd get a `true` from KPMCore, which -- because it was connected directly to the signal to the VM -- would override any other considerations. Hook up both signals to an intermediate slot that just recalculates whether the next button should be enabled, based on the state both of the Choice page and whatever else. --- src/modules/partition/gui/PartitionViewStep.cpp | 9 +++++++-- src/modules/partition/gui/PartitionViewStep.h | 3 +++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/modules/partition/gui/PartitionViewStep.cpp b/src/modules/partition/gui/PartitionViewStep.cpp index b0142a82a..900d10e57 100644 --- a/src/modules/partition/gui/PartitionViewStep.cpp +++ b/src/modules/partition/gui/PartitionViewStep.cpp @@ -107,8 +107,8 @@ PartitionViewStep::continueLoading() m_waitingWidget->deleteLater(); m_waitingWidget = nullptr; - connect( m_core, &PartitionCoreModule::hasRootMountPointChanged, this, &PartitionViewStep::nextStatusChanged ); - connect( m_choicePage, &ChoicePage::nextStatusChanged, this, &PartitionViewStep::nextStatusChanged ); + connect( m_core, &PartitionCoreModule::hasRootMountPointChanged, this, &PartitionViewStep::nextPossiblyChanged ); + connect( m_choicePage, &ChoicePage::nextStatusChanged, this, &PartitionViewStep::nextPossiblyChanged ); } @@ -348,6 +348,11 @@ PartitionViewStep::isNextEnabled() const return false; } +void +PartitionViewStep::nextPossiblyChanged(bool) +{ + emit nextStatusChanged( isNextEnabled() ); +} bool PartitionViewStep::isBackEnabled() const diff --git a/src/modules/partition/gui/PartitionViewStep.h b/src/modules/partition/gui/PartitionViewStep.h index 63d11c816..47479a135 100644 --- a/src/modules/partition/gui/PartitionViewStep.h +++ b/src/modules/partition/gui/PartitionViewStep.h @@ -78,6 +78,9 @@ private: void initPartitionCoreModule(); void continueLoading(); + /// "slot" for changes to next-status from the KPMCore and ChoicePage + void nextPossiblyChanged( bool ); + PartitionCoreModule* m_core; QStackedWidget* m_widget; ChoicePage* m_choicePage; From ef4c2666e1d964f2c42fd39ea365029289c44770 Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 29 Jul 2020 14:46:07 +0200 Subject: [PATCH 112/113] [partition] Update icons on all state changes The encryption widget (passphrase for disk encryption) should show ok / warning / error whenever the state changes; this avoids it showing up first with **no** icon (it should show a warning when both passphrases are empty). --- src/modules/partition/gui/EncryptWidget.cpp | 57 +++++++++++---------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/src/modules/partition/gui/EncryptWidget.cpp b/src/modules/partition/gui/EncryptWidget.cpp index 8fb9a7b30..5e44c15fd 100644 --- a/src/modules/partition/gui/EncryptWidget.cpp +++ b/src/modules/partition/gui/EncryptWidget.cpp @@ -91,9 +91,39 @@ EncryptWidget::retranslate() } +///@brief Give @p label the @p pixmap from the standard-pixmaps +static void +applyPixmap( QLabel* label, CalamaresUtils::ImageType pixmap ) +{ + label->setFixedWidth( label->height() ); + label->setPixmap( CalamaresUtils::defaultPixmap( pixmap, CalamaresUtils::Original, label->size() ) ); +} + void EncryptWidget::updateState() { + if ( m_ui->m_passphraseLineEdit->isVisible() ) + { + QString p1 = m_ui->m_passphraseLineEdit->text(); + QString p2 = m_ui->m_confirmLineEdit->text(); + + if ( p1.isEmpty() && p2.isEmpty() ) + { + applyPixmap( m_ui->m_iconLabel, CalamaresUtils::StatusWarning ); + m_ui->m_iconLabel->setToolTip( tr( "Please enter the same passphrase in both boxes." ) ); + } + else if ( p1 == p2 ) + { + applyPixmap( m_ui->m_iconLabel, CalamaresUtils::StatusOk ); + m_ui->m_iconLabel->setToolTip( QString() ); + } + else + { + applyPixmap( m_ui->m_iconLabel, CalamaresUtils::StatusError ); + m_ui->m_iconLabel->setToolTip( tr( "Please enter the same passphrase in both boxes." ) ); + } + } + Encryption newState; if ( m_ui->m_encryptCheckBox->isChecked() ) { @@ -119,14 +149,6 @@ EncryptWidget::updateState() } } -///@brief Give @p label the @p pixmap from the standard-pixmaps -static void -applyPixmap( QLabel* label, CalamaresUtils::ImageType pixmap ) -{ - label->setFixedWidth( label->height() ); - label->setPixmap( CalamaresUtils::defaultPixmap( pixmap, CalamaresUtils::Original, label->size() ) ); -} - void EncryptWidget::onPassphraseEdited() { @@ -135,25 +157,6 @@ EncryptWidget::onPassphraseEdited() m_ui->m_iconLabel->show(); } - QString p1 = m_ui->m_passphraseLineEdit->text(); - QString p2 = m_ui->m_confirmLineEdit->text(); - - m_ui->m_iconLabel->setToolTip( QString() ); - if ( p1.isEmpty() && p2.isEmpty() ) - { - applyPixmap( m_ui->m_iconLabel, CalamaresUtils::StatusWarning ); - m_ui->m_iconLabel->setToolTip( tr( "Please enter the same passphrase in both boxes." ) ); - } - else if ( p1 == p2 ) - { - applyPixmap( m_ui->m_iconLabel, CalamaresUtils::StatusOk ); - } - else - { - applyPixmap( m_ui->m_iconLabel, CalamaresUtils::StatusError ); - m_ui->m_iconLabel->setToolTip( tr( "Please enter the same passphrase in both boxes." ) ); - } - updateState(); } From 33fd5a1fad30b4d44c236335d8732923fbd2d12b Mon Sep 17 00:00:00 2001 From: Adriaan de Groot Date: Wed, 29 Jul 2020 17:56:30 +0200 Subject: [PATCH 113/113] [partition] Report a valid choice if a partition is selected --- src/modules/partition/gui/ChoicePage.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/partition/gui/ChoicePage.cpp b/src/modules/partition/gui/ChoicePage.cpp index 9f654b1bf..1d0c96623 100644 --- a/src/modules/partition/gui/ChoicePage.cpp +++ b/src/modules/partition/gui/ChoicePage.cpp @@ -1460,6 +1460,7 @@ ChoicePage::calculateNextEnabled() const cDebug() << "No partition selected"; return false; } + enabled = true; break; case Erase: case Manual: