Merge branch 'issue-1476' into calamares

Go over the locale module again:
- new models that avoid weird casts and inconvenient iteration
- shared timezone data
- simple sorting and filtering
- simplify the map / QML version

FIXES #1476
FIXES #1426
This commit is contained in:
Adriaan de Groot 2020-08-07 08:33:44 +02:00
commit 2ce12d5368
21 changed files with 1140 additions and 649 deletions

View File

@ -57,6 +57,7 @@ set( libSources
locale/Lookup.cpp
locale/TimeZone.cpp
locale/TranslatableConfiguration.cpp
locale/TranslatableString.cpp
# Modules
modulesystem/InstanceKey.cpp

View File

@ -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 );

View File

@ -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"

View File

@ -22,17 +22,28 @@
#include "TimeZone.h"
#include "locale/TranslatableString.h"
#include "utils/Logger.h"
#include "utils/String.h"
#include <QFile>
#include <QStringList>
#include <QTextStream>
#include <cstring>
#include <QString>
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 = [&region]( 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"

View File

@ -24,184 +24,198 @@
#include "DllMacro.h"
#include "utils/Logger.h"
#include "locale/TranslatableString.h"
#include <QAbstractListModel>
#include <QObject>
#include <QString>
#include <memory>
#include <QSortFilterProxyModel>
#include <QVariant>
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<QString, QString> 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

View File

@ -0,0 +1,89 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* 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 <http://www.gnu.org/licenses/>.
*
*/
#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

View File

@ -0,0 +1,68 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
* 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 <http://www.gnu.org/licenses/>.
*
*/
#ifndef LOCALE_TRANSLATABLESTRING_H
#define LOCALE_TRANSLATABLESTRING_H
#include <QString>
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<QString, QString> 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

View File

@ -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"

View File

@ -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()
{

View File

@ -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
*

View File

@ -1,7 +1,8 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Copyright 2014-2016, Teo Mrnjavac <teo@kde.org>
* Copyright 2017-2019, Adriaan de Groot <groot@kde.org>
* SPDX-FileCopyrightText: 2014-2016 Teo Mrnjavac <teo@kde.org>
* SPDX-FileCopyrightText: 2017-2019 Adriaan de Groot <groot@kde.org>
* 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 )
{

View File

@ -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();

View File

@ -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();
}

View File

@ -16,17 +16,40 @@
* along with Calamares. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Tests.h"
#include "Config.h"
#include "LocaleConfiguration.h"
#include "timezonewidget/TimeZoneImage.h"
#include "locale/TimeZone.h"
#include "utils/Logger.h"
#include <QtTest/QtTest>
#include <set>
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"

View File

@ -1,45 +0,0 @@
/* === This file is part of Calamares - <http://github.com/calamares> ===
*
* Copyright 2019-2020, Adriaan de Groot <groot@kde.org>
*
* Calamares is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Calamares is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Calamares. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef TESTS_H
#define TESTS_H
#include <QObject>
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

View File

@ -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();
}
}
}

View File

@ -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 );

View File

@ -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
}

View File

@ -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;

View File

@ -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();
*/
}
}

View File

@ -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