diff --git a/src/libcalamares/CMakeLists.txt b/src/libcalamares/CMakeLists.txt index ff341bb56..dc05270af 100644 --- a/src/libcalamares/CMakeLists.txt +++ b/src/libcalamares/CMakeLists.txt @@ -57,6 +57,7 @@ set( libSources locale/Lookup.cpp locale/TimeZone.cpp locale/TranslatableConfiguration.cpp + locale/TranslatableString.cpp # Modules modulesystem/InstanceKey.cpp diff --git a/src/libcalamares/geoip/Interface.cpp b/src/libcalamares/geoip/Interface.cpp index f8649f37a..47c826e1a 100644 --- a/src/libcalamares/geoip/Interface.cpp +++ b/src/libcalamares/geoip/Interface.cpp @@ -47,7 +47,6 @@ splitTZString( const QString& tz ) QStringList tzParts = timezoneString.split( '/', SplitSkipEmptyParts ); if ( tzParts.size() >= 2 ) { - cDebug() << "GeoIP reporting" << timezoneString; QString region = tzParts.takeFirst(); QString zone = tzParts.join( '/' ); return RegionZonePair( region, zone ); diff --git a/src/libcalamares/locale/Tests.cpp b/src/libcalamares/locale/Tests.cpp index 10b4ad056..e498ac039 100644 --- a/src/libcalamares/locale/Tests.cpp +++ b/src/libcalamares/locale/Tests.cpp @@ -47,8 +47,12 @@ private Q_SLOTS: void testInterlingue(); // TimeZone testing + void testRegions(); void testSimpleZones(); void testComplexZones(); + void testTZLookup(); + void testTZIterator(); + void testLocationLookup(); }; LocaleTests::LocaleTests() {} @@ -244,58 +248,169 @@ LocaleTests::testTranslatableConfig2() QCOMPARE( ts3.count(), 1 ); // The empty string } +void +LocaleTests::testRegions() +{ + using namespace CalamaresUtils::Locale; + RegionsModel regions; + + QVERIFY( regions.rowCount( QModelIndex() ) > 3 ); // Africa, America, Asia + + QStringList names; + for ( int i = 0; i < regions.rowCount( QModelIndex() ); ++i ) + { + QVariant name = regions.data( regions.index( i ), RegionsModel::NameRole ); + QVERIFY( name.isValid() ); + QVERIFY( !name.toString().isEmpty() ); + names.append( name.toString() ); + } + + QVERIFY( names.contains( "America" ) ); + QVERIFY( !names.contains( "UTC" ) ); +} + + +static void +displayedNames( QAbstractItemModel& model, QStringList& names ) +{ + names.clear(); + for ( int i = 0; i < model.rowCount( QModelIndex() ); ++i ) + { + QVariant name = model.data( model.index( i, 0 ), Qt::DisplayRole ); + QVERIFY( name.isValid() ); + QVERIFY( !name.toString().isEmpty() ); + names.append( name.toString() ); + } +} + void LocaleTests::testSimpleZones() { using namespace CalamaresUtils::Locale; + ZonesModel zones; + QVERIFY( zones.rowCount( QModelIndex() ) > 24 ); + + QStringList names; + displayedNames( zones, names ); + QVERIFY( names.contains( "Amsterdam" ) ); + if ( !names.contains( "New York" ) ) { - TZRegion r; - QVERIFY( r.tr().isEmpty() ); - } - { - TZZone n; - QVERIFY( n.tr().isEmpty() ); - } - { - TZZone r0( "xAmsterdam" ); - QCOMPARE( r0.tr(), QStringLiteral( "xAmsterdam" ) ); - TZZone r1( r0 ); - QCOMPARE( r0.tr(), QStringLiteral( "xAmsterdam" ) ); - QCOMPARE( r1.tr(), QStringLiteral( "xAmsterdam" ) ); - TZZone r2( std::move( r0 ) ); - QCOMPARE( r2.tr(), QStringLiteral( "xAmsterdam" ) ); - QCOMPARE( r0.tr(), QString() ); - } - { - TZZone r0( nullptr ); - QVERIFY( r0.tr().isEmpty() ); - TZZone r1( r0 ); - QVERIFY( r1.tr().isEmpty() ); - TZZone r2( std::move( r0 ) ); - QVERIFY( r2.tr().isEmpty() ); + for ( const auto& s : names ) + { + if ( s.startsWith( 'N' ) ) + { + cDebug() << s; + } + } } + QVERIFY( names.contains( "New York" ) ); + QVERIFY( !names.contains( "America" ) ); + QVERIFY( !names.contains( "New_York" ) ); } void LocaleTests::testComplexZones() { using namespace CalamaresUtils::Locale; + ZonesModel zones; + RegionalZonesModel europe( &zones ); - { - TZZone r0( "America/New_York" ); - TZZone r1( "America/New York" ); + QStringList names; + displayedNames( zones, names ); + QVERIFY( names.contains( "New York" ) ); + QVERIFY( names.contains( "Prague" ) ); + QVERIFY( names.contains( "Abidjan" ) ); - QCOMPARE( r0.tr(), r1.tr() ); - QCOMPARE( r0.tr(), QStringLiteral( "America/New York" ) ); - } - { - TZZone r( "zxc,;*_vm" ); - QVERIFY( !r.tr().isEmpty() ); - QCOMPARE( r.tr(), QStringLiteral( "zxc,;* vm" ) ); // Only _ is special - } + // No region set + displayedNames( europe, names ); + QVERIFY( names.contains( "New York" ) ); + QVERIFY( names.contains( "Prague" ) ); + QVERIFY( names.contains( "Abidjan" ) ); + + // Now filter + europe.setRegion( "Europe" ); + displayedNames( europe, names ); + QVERIFY( !names.contains( "New York" ) ); + QVERIFY( names.contains( "Prague" ) ); + QVERIFY( !names.contains( "Abidjan" ) ); + + europe.setRegion( "America" ); + displayedNames( europe, names ); + QVERIFY( names.contains( "New York" ) ); + QVERIFY( !names.contains( "Prague" ) ); + QVERIFY( !names.contains( "Abidjan" ) ); + + europe.setRegion( "Africa" ); + displayedNames( europe, names ); + QVERIFY( !names.contains( "New York" ) ); + QVERIFY( !names.contains( "Prague" ) ); + QVERIFY( names.contains( "Abidjan" ) ); } +void +LocaleTests::testTZLookup() +{ + using namespace CalamaresUtils::Locale; + ZonesModel zones; + + QVERIFY( zones.find( "America", "New_York" ) ); + QCOMPARE( zones.find( "America", "New_York" )->zone(), QStringLiteral( "New_York" ) ); + QCOMPARE( zones.find( "America", "New_York" )->tr(), QStringLiteral( "New York" ) ); + + QVERIFY( !zones.find( "Europe", "New_York" ) ); + QVERIFY( !zones.find( "America", "New York" ) ); +} + +void +LocaleTests::testTZIterator() +{ + using namespace CalamaresUtils::Locale; + const ZonesModel zones; + + QVERIFY( zones.find( "Europe", "Rome" ) ); + + int count = 0; + bool seenRome = false; + bool seenGnome = false; + for ( auto it = zones.begin(); it; ++it ) + { + QVERIFY( *it ); + QVERIFY( !( *it )->zone().isEmpty() ); + seenRome |= ( *it )->zone() == QStringLiteral( "Rome" ); + seenGnome |= ( *it )->zone() == QStringLiteral( "Gnome" ); + count++; + } + + QVERIFY( seenRome ); + QVERIFY( !seenGnome ); + QCOMPARE( count, zones.rowCount( QModelIndex() ) ); + + QCOMPARE( zones.data( zones.index( 0 ), ZonesModel::RegionRole ).toString(), QStringLiteral( "Africa" ) ); + QCOMPARE( ( *zones.begin() )->zone(), QStringLiteral( "Abidjan" ) ); +} + +void +LocaleTests::testLocationLookup() +{ + const CalamaresUtils::Locale::ZonesModel zones; + + const auto* zone = zones.find( 50.0, 0.0 ); + QVERIFY( zone ); + QCOMPARE( zone->zone(), QStringLiteral( "London" ) ); + + + // Tarawa is close to "the other side of the world" from London + zone = zones.find( 0.0, 179.0 ); + QVERIFY( zone ); + QCOMPARE( zone->zone(), QStringLiteral( "Tarawa" ) ); + + zone = zones.find( 0.0, -179.0 ); + QVERIFY( zone ); + QCOMPARE( zone->zone(), QStringLiteral( "Tarawa" ) ); +} + + QTEST_GUILESS_MAIN( LocaleTests ) #include "utils/moc-warnings.h" diff --git a/src/libcalamares/locale/TimeZone.cpp b/src/libcalamares/locale/TimeZone.cpp index 772a242fb..e2779281f 100644 --- a/src/libcalamares/locale/TimeZone.cpp +++ b/src/libcalamares/locale/TimeZone.cpp @@ -22,17 +22,28 @@ #include "TimeZone.h" +#include "locale/TranslatableString.h" #include "utils/Logger.h" #include "utils/String.h" #include -#include -#include - -#include +#include static const char TZ_DATA_FILE[] = "/usr/share/zoneinfo/zone.tab"; +namespace CalamaresUtils +{ +namespace Locale +{ +class RegionData; +using RegionVector = QVector< RegionData* >; +using ZoneVector = QVector< TimeZoneData* >; + +/** @brief Turns a string longitude or latitude notation into a double + * + * This handles strings like "+4230+00131" from zone.tab, + * which is degrees-and-minutes notation, and + means north or east. + */ static double getRightGeoLocation( QString str ) { @@ -62,252 +73,381 @@ getRightGeoLocation( QString str ) } -namespace CalamaresUtils -{ -namespace Locale -{ - - -CStringPair::CStringPair( CStringPair&& t ) - : m_human( nullptr ) - , m_key() -{ - // My pointers are initialized to nullptr - std::swap( m_human, t.m_human ); - std::swap( m_key, t.m_key ); -} - -CStringPair::CStringPair( const CStringPair& t ) - : m_human( t.m_human ? strdup( t.m_human ) : nullptr ) - , m_key( t.m_key ) -{ -} - -/** @brief Massage an identifier into a human-readable form - * - * Makes a copy of @p s, caller must free() it. - */ -static char* -munge( const char* s ) -{ - char* t = strdup( s ); - if ( !t ) - { - return nullptr; - } - - // replace("_"," ") in the Python script - char* p = t; - while ( *p ) - { - if ( ( *p ) == '_' ) - { - *p = ' '; - } - ++p; - } - - return t; -} - -CStringPair::CStringPair( const char* s1 ) - : m_human( s1 ? munge( s1 ) : nullptr ) - , m_key( s1 ? QString( s1 ) : QString() ) -{ -} - - -CStringPair::~CStringPair() -{ - free( m_human ); -} - - -QString -TZRegion::tr() const -{ - // NOTE: context name must match what's used in zone-extractor.py - return QObject::tr( m_human, "tz_regions" ); -} - -TZRegion::~TZRegion() -{ - qDeleteAll( m_zones ); -} - -const CStringPairList& -TZRegion::fromZoneTab() -{ - static CStringPairList zoneTab = TZRegion::fromFile( TZ_DATA_FILE ); - return zoneTab; -} - -CStringPairList -TZRegion::fromFile( const char* fileName ) -{ - CStringPairList model; - - QFile file( fileName ); - if ( !file.open( QIODevice::ReadOnly | QIODevice::Text ) ) - { - return model; - } - - TZRegion* thisRegion = nullptr; - QTextStream in( &file ); - while ( !in.atEnd() ) - { - QString line = in.readLine().trimmed().split( '#', SplitKeepEmptyParts ).first().trimmed(); - if ( line.isEmpty() ) - { - continue; - } - - QStringList list = line.split( QRegExp( "[\t ]" ), SplitSkipEmptyParts ); - if ( list.size() < 3 ) - { - continue; - } - - QStringList timezoneParts = list.at( 2 ).split( '/', SplitSkipEmptyParts ); - if ( timezoneParts.size() < 2 ) - { - continue; - } - - QString region = timezoneParts.first().trimmed(); - if ( region.isEmpty() ) - { - continue; - } - - auto keyMatch = [®ion]( const CStringPair* r ) { return r->key() == region; }; - auto it = std::find_if( model.begin(), model.end(), keyMatch ); - if ( it != model.end() ) - { - thisRegion = dynamic_cast< TZRegion* >( *it ); - } - else - { - thisRegion = new TZRegion( region.toUtf8().data() ); - model.append( thisRegion ); - } - - QString countryCode = list.at( 0 ).trimmed(); - if ( countryCode.size() != 2 ) - { - continue; - } - - timezoneParts.removeFirst(); - thisRegion->m_zones.append( - new TZZone( region, timezoneParts.join( '/' ).toUtf8().constData(), countryCode, list.at( 1 ) ) ); - } - - auto sorter = []( const CStringPair* l, const CStringPair* r ) { return *l < *r; }; - std::sort( model.begin(), model.end(), sorter ); - for ( auto& it : model ) - { - TZRegion* r = dynamic_cast< TZRegion* >( it ); - if ( r ) - { - std::sort( r->m_zones.begin(), r->m_zones.end(), sorter ); - } - } - - return model; -} - -TZZone::TZZone( const QString& region, const char* zoneName, const QString& country, QString position ) - : CStringPair( zoneName ) +TimeZoneData::TimeZoneData( const QString& region, + const QString& zone, + const QString& country, + double latitude, + double longitude ) + : TranslatableString( zone ) , m_region( region ) , m_country( country ) + , m_latitude( latitude ) + , m_longitude( longitude ) { - int cooSplitPos = position.indexOf( QRegExp( "[-+]" ), 1 ); - if ( cooSplitPos > 0 ) - { - m_latitude = getRightGeoLocation( position.mid( 0, cooSplitPos ) ); - m_longitude = getRightGeoLocation( position.mid( cooSplitPos ) ); - } + setObjectName( region + '/' + zone ); } QString -TZZone::tr() const +TimeZoneData::tr() const { // NOTE: context name must match what's used in zone-extractor.py return QObject::tr( m_human, "tz_names" ); } -CStringListModel::CStringListModel( CStringPairList l ) - : m_list( l ) +class RegionData : public TranslatableString +{ +public: + using TranslatableString::TranslatableString; + QString tr() const override; +}; + +QString +RegionData::tr() const +{ + // NOTE: context name must match what's used in zone-extractor.py + return QObject::tr( m_human, "tz_regions" ); +} + +static void +loadTZData( RegionVector& regions, ZoneVector& zones ) +{ + QFile file( TZ_DATA_FILE ); + if ( file.open( QIODevice::ReadOnly | QIODevice::Text ) ) + { + QTextStream in( &file ); + while ( !in.atEnd() ) + { + QString line = in.readLine().trimmed().split( '#', SplitKeepEmptyParts ).first().trimmed(); + if ( line.isEmpty() ) + { + continue; + } + + QStringList list = line.split( QRegExp( "[\t ]" ), SplitSkipEmptyParts ); + if ( list.size() < 3 ) + { + continue; + } + + QStringList timezoneParts = list.at( 2 ).split( '/', SplitSkipEmptyParts ); + if ( timezoneParts.size() < 2 ) + { + continue; + } + + QString region = timezoneParts.first().trimmed(); + if ( region.isEmpty() ) + { + continue; + } + + QString countryCode = list.at( 0 ).trimmed(); + if ( countryCode.size() != 2 ) + { + continue; + } + + timezoneParts.removeFirst(); + QString zone = timezoneParts.join( '/' ); + if ( zone.length() < 2 ) + { + continue; + } + + QString position = list.at( 1 ); + int cooSplitPos = position.indexOf( QRegExp( "[-+]" ), 1 ); + double latitude; + double longitude; + if ( cooSplitPos > 0 ) + { + latitude = getRightGeoLocation( position.mid( 0, cooSplitPos ) ); + longitude = getRightGeoLocation( position.mid( cooSplitPos ) ); + } + else + { + continue; + } + + // Now we have region, zone, country, lat and longitude + const RegionData* existingRegion = nullptr; + for ( const auto* p : regions ) + { + if ( p->key() == region ) + { + existingRegion = p; + break; + } + } + if ( !existingRegion ) + { + regions.append( new RegionData( region ) ); + } + zones.append( new TimeZoneData( region, zone, countryCode, latitude, longitude ) ); + } + } +} + + +class Private : public QObject +{ + Q_OBJECT +public: + RegionVector m_regions; + ZoneVector m_zones; + + Private() + { + m_regions.reserve( 12 ); // reasonable guess + m_zones.reserve( 452 ); // wc -l /usr/share/zoneinfo/zone.tab + + loadTZData( m_regions, m_zones ); + + std::sort( m_regions.begin(), m_regions.end(), []( const RegionData* lhs, const RegionData* rhs ) { + return lhs->key() < rhs->key(); + } ); + std::sort( m_zones.begin(), m_zones.end(), []( const TimeZoneData* lhs, const TimeZoneData* rhs ) { + if ( lhs->region() == rhs->region() ) + { + return lhs->zone() < rhs->zone(); + } + return lhs->region() < rhs->region(); + } ); + + for ( auto* z : m_zones ) + { + z->setParent( this ); + } + } +}; + +static Private* +privateInstance() +{ + static Private* s_p = new Private; + return s_p; +} + +RegionsModel::RegionsModel( QObject* parent ) + : QAbstractListModel( parent ) + , m_private( privateInstance() ) { } -void -CStringListModel::setList( CalamaresUtils::Locale::CStringPairList l ) -{ - beginResetModel(); - m_list = l; - endResetModel(); -} +RegionsModel::~RegionsModel() {} int -CStringListModel::rowCount( const QModelIndex& ) const +RegionsModel::rowCount( const QModelIndex& ) const { - return m_list.count(); + return m_private->m_regions.count(); } QVariant -CStringListModel::data( const QModelIndex& index, int role ) const +RegionsModel::data( const QModelIndex& index, int role ) const { - if ( ( role != Qt::DisplayRole ) && ( role != Qt::UserRole ) ) + if ( !index.isValid() || index.row() < 0 || index.row() >= m_private->m_regions.count() ) { return QVariant(); } - if ( !index.isValid() ) + const auto& region = m_private->m_regions[ index.row() ]; + if ( role == NameRole ) { - return QVariant(); + return region->tr(); } - - const auto* item = m_list.at( index.row() ); - return item ? ( role == Qt::DisplayRole ? item->tr() : item->key() ) : QVariant(); -} - -void -CStringListModel::setCurrentIndex( int index ) -{ - if ( ( index < 0 ) || ( index >= m_list.count() ) ) + if ( role == KeyRole ) { - return; + return region->key(); } - - m_currentIndex = index; - emit currentIndexChanged(); -} - -int -CStringListModel::currentIndex() const -{ - return m_currentIndex; + return QVariant(); } QHash< int, QByteArray > -CStringListModel::roleNames() const +RegionsModel::roleNames() const { - return { { Qt::DisplayRole, "label" }, { Qt::UserRole, "key" } }; + return { { NameRole, "name" }, { KeyRole, "key" } }; } -const CStringPair* -CStringListModel::item( int index ) const +QString +RegionsModel::tr( const QString& region ) const { - if ( ( index < 0 ) || ( index >= m_list.count() ) ) + for ( const auto* p : m_private->m_regions ) { - return nullptr; + if ( p->key() == region ) + { + return p->tr(); + } } - return m_list[ index ]; + return region; } +ZonesModel::ZonesModel( QObject* parent ) + : QAbstractListModel( parent ) + , m_private( privateInstance() ) +{ +} + +ZonesModel::~ZonesModel() {} + +int +ZonesModel::rowCount( const QModelIndex& ) const +{ + return m_private->m_zones.count(); +} + +QVariant +ZonesModel::data( const QModelIndex& index, int role ) const +{ + if ( !index.isValid() || index.row() < 0 || index.row() >= m_private->m_zones.count() ) + { + return QVariant(); + } + + const auto* zone = m_private->m_zones[ index.row() ]; + switch ( role ) + { + case NameRole: + return zone->tr(); + case KeyRole: + return zone->key(); + case RegionRole: + return zone->region(); + default: + return QVariant(); + } +} + +QHash< int, QByteArray > +ZonesModel::roleNames() const +{ + return { { NameRole, "name" }, { KeyRole, "key" } }; +} + +const TimeZoneData* +ZonesModel::find( const QString& region, const QString& zone ) const +{ + for ( const auto* p : m_private->m_zones ) + { + if ( p->region() == region && p->zone() == zone ) + { + return p; + } + } + return nullptr; +} + +const TimeZoneData* +ZonesModel::find( double latitude, double longitude ) const +{ + /* This is a somewhat derpy way of finding "closest", + * in that it considers one degree of separation + * either N/S or E/W equal to any other; this obviously + * falls apart at the poles. + */ + + double largestDifference = 720.0; + const TimeZoneData* closest = nullptr; + + for ( const auto* zone : m_private->m_zones ) + { + // Latitude doesn't wrap around: there is nothing north of 90 + double latitudeDifference = abs( zone->latitude() - latitude ); + + // Longitude **does** wrap around, so consider the case of -178 and 178 + // which differ by 4 degrees. + double westerly = qMin( zone->longitude(), longitude ); + double easterly = qMax( zone->longitude(), longitude ); + double longitudeDifference = 0.0; + if ( westerly < 0.0 && !( easterly < 0.0 ) ) + { + // Only if they're different signs can we have wrap-around. + longitudeDifference = qMin( abs( westerly - easterly ), abs( 360.0 + westerly - easterly ) ); + } + else + { + longitudeDifference = abs( westerly - easterly ); + } + + if ( latitudeDifference + longitudeDifference < largestDifference ) + { + largestDifference = latitudeDifference + longitudeDifference; + closest = zone; + } + } + return closest; +} + +QObject* +ZonesModel::lookup( double latitude, double longitude ) const +{ + const auto* p = find( latitude, longitude ); + if ( !p ) + { + p = find( "America", "New_York" ); + } + if ( !p ) + { + cWarning() << "No zone (not even New York) found, expect crashes."; + } + return const_cast< QObject* >( reinterpret_cast< const QObject* >( p ) ); +} + + +ZonesModel::Iterator::operator bool() const +{ + return 0 <= m_index && m_index < m_p->m_zones.count(); +} + +const TimeZoneData* ZonesModel::Iterator::operator*() const +{ + if ( *this ) + { + return m_p->m_zones[ m_index ]; + } + return nullptr; +} + +RegionalZonesModel::RegionalZonesModel( CalamaresUtils::Locale::ZonesModel* source, QObject* parent ) + : QSortFilterProxyModel( parent ) + , m_private( privateInstance() ) +{ + setSourceModel( source ); +} + +RegionalZonesModel::~RegionalZonesModel() {} + +void +RegionalZonesModel::setRegion( const QString& r ) +{ + if ( r != m_region ) + { + m_region = r; + invalidateFilter(); + emit regionChanged( r ); + } +} + +bool +RegionalZonesModel::filterAcceptsRow( int sourceRow, const QModelIndex& ) const +{ + if ( m_region.isEmpty() ) + { + return true; + } + + if ( sourceRow < 0 || sourceRow >= m_private->m_zones.count() ) + { + return false; + } + + const auto& zone = m_private->m_zones[ sourceRow ]; + return ( zone->m_region == m_region ); +} + + } // namespace Locale } // namespace CalamaresUtils + +#include "utils/moc-warnings.h" + +#include "TimeZone.moc" diff --git a/src/libcalamares/locale/TimeZone.h b/src/libcalamares/locale/TimeZone.h index 05820817a..1d4b4a8ff 100644 --- a/src/libcalamares/locale/TimeZone.h +++ b/src/libcalamares/locale/TimeZone.h @@ -24,184 +24,198 @@ #include "DllMacro.h" -#include "utils/Logger.h" +#include "locale/TranslatableString.h" #include #include -#include - -#include +#include +#include namespace CalamaresUtils { namespace Locale { +class Private; +class RegionalZonesModel; +class ZonesModel; -/** @brief A pair of strings, one human-readable, one a key - * - * Given an identifier-like string (e.g. "New_York"), makes - * a human-readable version of that and keeps a copy of the - * identifier itself. - * - * This explicitly uses const char* instead of just being - * QPair because there is API that needs - * C-style strings. - */ -class CStringPair : public QObject +class TimeZoneData : public QObject, TranslatableString { + friend class RegionalZonesModel; + friend class ZonesModel; + Q_OBJECT + + Q_PROPERTY( QString region READ region CONSTANT ) + Q_PROPERTY( QString zone READ zone CONSTANT ) + Q_PROPERTY( QString name READ tr CONSTANT ) + Q_PROPERTY( QString countryCode READ country CONSTANT ) + public: - /// @brief An empty pair - CStringPair() {} - /// @brief Given an identifier, create the pair - explicit CStringPair( const char* s1 ); - CStringPair( CStringPair&& t ); - CStringPair( const CStringPair& ); - virtual ~CStringPair(); + TimeZoneData( const QString& region, + const QString& zone, + const QString& country, + double latitude, + double longitude ); + TimeZoneData( const TimeZoneData& ) = delete; + TimeZoneData( TimeZoneData&& ) = delete; - /// @brief Give the localized human-readable form - virtual QString tr() const = 0; - QString key() const { return m_key; } - - bool operator<( const CStringPair& other ) const { return m_key < other.m_key; } - -protected: - char* m_human = nullptr; - QString m_key; -}; - -class CStringPairList : public QList< CStringPair* > -{ -public: - template < typename T > - T* find( const QString& key ) const - { - for ( auto* p : *this ) - { - if ( p->key() == key ) - { - return dynamic_cast< T* >( p ); - } - } - return nullptr; - } -}; - -/** @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 -public: - using CStringPair::CStringPair; - virtual ~TZRegion() override; - TZRegion( const TZRegion& ) = delete; QString tr() const override; - QString region() const { return key(); } - - /** @brief Create list from a zone.tab-like file - * - * Returns a list of all the regions; each region has a list - * of zones within that region. Dyamically, the items in the - * returned list are TZRegions; their zones dynamically are - * TZZones even though all those lists have type CStringPairList. - * - * The list owns the regions, and the regions own their own list of zones. - * When getting rid of the list, remember to qDeleteAll() on it. - */ - static CStringPairList fromFile( const char* fileName ); - /// @brief Calls fromFile with the standard zone.tab name - static const CStringPairList& fromZoneTab(); - - const CStringPairList& zones() const { return m_zones; } - -private: - CStringPairList m_zones; -}; - -/** @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 -public: - using CStringPair::CStringPair; - QString tr() const override; - - TZZone( const QString& region, const char* zoneName, const QString& country, QString position ); - QString region() const { return m_region; } QString zone() const { return key(); } + QString country() const { return m_country; } double latitude() const { return m_latitude; } double longitude() const { return m_longitude; } -protected: +private: QString m_region; QString m_country; - double m_latitude = 0.0, m_longitude = 0.0; + double m_latitude; + double m_longitude; }; -class CStringListModel : public QAbstractListModel + +/** @brief The list of timezone regions + * + * The regions are a short list of global areas (Africa, America, India ..) + * which contain zones. + */ +class DLLEXPORT RegionsModel : public QAbstractListModel { Q_OBJECT - Q_PROPERTY( int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged ) public: - /// @brief Create empty model - CStringListModel() {} - /// @brief Create model from list (non-owning) - CStringListModel( CStringPairList ); + enum Roles + { + NameRole = Qt::DisplayRole, + KeyRole = Qt::UserRole // So that currentData() will get the key + }; + + RegionsModel( QObject* parent = nullptr ); + virtual ~RegionsModel() override; int rowCount( const QModelIndex& parent ) const override; - QVariant data( const QModelIndex& index, int role ) const override; - const CStringPair* item( int index ) const; QHash< int, QByteArray > roleNames() const override; - void setCurrentIndex( int index ); - int currentIndex() const; - - void setList( CStringPairList ); - - inline int indexOf( const QString& key ) - { - const auto it = std::find_if( - m_list.constBegin(), m_list.constEnd(), [&]( const CalamaresUtils::Locale::CStringPair* item ) -> bool { - return item->key() == key; - } ); - - if ( it != m_list.constEnd() ) - { - // distance() is usually a long long - return int( std::distance( m_list.constBegin(), it ) ); - } - else - { - return -1; - } - } - +public Q_SLOTS: + /** @brief Provides a human-readable version of the region + * + * Returns @p region unchanged if there is no such region + * or no translation for the region's name. + */ + QString tr( const QString& region ) const; private: - CStringPairList m_list; - int m_currentIndex = -1; + Private* m_private; +}; + +class DLLEXPORT ZonesModel : public QAbstractListModel +{ + Q_OBJECT + +public: + enum Roles + { + NameRole = Qt::DisplayRole, + KeyRole = Qt::UserRole, // So that currentData() will get the key + RegionRole = Qt::UserRole + 1 + }; + + ZonesModel( QObject* parent = nullptr ); + virtual ~ZonesModel() override; + + int rowCount( const QModelIndex& parent ) const override; + QVariant data( const QModelIndex& index, int role ) const override; + + QHash< int, QByteArray > roleNames() const override; + + /** @brief Iterator for the underlying list of zones + * + * Iterates over all the zones in the model. Operator * may return + * a @c nullptr when the iterator is not valid. Typical usage: + * + * ``` + * for( auto it = model.begin(); it; ++it ) + * { + * const auto* zonedata = *it; + * ... + * } + */ + class Iterator + { + friend class ZonesModel; + Iterator( const Private* m ) + : m_index( 0 ) + , m_p( m ) + { + } + + public: + operator bool() const; + void operator++() { ++m_index; } + const TimeZoneData* operator*() const; + int index() const { return m_index; } + + private: + int m_index; + const Private* m_p; + }; + + Iterator begin() const { return Iterator( m_private ); } + +public Q_SLOTS: + /** @brief Look up TZ data based on its name. + * + * Returns @c nullptr if not found. + */ + const TimeZoneData* find( const QString& region, const QString& zone ) const; + + /** @brief Look up TZ data based on the location. + * + * Returns the nearest zone to the given lat and lon. + */ + const TimeZoneData* find( double latitude, double longitude ) const; + + /** @brief Look up TZ data based on the location. + * + * Returns the nearest zone, or New York. This is non-const for QML + * purposes, but the object should be considered const anyway. + */ + QObject* lookup( double latitude, double longitude ) const; + +private: + Private* m_private; +}; + +class DLLEXPORT RegionalZonesModel : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY( QString region READ region WRITE setRegion NOTIFY regionChanged ) + +public: + RegionalZonesModel( ZonesModel* source, QObject* parent = nullptr ); + ~RegionalZonesModel() override; + + bool filterAcceptsRow( int sourceRow, const QModelIndex& sourceParent ) const override; + + QString region() const { return m_region; } + +public Q_SLOTS: + void setRegion( const QString& r ); signals: - void currentIndexChanged(); + void regionChanged( const QString& ); + +private: + Private* m_private; + QString m_region; }; + } // namespace Locale } // namespace CalamaresUtils diff --git a/src/libcalamares/locale/TranslatableString.cpp b/src/libcalamares/locale/TranslatableString.cpp new file mode 100644 index 000000000..9200c8d65 --- /dev/null +++ b/src/libcalamares/locale/TranslatableString.cpp @@ -0,0 +1,89 @@ +/* === 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 + * 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 "TranslatableString.h" + + +/** @brief Massage an identifier into a human-readable form + * + * Makes a copy of @p s, caller must free() it. + */ +static char* +munge( const char* s ) +{ + char* t = strdup( s ); + if ( !t ) + { + return nullptr; + } + + // replace("_"," ") in the Python script + char* p = t; + while ( *p ) + { + if ( ( *p ) == '_' ) + { + *p = ' '; + } + ++p; + } + + return t; +} + +namespace CalamaresUtils +{ +namespace Locale +{ + +TranslatableString::TranslatableString( TranslatableString&& t ) + : m_human( nullptr ) + , m_key() +{ + // My pointers are initialized to nullptr + std::swap( m_human, t.m_human ); + std::swap( m_key, t.m_key ); +} + +TranslatableString::TranslatableString( const TranslatableString& t ) + : m_human( t.m_human ? strdup( t.m_human ) : nullptr ) + , m_key( t.m_key ) +{ +} + +TranslatableString::TranslatableString( const char* s1 ) + : m_human( s1 ? munge( s1 ) : nullptr ) + , m_key( s1 ? QString( s1 ) : QString() ) +{ +} + +TranslatableString::TranslatableString( const QString& s ) + : m_human( munge( s.toUtf8().constData() ) ) + , m_key( s ) +{ +} + + +TranslatableString::~TranslatableString() +{ + free( m_human ); +} + +} // namespace Locale +} // namespace CalamaresUtils diff --git a/src/libcalamares/locale/TranslatableString.h b/src/libcalamares/locale/TranslatableString.h new file mode 100644 index 000000000..8347488f1 --- /dev/null +++ b/src/libcalamares/locale/TranslatableString.h @@ -0,0 +1,68 @@ +/* === 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 + * 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 LOCALE_TRANSLATABLESTRING_H +#define LOCALE_TRANSLATABLESTRING_H + +#include + +namespace CalamaresUtils +{ +namespace Locale +{ + +/** @brief A pair of strings, one human-readable, one a key + * + * Given an identifier-like string (e.g. "New_York"), makes + * a human-readable version of that and keeps a copy of the + * identifier itself. + * + * This explicitly uses const char* instead of just being + * QPair because the human-readable part + * may need to be translated through tr(), and that takes a char* + * C-style strings. + */ +class TranslatableString +{ +public: + /// @brief An empty pair + TranslatableString() {} + /// @brief Given an identifier, create the pair + explicit TranslatableString( const char* s1 ); + explicit TranslatableString( const QString& s ); + TranslatableString( TranslatableString&& t ); + TranslatableString( const TranslatableString& ); + virtual ~TranslatableString(); + + /// @brief Give the localized human-readable form + virtual QString tr() const = 0; + QString key() const { return m_key; } + + bool operator==( const TranslatableString& other ) const { return m_key == other.m_key; } + bool operator<( const TranslatableString& other ) const { return m_key < other.m_key; } + +protected: + char* m_human = nullptr; + QString m_key; +}; + +} // namespace Locale +} // namespace CalamaresUtils + +#endif diff --git a/src/modules/locale/CMakeLists.txt b/src/modules/locale/CMakeLists.txt index a09bde282..6f965f041 100644 --- a/src/modules/locale/CMakeLists.txt +++ b/src/modules/locale/CMakeLists.txt @@ -36,7 +36,9 @@ calamares_add_test( localetest SOURCES Tests.cpp + Config.cpp LocaleConfiguration.cpp + SetTimezoneJob.cpp timezonewidget/TimeZoneImage.cpp DEFINITIONS SOURCE_DIR="${CMAKE_CURRENT_LIST_DIR}/images" diff --git a/src/modules/locale/Config.cpp b/src/modules/locale/Config.cpp index 7a49525f2..5bbe20038 100644 --- a/src/modules/locale/Config.cpp +++ b/src/modules/locale/Config.cpp @@ -148,17 +148,50 @@ loadLocales( const QString& localeGenPath ) return localeGenLines; } -static inline const CalamaresUtils::Locale::CStringPairList& -timezoneData() +static bool +updateGSLocation( Calamares::GlobalStorage* gs, const CalamaresUtils::Locale::TimeZoneData* location ) { - return CalamaresUtils::Locale::TZRegion::fromZoneTab(); + const QString regionKey = QStringLiteral( "locationRegion" ); + const QString zoneKey = QStringLiteral( "locationZone" ); + + if ( !location ) + { + if ( gs->contains( regionKey ) || gs->contains( zoneKey ) ) + { + gs->remove( regionKey ); + gs->remove( zoneKey ); + return true; + } + return false; + } + + // Update the GS region and zone (and possibly the live timezone) + bool locationChanged + = ( location->region() != gs->value( regionKey ) ) || ( location->zone() != gs->value( zoneKey ) ); + + gs->insert( regionKey, location->region() ); + gs->insert( zoneKey, location->zone() ); + + return locationChanged; } +static void +updateGSLocale( Calamares::GlobalStorage* gs, const LocaleConfiguration& locale ) +{ + auto map = locale.toMap(); + QVariantMap vm; + for ( auto it = map.constBegin(); it != map.constEnd(); ++it ) + { + vm.insert( it.key(), it.value() ); + } + gs->insert( "localeConf", vm ); +} Config::Config( QObject* parent ) : QObject( parent ) - , m_regionModel( std::make_unique< CalamaresUtils::Locale::CStringListModel >( ::timezoneData() ) ) - , m_zonesModel( std::make_unique< CalamaresUtils::Locale::CStringListModel >() ) + , m_regionModel( std::make_unique< CalamaresUtils::Locale::RegionsModel >() ) + , m_zonesModel( std::make_unique< CalamaresUtils::Locale::ZonesModel >() ) + , m_regionalZonesModel( std::make_unique< CalamaresUtils::Locale::RegionalZonesModel >( m_zonesModel.get() ) ) { // Slightly unusual: connect to our *own* signals. Wherever the language // or the location is changed, these signals are emitted, so hook up to @@ -172,32 +205,21 @@ Config::Config( QObject* parent ) } ); 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 ); + updateGSLocale( Calamares::JobQueue::instance()->globalStorage(), localeConfiguration() ); } ); connect( this, &Config::currentLocationChanged, [&]() { - auto* gs = Calamares::JobQueue::instance()->globalStorage(); + const bool locationChanged + = updateGSLocation( Calamares::JobQueue::instance()->globalStorage(), currentLocation() ); - // 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() } ); + { "set-timezone", currentTimezoneCode() } ); } + + emit currentTimezoneCodeChanged( currentTimezoneCode() ); + emit currentTimezoneNameChanged( currentTimezoneName() ); } ); auto prettyStatusNotify = [&]() { emit prettyStatusChanged( prettyStatus() ); }; @@ -208,12 +230,6 @@ Config::Config( QObject* parent ) Config::~Config() {} -const CalamaresUtils::Locale::CStringPairList& -Config::timezoneData() const -{ - return ::timezoneData(); -} - void Config::setCurrentLocation() { @@ -223,7 +239,8 @@ Config::setCurrentLocation() } } -void Config::setCurrentLocation(const QString& regionzone) +void +Config::setCurrentLocation( const QString& regionzone ) { auto r = CalamaresUtils::GeoIP::splitTZString( regionzone ); if ( r.isValid() ) @@ -236,8 +253,7 @@ 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; + auto* zone = m_zonesModel->find( regionName, zoneName ); if ( zone ) { setCurrentLocation( zone ); @@ -250,7 +266,7 @@ Config::setCurrentLocation( const QString& regionName, const QString& zoneName ) } void -Config::setCurrentLocation( const CalamaresUtils::Locale::TZZone* location ) +Config::setCurrentLocation( const CalamaresUtils::Locale::TimeZoneData* location ) { if ( location != m_currentLocation ) { @@ -277,6 +293,7 @@ Config::setCurrentLocation( const CalamaresUtils::Locale::TZZone* location ) emit currentLCStatusChanged( currentLCStatus() ); } emit currentLocationChanged( m_currentLocation ); + // Other signals come from the LocationChanged signal } } @@ -330,9 +347,32 @@ Config::setLCLocaleExplicitly( const QString& locale ) QString Config::currentLocationStatus() const { - return tr( "Set timezone to %1/%2." ).arg( m_currentLocation->region(), m_currentLocation->zone() ); + return tr( "Set timezone to %1/%2." ) + .arg( m_currentLocation ? m_currentLocation->region() : QString(), + m_currentLocation ? m_currentLocation->zone() : QString() ); } +QString +Config::currentTimezoneCode() const +{ + if ( m_currentLocation ) + { + return m_currentLocation->region() + '/' + m_currentLocation->zone(); + } + return QString(); +} + +QString +Config::currentTimezoneName() const +{ + if ( m_currentLocation ) + { + return m_regionModel->tr( m_currentLocation->region() ) + '/' + m_currentLocation->tr(); + } + return QString(); +} + + static inline QString localeLabel( const QString& s ) { @@ -380,7 +420,7 @@ getAdjustLiveTimezone( const QVariantMap& configurationMap, bool& adjustLiveTime adjustLiveTimezone = CalamaresUtils::getBool( configurationMap, "adjustLiveTimezone", Calamares::Settings::instance()->doChroot() ); #ifdef DEBUG_TIMEZONES - if ( m_adjustLiveTimezone ) + if ( adjustLiveTimezone ) { cWarning() << "Turning off live-timezone adjustments because debugging is on."; adjustLiveTimezone = false; @@ -448,18 +488,20 @@ Config::setConfigurationMap( const QVariantMap& configurationMap ) getStartingTimezone( configurationMap, m_startingTimezone ); getGeoIP( configurationMap, m_geoip ); +#ifndef BUILD_AS_TEST if ( m_geoip && m_geoip->isValid() ) { connect( Calamares::ModuleManager::instance(), &Calamares::ModuleManager::modulesLoaded, this, &Config::startGeoIP ); } +#endif } Calamares::JobList Config::createJobs() { Calamares::JobList list; - const CalamaresUtils::Locale::TZZone* location = currentLocation(); + const auto* location = currentLocation(); if ( location ) { @@ -470,6 +512,15 @@ Config::createJobs() return list; } +void +Config::finalizeGlobalStorage() const +{ + auto* gs = Calamares::JobQueue::instance()->globalStorage(); + updateGSLocale( gs, localeConfiguration() ); + updateGSLocation( gs, currentLocation() ); +} + + void Config::startGeoIP() { diff --git a/src/modules/locale/Config.h b/src/modules/locale/Config.h index e9a8e6373..d95606d99 100644 --- a/src/modules/locale/Config.h +++ b/src/modules/locale/Config.h @@ -37,18 +37,25 @@ 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 ) + Q_PROPERTY( CalamaresUtils::Locale::RegionsModel* regionModel READ regionModel CONSTANT FINAL ) + Q_PROPERTY( CalamaresUtils::Locale::ZonesModel* zonesModel READ zonesModel CONSTANT FINAL ) + Q_PROPERTY( QAbstractItemModel* regionalZonesModel READ regionalZonesModel CONSTANT FINAL ) - Q_PROPERTY( const CalamaresUtils::Locale::TZZone* currentLocation READ currentLocation WRITE setCurrentLocation - NOTIFY currentLocationChanged ) + Q_PROPERTY( + CalamaresUtils::Locale::TimeZoneData* currentLocation READ currentLocation_c 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 ) + // Name are shorter human-readable names + // .. main difference is that status is a full sentence, like "Timezone is America/New York" + // while name is just "America/New York" (and the code, below, is "America/New_York") + Q_PROPERTY( QString currentTimezoneName READ currentTimezoneName NOTIFY currentTimezoneNameChanged ) // Code are internal identifiers, like "en_US.UTF-8" - Q_PROPERTY( QString currentLanguageCode READ currentLanguageCode WRITE setLanguageExplicitly NOTIFY currentLanguageCodeChanged ) + Q_PROPERTY( QString currentTimezoneCode READ currentTimezoneCode NOTIFY currentTimezoneCodeChanged ) + 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 @@ -59,17 +66,9 @@ public: ~Config(); void setConfigurationMap( const QVariantMap& ); + void finalizeGlobalStorage() const; Calamares::JobList createJobs(); - // 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. - */ - 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 @@ -85,13 +84,27 @@ public: /// The human-readable summary of what the module will do QString prettyStatus() const; + // A long list of locale codes (e.g. en_US.UTF-8) 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(); } + // All the regions (Africa, America, ...) + CalamaresUtils::Locale::RegionsModel* regionModel() const { return m_regionModel.get(); } + // All of the timezones in the world, according to zone.tab + CalamaresUtils::Locale::ZonesModel* zonesModel() const { return m_zonesModel.get(); } + // This model can be filtered by region + CalamaresUtils::Locale::RegionalZonesModel* regionalZonesModel() const { return m_regionalZonesModel.get(); } + + const CalamaresUtils::Locale::TimeZoneData* currentLocation() const { return m_currentLocation; } + /// Special case, set location from starting timezone if not already set void setCurrentLocation(); +private: + CalamaresUtils::Locale::TimeZoneData* currentLocation_c() const + { + return const_cast< CalamaresUtils::Locale::TimeZoneData* >( m_currentLocation ); + } + public Q_SLOTS: /// Set a language by user-choice, overriding future location changes void setLanguageExplicitly( const QString& language ); @@ -111,38 +124,38 @@ public Q_SLOTS: * names a zone within that region. */ void setCurrentLocation( const QString& region, const QString& zone ); - /** @brief Sets a location by pointer + + /** @brief Sets a location by pointer to zone data. * - * 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 ); + void setCurrentLocation( const CalamaresUtils::Locale::TimeZoneData* tz ); QString currentLanguageCode() const { return localeConfiguration().language(); } QString currentLCCode() const { return localeConfiguration().lc_numeric; } + QString currentTimezoneName() const; // human-readable + QString currentTimezoneCode() const; signals: - void currentLocationChanged( const CalamaresUtils::Locale::TZZone* location ) const; + void currentLocationChanged( const CalamaresUtils::Locale::TimeZoneData* location ) const; 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; + void currentTimezoneCodeChanged( const QString& ) const; + void currentTimezoneNameChanged( const QString& ) const; 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; + std::unique_ptr< CalamaresUtils::Locale::RegionsModel > m_regionModel; + std::unique_ptr< CalamaresUtils::Locale::ZonesModel > m_zonesModel; + std::unique_ptr< CalamaresUtils::Locale::RegionalZonesModel > m_regionalZonesModel; - /// The location, points into the timezone data - const CalamaresUtils::Locale::TZZone* m_currentLocation = nullptr; + const CalamaresUtils::Locale::TimeZoneData* m_currentLocation = nullptr; /** @brief Specific locale configurations * diff --git a/src/modules/locale/LocalePage.cpp b/src/modules/locale/LocalePage.cpp index 406f27a6e..d4ad6854e 100644 --- a/src/modules/locale/LocalePage.cpp +++ b/src/modules/locale/LocalePage.cpp @@ -1,7 +1,8 @@ /* === This file is part of Calamares - === * - * Copyright 2014-2016, Teo Mrnjavac - * Copyright 2017-2019, Adriaan de Groot + * SPDX-FileCopyrightText: 2014-2016 Teo Mrnjavac + * SPDX-FileCopyrightText: 2017-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 @@ -42,7 +43,7 @@ LocalePage::LocalePage( Config* config, QWidget* parent ) QBoxLayout* mainLayout = new QVBoxLayout; QBoxLayout* tzwLayout = new QHBoxLayout; - m_tzWidget = new TimeZoneWidget( config->timezoneData(), this ); + m_tzWidget = new TimeZoneWidget( m_config->zonesModel(), this ); tzwLayout->addStretch(); tzwLayout->addWidget( m_tzWidget ); tzwLayout->addStretch(); @@ -101,6 +102,7 @@ LocalePage::LocalePage( Config* config, QWidget* parent ) // Set up the location before connecting signals, to avoid a signal // storm as various parts interact. m_regionCombo->setModel( m_config->regionModel() ); + m_zoneCombo->setModel( m_config->regionalZonesModel() ); locationChanged( m_config->currentLocation() ); // doesn't inform TZ widget m_tzWidget->setCurrentLocation( m_config->currentLocation() ); @@ -111,7 +113,7 @@ LocalePage::LocalePage( Config* config, QWidget* parent ) connect( m_tzWidget, &TimeZoneWidget::locationChanged, config, - QOverload< const CalamaresUtils::Locale::TZZone* >::of( &Config::setCurrentLocation ) ); + QOverload< const CalamaresUtils::Locale::TimeZoneData* >::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 ); @@ -151,35 +153,26 @@ LocalePage::regionChanged( int currentIndex ) { using namespace CalamaresUtils::Locale; - Q_UNUSED( currentIndex ) - QString selectedRegion = m_regionCombo->currentData().toString(); - - TZRegion* region = m_config->timezoneData().find< TZRegion >( selectedRegion ); - if ( !region ) + QString selectedRegion = m_regionCombo->itemData( currentIndex ).toString(); { - return; + cSignalBlocker z( m_zoneCombo ); + m_config->regionalZonesModel()->setRegion( selectedRegion ); } - - { - cSignalBlocker b( m_zoneCombo ); - m_zoneCombo->setModel( new CStringListModel( region->zones() ) ); - } - - m_zoneCombo->currentIndexChanged( m_zoneCombo->currentIndex() ); + m_zoneCombo->currentIndexChanged( 0 ); } void 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->itemData( currentIndex ).toString() ); } } void -LocalePage::locationChanged( const CalamaresUtils::Locale::TZZone* location ) +LocalePage::locationChanged( const CalamaresUtils::Locale::TimeZoneData* location ) { if ( !location ) { diff --git a/src/modules/locale/LocalePage.h b/src/modules/locale/LocalePage.h index bf41f8f69..4f2d321b5 100644 --- a/src/modules/locale/LocalePage.h +++ b/src/modules/locale/LocalePage.h @@ -53,7 +53,7 @@ private: void regionChanged( int currentIndex ); void zoneChanged( int currentIndex ); - void locationChanged( const CalamaresUtils::Locale::TZZone* location ); + void locationChanged( const CalamaresUtils::Locale::TimeZoneData* location ); void changeLocale(); void changeFormats(); diff --git a/src/modules/locale/LocaleViewStep.cpp b/src/modules/locale/LocaleViewStep.cpp index a85c87e4f..9ffb96c25 100644 --- a/src/modules/locale/LocaleViewStep.cpp +++ b/src/modules/locale/LocaleViewStep.cpp @@ -138,6 +138,7 @@ LocaleViewStep::jobs() const void LocaleViewStep::onActivate() { + m_config->setCurrentLocation(); // Finalize the location if ( !m_actualWidget ) { setUpPage(); @@ -149,6 +150,7 @@ LocaleViewStep::onActivate() void LocaleViewStep::onLeave() { + m_config->finalizeGlobalStorage(); } diff --git a/src/modules/locale/Tests.cpp b/src/modules/locale/Tests.cpp index af37a664b..52d4882a2 100644 --- a/src/modules/locale/Tests.cpp +++ b/src/modules/locale/Tests.cpp @@ -16,17 +16,40 @@ * along with Calamares. If not, see . */ - -#include "Tests.h" +#include "Config.h" #include "LocaleConfiguration.h" #include "timezonewidget/TimeZoneImage.h" #include "locale/TimeZone.h" +#include "utils/Logger.h" #include #include +class LocaleTests : public QObject +{ + Q_OBJECT +public: + LocaleTests(); + ~LocaleTests() override; + +private Q_SLOTS: + void initTestCase(); + // Check the sample config file is processed correctly + void testEmptyLocaleConfiguration(); + void testDefaultLocaleConfiguration(); + void testSplitLocaleConfiguration(); + + // Check the TZ images for consistency + void testTZImages(); // No overlaps in images + void testTZLocations(); // No overlaps in locations + void testSpecificLocations(); + + // Check the Config loading + void testConfigInitialization(); +}; + QTEST_MAIN( LocaleTests ) @@ -115,37 +138,35 @@ LocaleTests::testTZImages() // // using namespace CalamaresUtils::Locale; - const CStringPairList& regions = TZRegion::fromZoneTab(); + const ZonesModel m; int overlapcount = 0; - for ( const auto* pr : regions ) + for ( auto it = m.begin(); it; ++it ) { - const TZRegion* region = dynamic_cast< const TZRegion* >( pr ); - QVERIFY( region ); + QString region = m.data( m.index( it.index() ), ZonesModel::RegionRole ).toString(); + QString zoneName = m.data( m.index( it.index() ), ZonesModel::KeyRole ).toString(); + QVERIFY( !region.isEmpty() ); + QVERIFY( !zoneName.isEmpty() ); + const auto* zone = m.find( region, zoneName ); + const auto* iterzone = *it; - Logger::setupLogLevel( Logger::LOGDEBUG ); - cDebug() << "Region" << region->region() << "zones #" << region->zones().count(); - Logger::setupLogLevel( Logger::LOGERROR ); + QVERIFY( iterzone ); + QVERIFY( zone ); + QCOMPARE( zone, iterzone ); + QCOMPARE( zone->zone(), zoneName ); + QCOMPARE( zone->region(), region ); - const auto zones = region->zones(); - QVERIFY( zones.count() > 0 ); - for ( const auto* pz : zones ) + int overlap = 0; + auto pos = images.getLocationPosition( zone->longitude(), zone->latitude() ); + QVERIFY( images.index( pos, overlap ) >= 0 ); + QVERIFY( overlap > 0 ); // At least one image contains the spot + if ( overlap > 1 ) { - const TZZone* zone = dynamic_cast< const TZZone* >( pz ); - QVERIFY( zone ); - - int overlap = 0; - auto pos = images.getLocationPosition( zone->longitude(), zone->latitude() ); - QVERIFY( images.index( pos, overlap ) >= 0 ); - QVERIFY( overlap > 0 ); // At least one image contains the spot - if ( overlap > 1 ) - { - Logger::setupLogLevel( Logger::LOGDEBUG ); - cDebug() << Logger::SubEntry << "Zone" << zone->zone() << pos; - (void)images.index( pos, overlap ); - Logger::setupLogLevel( Logger::LOGERROR ); - overlapcount++; - } + Logger::setupLogLevel( Logger::LOGDEBUG ); + cDebug() << Logger::SubEntry << "Zone" << zone->zone() << pos; + (void)images.index( pos, overlap ); + Logger::setupLogLevel( Logger::LOGERROR ); + overlapcount++; } } @@ -168,12 +189,17 @@ operator<( const QPoint& l, const QPoint& r ) } void -listAll( const QPoint& p, const CalamaresUtils::Locale::CStringPairList& zones ) +listAll( const QPoint& p, const CalamaresUtils::Locale::ZonesModel& zones ) { using namespace CalamaresUtils::Locale; - for ( const auto* pz : zones ) + for ( auto it = zones.begin(); it; ++it ) { - const TZZone* zone = dynamic_cast< const TZZone* >( pz ); + const auto* zone = *it; + if ( !zone ) + { + cError() << Logger::SubEntry << "NULL zone"; + return; + } if ( p == TimeZoneImageList::getLocationPosition( zone->longitude(), zone->latitude() ) ) { cError() << Logger::SubEntry << zone->zone(); @@ -185,78 +211,37 @@ void LocaleTests::testTZLocations() { using namespace CalamaresUtils::Locale; - const CStringPairList& regions = TZRegion::fromZoneTab(); + ZonesModel zones; + + QVERIFY( zones.rowCount( QModelIndex() ) > 100 ); int overlapcount = 0; - for ( const auto* pr : regions ) + std::set< QPoint > occupied; + for ( auto it = zones.begin(); it; ++it ) { - const TZRegion* region = dynamic_cast< const TZRegion* >( pr ); - QVERIFY( region ); + const auto* zone = *it; + QVERIFY( zone ); - Logger::setupLogLevel( Logger::LOGDEBUG ); - cDebug() << "Region" << region->region() << "zones #" << region->zones().count(); - Logger::setupLogLevel( Logger::LOGERROR ); - - std::set< QPoint > occupied; - - const auto zones = region->zones(); - QVERIFY( zones.count() > 0 ); - for ( const auto* pz : zones ) + auto pos = TimeZoneImageList::getLocationPosition( zone->longitude(), zone->latitude() ); + if ( occupied.find( pos ) != occupied.end() ) { - const TZZone* zone = dynamic_cast< const TZZone* >( pz ); - QVERIFY( zone ); - - auto pos = TimeZoneImageList::getLocationPosition( zone->longitude(), zone->latitude() ); - if ( occupied.find( pos ) != occupied.end() ) - { - cError() << "Zone" << zone->zone() << "occupies same spot as .."; - listAll( pos, zones ); - overlapcount++; - } - occupied.insert( pos ); + cError() << "Zone" << zone->zone() << "occupies same spot as .."; + listAll( pos, zones ); + overlapcount++; } + occupied.insert( pos ); } QEXPECT_FAIL( "", "TZ Images contain pixel-overlaps", Continue ); QCOMPARE( overlapcount, 0 ); } -const CalamaresUtils::Locale::TZZone* -findZone( const QString& name ) -{ - using namespace CalamaresUtils::Locale; - const CStringPairList& regions = TZRegion::fromZoneTab(); - - for ( const auto* pr : regions ) - { - const TZRegion* region = dynamic_cast< const TZRegion* >( pr ); - if ( !region ) - { - continue; - } - const auto zones = region->zones(); - for ( const auto* pz : zones ) - { - const TZZone* zone = dynamic_cast< const TZZone* >( pz ); - if ( !zone ) - { - continue; - } - - if ( zone->zone() == name ) - { - return zone; - } - } - } - return nullptr; -} - void LocaleTests::testSpecificLocations() { - const auto* gibraltar = findZone( "Gibraltar" ); - const auto* ceuta = findZone( "Ceuta" ); + CalamaresUtils::Locale::ZonesModel zones; + const auto* gibraltar = zones.find( "Europe", "Gibraltar" ); + const auto* ceuta = zones.find( "Africa", "Ceuta" ); QVERIFY( gibraltar ); QVERIFY( ceuta ); @@ -268,3 +253,17 @@ LocaleTests::testSpecificLocations() QEXPECT_FAIL( "", "Gibraltar and Ceuta are really close", Continue ); QVERIFY( gpos.y() < cpos.y() ); // Gibraltar is north of Ceuta } + +void +LocaleTests::testConfigInitialization() +{ + Config c; + + QVERIFY( !c.currentLocation() ); + QVERIFY( !c.currentLocationStatus().isEmpty() ); +} + + +#include "utils/moc-warnings.h" + +#include "Tests.moc" diff --git a/src/modules/locale/Tests.h b/src/modules/locale/Tests.h deleted file mode 100644 index e01b1a25c..000000000 --- a/src/modules/locale/Tests.h +++ /dev/null @@ -1,45 +0,0 @@ -/* === This file is part of Calamares - === - * - * Copyright 2019-2020, 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 TESTS_H -#define TESTS_H - -#include - -class LocaleTests : public QObject -{ - Q_OBJECT -public: - LocaleTests(); - ~LocaleTests() override; - -private Q_SLOTS: - void initTestCase(); - // Check the sample config file is processed correctly - void testEmptyLocaleConfiguration(); - void testDefaultLocaleConfiguration(); - void testSplitLocaleConfiguration(); - - // Check the TZ images for consistency - void testTZImages(); // No overlaps in images - void testTZLocations(); // No overlaps in locations - void testSpecificLocations(); -}; - -#endif diff --git a/src/modules/locale/timezonewidget/timezonewidget.cpp b/src/modules/locale/timezonewidget/timezonewidget.cpp index 0972e3296..b1d3cfeaa 100644 --- a/src/modules/locale/timezonewidget/timezonewidget.cpp +++ b/src/modules/locale/timezonewidget/timezonewidget.cpp @@ -35,13 +35,13 @@ #endif static QPoint -getLocationPosition( const CalamaresUtils::Locale::TZZone* l ) +getLocationPosition( const CalamaresUtils::Locale::TimeZoneData* l ) { return TimeZoneImageList::getLocationPosition( l->longitude(), l->latitude() ); } -TimeZoneWidget::TimeZoneWidget( const CalamaresUtils::Locale::CStringPairList& zones, QWidget* parent ) +TimeZoneWidget::TimeZoneWidget( const CalamaresUtils::Locale::ZonesModel* zones, QWidget* parent ) : QWidget( parent ) , timeZoneImages( TimeZoneImageList::fromQRC() ) , m_zonesData( zones ) @@ -65,7 +65,7 @@ TimeZoneWidget::TimeZoneWidget( const CalamaresUtils::Locale::CStringPairList& z void -TimeZoneWidget::setCurrentLocation( const CalamaresUtils::Locale::TZZone* location ) +TimeZoneWidget::setCurrentLocation( const TimeZoneData* location ) { if ( location == m_currentLocation ) { @@ -190,32 +190,24 @@ TimeZoneWidget::mousePressEvent( QMouseEvent* event ) { return; } - // Set nearest location int nX = 999999, mX = event->pos().x(); int nY = 999999, mY = event->pos().y(); using namespace CalamaresUtils::Locale; - const TZZone* closest = nullptr; - for ( const auto* region_p : m_zonesData ) + const TimeZoneData* closest = nullptr; + for ( auto it = m_zonesData->begin(); it; ++it ) { - const auto* region = dynamic_cast< const TZRegion* >( region_p ); - if ( region ) + const auto* zone = *it; + if ( zone ) { - for ( const auto* zone_p : region->zones() ) - { - const auto* zone = dynamic_cast< const TZZone* >( zone_p ); - if ( zone ) - { - QPoint locPos = TimeZoneImageList::getLocationPosition( zone->longitude(), zone->latitude() ); + QPoint locPos = TimeZoneImageList::getLocationPosition( zone->longitude(), zone->latitude() ); - if ( ( abs( mX - locPos.x() ) + abs( mY - locPos.y() ) < abs( mX - nX ) + abs( mY - nY ) ) ) - { - closest = zone; - nX = locPos.x(); - nY = locPos.y(); - } - } + if ( ( abs( mX - locPos.x() ) + abs( mY - locPos.y() ) < abs( mX - nX ) + abs( mY - nY ) ) ) + { + closest = zone; + nX = locPos.x(); + nY = locPos.y(); } } } diff --git a/src/modules/locale/timezonewidget/timezonewidget.h b/src/modules/locale/timezonewidget/timezonewidget.h index 6bb94c0dd..c15570b52 100644 --- a/src/modules/locale/timezonewidget/timezonewidget.h +++ b/src/modules/locale/timezonewidget/timezonewidget.h @@ -51,28 +51,28 @@ class TimeZoneWidget : public QWidget { Q_OBJECT public: - using TZZone = CalamaresUtils::Locale::TZZone; + using TimeZoneData = CalamaresUtils::Locale::TimeZoneData; - explicit TimeZoneWidget( const CalamaresUtils::Locale::CStringPairList& zones, QWidget* parent = nullptr ); + explicit TimeZoneWidget( const CalamaresUtils::Locale::ZonesModel* zones, QWidget* parent = nullptr ); 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 ); + void setCurrentLocation( const TimeZoneData* location ); signals: /** @brief The location has changed by mouse click */ - void locationChanged( const TZZone* location ); + void locationChanged( const TimeZoneData* location ); private: QFont font; QImage background, pin, currentZoneImage; TimeZoneImageList timeZoneImages; - const CalamaresUtils::Locale::CStringPairList& m_zonesData; - const TZZone* m_currentLocation = nullptr; // Not owned by me + const CalamaresUtils::Locale::ZonesModel* m_zonesData; + const TimeZoneData* m_currentLocation = nullptr; // Not owned by me void paintEvent( QPaintEvent* event ); void mousePressEvent( QMouseEvent* event ); diff --git a/src/modules/localeq/LocaleQmlViewStep.cpp b/src/modules/localeq/LocaleQmlViewStep.cpp index ead2e2673..d60664a8f 100644 --- a/src/modules/localeq/LocaleQmlViewStep.cpp +++ b/src/modules/localeq/LocaleQmlViewStep.cpp @@ -79,9 +79,22 @@ LocaleQmlViewStep::jobs() const return m_config->createJobs(); } +void +LocaleQmlViewStep::onActivate() +{ + m_config->setCurrentLocation(); // Finalize the location + QmlViewStep::onActivate(); +} + +void +LocaleQmlViewStep::onLeave() +{ + m_config->finalizeGlobalStorage(); +} + void LocaleQmlViewStep::setConfigurationMap( const QVariantMap& configurationMap ) { m_config->setConfigurationMap( configurationMap ); - Calamares::QmlViewStep::setConfigurationMap( configurationMap ); // call parent implementation last + QmlViewStep::setConfigurationMap( configurationMap ); // call parent implementation last } diff --git a/src/modules/localeq/LocaleQmlViewStep.h b/src/modules/localeq/LocaleQmlViewStep.h index 3d73c6f79..bbafd5abb 100644 --- a/src/modules/localeq/LocaleQmlViewStep.h +++ b/src/modules/localeq/LocaleQmlViewStep.h @@ -43,6 +43,9 @@ public: bool isAtBeginning() const override; bool isAtEnd() const override; + virtual void onActivate() override; + virtual void onLeave() override; + Calamares::JobList jobs() const override; void setConfigurationMap( const QVariantMap& configurationMap ) override; diff --git a/src/modules/localeq/Map.qml b/src/modules/localeq/Map.qml index 080d7388a..d414825dd 100644 --- a/src/modules/localeq/Map.qml +++ b/src/modules/localeq/Map.qml @@ -29,19 +29,19 @@ import QtPositioning 5.14 Column { width: parent.width - //Needs to come from .conf/geoip - property var configCity: "New York" - property var configCountry: "USA" - property var configTimezone: "America/New York" - property var geoipCity: "" //"Amsterdam" - property var geoipCountry: "" //"Netherlands" - property var geoipTimezone: "" //"Europe/Amsterdam" - // vars that will stay once connected - property var cityName: (geoipCity != "") ? geoipCity : configCity - property var countryName: (geoipCountry != "") ? geoipCountry : configCountry - property var timeZone: (geoipTimezone != "") ? geoipTimezone : configTimezone + // These are used by the map query to initially center the + // map on the user's likely location. They are updated by + // getIp() which does a more accurate GeoIP lookup than + // the default one in Calamares + property var cityName: "" + property var countryName: "" - function getIp() { + /* This is an extra GeoIP lookup, which will find better-accuracy + * location data for the user's IP, and then sets the current timezone + * and map location. Call it from Component.onCompleted so that + * it happens "on time" before the page is shown. + */ + function getIpOnline() { var xhr = new XMLHttpRequest xhr.onreadystatechange = function() { @@ -51,9 +51,10 @@ Column { var ct = responseJSON.city var cy = responseJSON.country - tzText.text = "Timezone: " + tz cityName = ct countryName = cy + + config.setCurrentLocation(tz) } } @@ -63,7 +64,25 @@ Column { xhr.send() } - function getTz() { + /* This is an "offline" GeoIP lookup -- it just follows what + * Calamares itself has figured out with its GeoIP or configuration. + * Call it from the **Component** onActivate() -- in localeq.qml -- + * so it happens as the page is shown. + */ + function getIpOffline() { + cityName = config.currentLocation.zone + countryName = config.currentLocation.countryCode + } + + /* This is an **accurate** TZ lookup method: it queries an + * online service for the TZ at the given coordinates. It + * requires an internet connection, though, and the distribution + * will need to have an account with geonames to not hit the + * daily query limit. + * + * See below, in MouseArea, for calling the right method. + */ + function getTzOnline() { var xhr = new XMLHttpRequest var latC = map.center.latitude var lonC = map.center.longitude @@ -73,16 +92,29 @@ Column { var responseJSON = JSON.parse(xhr.responseText) var tz2 = responseJSON.timezoneId - tzText.text = "Timezone: " + tz2 config.setCurrentLocation(tz2) } } + console.log("Online lookup", latC, lonC) // Needs to move to localeq.conf, each distribution will need their own account xhr.open("GET", "http://api.geonames.org/timezoneJSON?lat=" + latC + "&lng=" + lonC + "&username=SOME_USERNAME") xhr.send() } + /* This is a quick TZ lookup method: it uses the existing + * Calamares "closest TZ" code, which has lots of caveats. + * + * See below, in MouseArea, for calling the right method. + */ + function getTzOffline() { + var latC = map.center.latitude + var lonC = map.center.longitude + var tz = config.zonesModel.lookup(latC, lonC) + console.log("Offline lookup", latC, lonC) + config.setCurrentLocation(tz.region, tz.zone) + } + Rectangle { width: parent.width height: parent.height / 1.28 @@ -156,9 +188,8 @@ Column { map.center.latitude = coordinate.latitude map.center.longitude = coordinate.longitude - getTz(); - - console.log(coordinate.latitude, coordinate.longitude) + // Pick a TZ lookup method here (quick:offline, accurate:online) + getTzOffline(); } } } @@ -218,13 +249,16 @@ Column { Text { id: tzText - text: tzText.text - //text: qsTr("Timezone: %1").arg(timeZone) + text: qsTr("Timezone: %1").arg(config.currentTimezoneName) color: Kirigami.Theme.textColor anchors.centerIn: parent } - Component.onCompleted: getIp(); + /* If you want an extra (and accurate) GeoIP lookup, + * enable this one and disable the offline lookup in + * onActivate(). + Component.onCompleted: getIpOnline(); + */ } } diff --git a/src/modules/localeq/localeq.qml b/src/modules/localeq/localeq.qml index 49719db65..1a250f5c1 100644 --- a/src/modules/localeq/localeq.qml +++ b/src/modules/localeq/localeq.qml @@ -29,6 +29,14 @@ Page { width: 800 height: 550 + function onActivate() { + /* If you want the map to follow Calamares's GeoIP + * lookup or configuration, call the update function + * here, and disable the one at onCompleted in Map.qml. + */ + if (Network.hasInternet) { image.item.getIpOffline() } + } + Loader { id: image anchors.horizontalCenter: parent.horizontalCenter