on xenial help sign in
make the timezone model filterable by both predefined country and user filter string
63783c6Lukáš Tinkl
debian/control+2−1
@@ -8,7 +8,7 @@ Build-Depends: cmake,
8 doxyqml,
8 doxyqml,
9# To allow cross-compiling to work, we also must append :native
9# To allow cross-compiling to work, we also must append :native
10# to g++ so we don't try to run armhf g++
10# to g++ so we don't try to run armhf g++
11−# on an x86 CPU for eaxmple, when cross-compiling.
11+# on an x86 CPU for example, when cross-compiling.
12 g++:native,
12 g++:native,
13 libandroid-properties-dev,
13 libandroid-properties-dev,
14 graphviz,
14 graphviz,
@@ -28,6 +28,7 @@ Build-Depends: cmake,
28 libqt5svg5-dev,
28 libqt5svg5-dev,
29 libqt5xmlpatterns5-dev,
29 libqt5xmlpatterns5-dev,
30 libsystemsettings-dev,
30 libsystemsettings-dev,
31+ libtimezonemap1-dev,
31 libudev-dev,
32 libudev-dev,
32 libunity-api-dev (>= 7.101),
33 libunity-api-dev (>= 7.101),
33 libusermetricsoutput1-dev,
34 libusermetricsoutput1-dev,
plugins/Wizard/CMakeLists.txt+6−1
@@ -1,3 +1,7 @@
1+pkg_search_module(TIMEZONEMAP REQUIRED timezonemap)
2+
3+include_directories(${GLIB_INCLUDE_DIRS} ${TIMEZONEMAP_INCLUDE_DIRS})
4+
1add_library(Wizard-qml MODULE
5add_library(Wizard-qml MODULE
2 plugin.cpp
6 plugin.cpp
3 PageList.cpp
7 PageList.cpp
@@ -7,7 +11,8 @@ add_library(Wizard-qml MODULE
7 Status.cpp
11 Status.cpp
8)
12)
9
13
10−qt5_use_modules(Wizard-qml DBus Qml)
14+qt5_use_modules(Wizard-qml DBus Qml Concurrent)
15+target_link_libraries(Wizard-qml ${GLIB_LDFLAGS} ${TIMEZONEMAP_LDFLAGS})
11add_unity8_plugin(Wizard 0.1 Wizard TARGETS Wizard-qml)
16add_unity8_plugin(Wizard 0.1 Wizard TARGETS Wizard-qml)
12
17
13set(POLKIT_LIB_DIR "${CMAKE_INSTALL_LOCALSTATEDIR}/lib/polkit-1")
18set(POLKIT_LIB_DIR "${CMAKE_INSTALL_LOCALSTATEDIR}/lib/polkit-1")
plugins/Wizard/plugin.cpp+1−1
@@ -29,7 +29,7 @@ void WizardPlugin::registerTypes(const char *uri)
29 qmlRegisterType<PageList>(uri, 0, 1, "PageList");
29 qmlRegisterType<PageList>(uri, 0, 1, "PageList");
30 qmlRegisterSingletonType<System>(uri, 0, 1, "System", [](QQmlEngine*, QJSEngine*) -> QObject* { return new System; });
30 qmlRegisterSingletonType<System>(uri, 0, 1, "System", [](QQmlEngine*, QJSEngine*) -> QObject* { return new System; });
31 qmlRegisterSingletonType<Status>(uri, 0, 1, "Status", [](QQmlEngine*, QJSEngine*) -> QObject* { return new Status; });
31 qmlRegisterSingletonType<Status>(uri, 0, 1, "Status", [](QQmlEngine*, QJSEngine*) -> QObject* { return new Status; });
32− qmlRegisterType<TimeZoneModel>(uri, 0, 1, "TimeZoneModel");
32+ qmlRegisterType<TimeZoneLocationModel>(uri, 0, 1, "TimeZoneModel");
33 qmlRegisterType<TimeZoneFilterModel>(uri, 0, 1, "TimeZoneFilterModel");
33 qmlRegisterType<TimeZoneFilterModel>(uri, 0, 1, "TimeZoneFilterModel");
34 qmlRegisterType<LocalePlugin>(uri, 0, 1, "LocalePlugin");
34 qmlRegisterType<LocalePlugin>(uri, 0, 1, "LocalePlugin");
35}
35}
plugins/Wizard/timezonemodel.cpp+110−63
@@ -14,94 +14,141 @@
14 * with this program. If not, see <http://www.gnu.org/licenses/>.
14 * with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
15 */
16
16
17−#include <QTimeZone>
18#include <QDebug>
17#include <QDebug>
19
18
19+#include <glib.h>
20+#include <glib-object.h>
21+#include <timezonemap/tz.h>
22+
20#include "LocalePlugin.h"
23#include "LocalePlugin.h"
21#include "timezonemodel.h"
24#include "timezonemodel.h"
22
25
23
26
24−TimeZoneModel::TimeZoneModel(QObject *parent)
27+TimeZoneLocationModel::TimeZoneLocationModel(QObject *parent):
25− : QAbstractListModel(parent)
28+ QAbstractListModel(parent),
29+ m_workerThread(new TimeZonePopulateWorker())
26{
30{
31+ qRegisterMetaType<TzLocation>();
32+
27− m_roleNames = {{IdRole, "id"},
33+ m_roleNames[Qt::DisplayRole] = "displayName";
28− {Abbreviation, "abbreviation"},
34+ m_roleNames[TimeZoneRole] = "timeZone";
29− {Country, "country"},
35+ m_roleNames[CityRole] = "city";
30− {CountryCode, "countryCode"},
36+ m_roleNames[CountryRole] = "country";
31− {City, "city"},
37+
32− {Comment, "comment"},
38+ QObject::connect(m_workerThread,
33− {Time, "time"}};
39+ &TimeZonePopulateWorker::resultReady,
40+ this,
41+ &TimeZoneLocationModel::processModelResult);
42+ QObject::connect(m_workerThread,
43+ &TimeZonePopulateWorker::finished,
44+ this,
45+ &TimeZoneLocationModel::store);
46+ QObject::connect(m_workerThread,
47+ &TimeZonePopulateWorker::finished,
48+ m_workerThread,
49+ &QObject::deleteLater);
50+
34 init();
51 init();
35}
52}
36
53
37−QString TimeZoneModel::selectedZoneId() const
54+void TimeZoneLocationModel::init()
38{
55{
39− return m_selectedZoneId;
56+ beginResetModel();
57+ m_workerThread->start();
40}
58}
41
59
42−void TimeZoneModel::setSelectedZoneId(const QString &selectedZoneId)
60+void TimeZoneLocationModel::store()
43{
61{
44− if (m_selectedZoneId == selectedZoneId)
62+ m_workerThread = nullptr;
45− return;
63+ endResetModel();
46−
47− m_selectedZoneId = selectedZoneId;
48− Q_EMIT selectedZoneIdChanged(selectedZoneId);
49}
64}
50
65
51−int TimeZoneModel::rowCount(const QModelIndex &parent) const
66+void TimeZoneLocationModel::processModelResult(const TzLocation &location)
52{
67{
53− Q_UNUSED(parent)
68+ m_locations.append(location);
54− return m_zoneIds.count();
55}
69}
56
70
57−QVariant TimeZoneModel::data(const QModelIndex &index, int role) const
71+int TimeZoneLocationModel::rowCount(const QModelIndex &parent) const
58{
72{
59− if (index.isValid()) {
73+ if (parent.isValid())
60− const QByteArray tzid = m_zoneIds.at(index.row());
74+ return 0;
61− QTimeZone tz(tzid);
75+ return m_locations.count();
62−
63− if (!tz.isValid()) {
64− qWarning() << Q_FUNC_INFO << "Invalid timezone" << tzid;
65− return QVariant();
66− }
76+}
67
77
68− switch (role) {
78+QVariant TimeZoneLocationModel::data(const QModelIndex &index, int role) const
69− case IdRole:
79+{
70− return QString(tz.id()); // to let QML compare it effortlessly with a QString
80+ if (index.row() >= m_locations.count() || index.row() < 0)
71− case Abbreviation:
81+ return QVariant();
72− return tz.abbreviation(QDateTime::currentDateTime());
82+
73− case Country:
83+ const TzLocation tz = m_locations.at(index.row());
74− return LocaleAttached::countryToString(tz.country());
84+
75− case CountryCode: {
85+ const QString country(tz.full_country.isEmpty() ? tz.country : tz.full_country);
76− return LocaleAttached::qlocToCountryCode(tz.country());
86+
77− }
87+ switch (role) {
78− case City: {
88+ case Qt::DisplayRole:
79− const QString cityName = QString::fromUtf8(tzid.split('/').last().replace('_', ' ')); // take the last part, replace _ by a space
89+ if (!tz.state.isEmpty())
80− return cityName;
90+ return QStringLiteral("%1, %2, %3").arg(tz.city).arg(tz.state).arg(country);
81− }
91+ else
82− case Comment:
92+ return QStringLiteral("%1, %2").arg(tz.city).arg(country);
83− return tz.comment();
93+ case SimpleRole:
84− case Time:
94+ return QStringLiteral("%1, %2").arg(tz.city).arg(country);
85− return QDateTime::currentDateTime().toTimeZone(tz).toString("h:mm");
95+ case TimeZoneRole:
96+ return tz.timezone;
97+ case CountryRole:
98+ return tz.country;
99+ case CityRole:
100+ return tz.city;
86− default:
101+ default:
87− qWarning() << Q_FUNC_INFO << "Unsupported data role" << role;
102+ qWarning() << Q_FUNC_INFO << "Unknown role";
88− break;
103+ return QVariant();
89− }
90 }
104 }
91−
92− return QVariant();
93}
105}
94
106
95−QHash<int, QByteArray> TimeZoneModel::roleNames() const
107+QHash<int, QByteArray> TimeZoneLocationModel::roleNames() const
96{
108{
97 return m_roleNames;
109 return m_roleNames;
98}
110}
99
111
100−void TimeZoneModel::init()
112+void TimeZonePopulateWorker::run()
101{
113{
102− beginResetModel();
114+ buildCityMap();
103− m_zoneIds = QTimeZone::availableTimeZoneIds();
115+}
104− endResetModel();
116+
117+void TimeZonePopulateWorker::buildCityMap()
118+{
119+ TzDB *tzdb = tz_load_db();
120+ GPtrArray *tz_locations = tz_get_locations(tzdb);
121+
122+ TimeZoneLocationModel::TzLocation tmpTz;
123+
124+ for (guint i = 0; i < tz_locations->len; ++i) {
125+ auto tmp = static_cast<CcTimezoneLocation *>(g_ptr_array_index(tz_locations, i));
126+ gchar *en_name, *country, *zone, *state, *full_country;
127+ g_object_get (tmp, "en_name", &en_name,
128+ "country", &country,
129+ "zone", &zone,
130+ "state", &state,
131+ "full_country", &full_country,
132+ nullptr);
133+ // There are empty entries in the DB
134+ if (g_strcmp0(en_name, "") != 0) {
135+ tmpTz.city = en_name;
136+ tmpTz.country = country;
137+ tmpTz.timezone = zone;
138+ tmpTz.state = state;
139+ tmpTz.full_country = full_country;
140+ }
141+ g_free (en_name);
142+ g_free (country);
143+ g_free (zone);
144+ g_free (state);
145+ g_free (full_country);
146+
147+ Q_EMIT (resultReady(tmpTz));
148+ }
149+
150+ g_ptr_array_free (tz_locations, TRUE);
151+ tz_db_free(tzdb);
105}
152}
106
153
107
154
@@ -110,7 +157,7 @@ TimeZoneFilterModel::TimeZoneFilterModel(QObject *parent)
110{
157{
111 setDynamicSortFilter(false);
158 setDynamicSortFilter(false);
112 setSortLocaleAware(true);
159 setSortLocaleAware(true);
113− setSortRole(TimeZoneModel::City);
160+ setSortRole(TimeZoneLocationModel::CityRole);
114 m_stringMatcher.setCaseSensitivity(Qt::CaseInsensitive);
161 m_stringMatcher.setCaseSensitivity(Qt::CaseInsensitive);
115 sort(0);
162 sort(0);
116}
163}
@@ -122,14 +169,14 @@ bool TimeZoneFilterModel::filterAcceptsRow(int row, const QModelIndex &parentInd
122 }
169 }
123
170
124 if (!m_filter.isEmpty()) { // filtering by freeform text input, cf setFilter(QString)
171 if (!m_filter.isEmpty()) { // filtering by freeform text input, cf setFilter(QString)
125− const QString city = sourceModel()->index(row, 0, parentIndex).data(TimeZoneModel::City).toString();
172+ const QString city = sourceModel()->index(row, 0, parentIndex).data(TimeZoneLocationModel::CityRole).toString();
126− //const QString country = sourceModel()->index(row, 0, parentIndex).data(TimeZoneModel::Country).toString();
173+ //const QString country = sourceModel()->index(row, 0, parentIndex).data(TimeZoneLocationModel::CountryRole).toString();
127
174
128 if (m_stringMatcher.indexIn(city) == 0 /*|| m_stringMatcher.indexIn(country) == 0*/) { // match at the beginning of the city name
175 if (m_stringMatcher.indexIn(city) == 0 /*|| m_stringMatcher.indexIn(country) == 0*/) { // match at the beginning of the city name
129 return true;
176 return true;
130 }
177 }
131 } else if (!m_country.isEmpty()) { // filter by country code
178 } else if (!m_country.isEmpty()) { // filter by country code
132− const QString countryCode = sourceModel()->index(row, 0, parentIndex).data(TimeZoneModel::CountryCode).toString();
179+ const QString countryCode = sourceModel()->index(row, 0, parentIndex).data(TimeZoneLocationModel::CountryRole).toString();
133 return m_country.compare(countryCode, Qt::CaseInsensitive) == 0;
180 return m_country.compare(countryCode, Qt::CaseInsensitive) == 0;
134 }
181 }
135
182
plugins/Wizard/timezonemodel.h+64−37
@@ -19,6 +19,70 @@
19
19
20#include <QAbstractListModel>
20#include <QAbstractListModel>
21#include <QSortFilterProxyModel>
21#include <QSortFilterProxyModel>
22+#include <QThread>
23+#include <QtConcurrent>
24+
25+class TimeZonePopulateWorker;
26+
27+class TimeZoneLocationModel: public QAbstractListModel
28+{
29+ Q_OBJECT
30+ Q_ENUMS(Roles)
31+public:
32+ explicit TimeZoneLocationModel(QObject *parent = 0);
33+ ~TimeZoneLocationModel() = default;
34+
35+ enum Roles {
36+ TimeZoneRole = Qt::UserRole + 1,
37+ CityRole,
38+ CountryRole,
39+ SimpleRole
40+ };
41+
42+ struct TzLocation {
43+ bool operator<(const TzLocation &other) const
44+ {
45+ QString pattern("%1, %2");
46+ return pattern.arg(city).arg(country) <
47+ pattern.arg(other.city).arg(other.country);
48+ }
49+
50+ QString city;
51+ QString country;
52+ QString timezone;
53+ QString state;
54+ QString full_country;
55+ };
56+
57+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
58+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
59+ QHash<int, QByteArray> roleNames() const override;
60+
61+private Q_SLOTS:
62+ void processModelResult(const TzLocation &location);
63+ void store();
64+
65+private:
66+ void init();
67+ QHash<int, QByteArray> m_roleNames;
68+ QList<TzLocation> m_locations;
69+ TimeZonePopulateWorker *m_workerThread;
70+};
71+
72+Q_DECLARE_METATYPE (TimeZoneLocationModel::TzLocation)
73+
74+class TimeZonePopulateWorker: public QThread
75+{
76+ Q_OBJECT
77+public:
78+ void run() override;
79+
80+Q_SIGNALS:
81+ void resultReady(const TimeZoneLocationModel::TzLocation &tz);
82+
83+private:
84+ void buildCityMap();
85+};
22
86
23class TimeZoneFilterModel: public QSortFilterProxyModel
87class TimeZoneFilterModel: public QSortFilterProxyModel
24{
88{
@@ -47,41 +111,4 @@ private:
47 QString m_country;
111 QString m_country;
48};
112};
49
113
50−class TimeZoneModel: public QAbstractListModel
51−{
52− Q_OBJECT
53− Q_PROPERTY(QString selectedZoneId READ selectedZoneId WRITE setSelectedZoneId NOTIFY selectedZoneIdChanged)
54− Q_ENUMS(Roles)
55−public:
56− enum Roles {
57− IdRole = Qt::UserRole + 1,
58− Abbreviation,
59− Country,
60− CountryCode,
61− City,
62− Comment,
63− Time
64− };
65−
66− explicit TimeZoneModel(QObject *parent = nullptr);
67− ~TimeZoneModel() = default;
68−
69− QString selectedZoneId() const;
70− void setSelectedZoneId(const QString &selectedZoneId);
71−
72−protected:
73− int rowCount(const QModelIndex &parent) const override;
74− QVariant data(const QModelIndex &index, int role) const override;
75− QHash<int, QByteArray> roleNames() const override;
76−
77−Q_SIGNALS:
78− void selectedZoneIdChanged(const QString &selectedZoneId);
79−
80−private:
81− void init();
82− QByteArrayList m_zoneIds;
83− QString m_selectedZoneId;
84− QHash<int, QByteArray> m_roleNames;
85−};
86−
87#endif
114#endif
qml/Wizard/Pages/50-timezone.qml+12−1
@@ -32,7 +32,17 @@ LocalComponents.Page {
32
32
33 UbuntuTimeDatePanel {
33 UbuntuTimeDatePanel {
34 id: timeDatePanel
34 id: timeDatePanel
35+ }
36+
37+ TimeZoneModel {
38+ id: tzModel
39+ }
40+
41+ TimeZoneFilterModel {
42+ id: tzFilterModel
43+ sourceModel: tzModel
35 filter: searchField.text
44 filter: searchField.text
45+ country: root.countryCode
36 }
46 }
37
47
38 Component.onCompleted: {
48 Component.onCompleted: {
@@ -99,6 +109,7 @@ LocalComponents.Page {
99 print("Clicked index", index)
109 print("Clicked index", index)
100 ListView.view.currentIndex = index
110 ListView.view.currentIndex = index
101 selectedTimeZone = timeZone
111 selectedTimeZone = timeZone
112+ print("Clicked city", city)
102 print("Clicked country", country)
113 print("Clicked country", country)
103 }
114 }
104 }
115 }
@@ -153,7 +164,7 @@ LocalComponents.Page {
153 }
164 }
154
165
155 height: column.height - searchField.height - customMargin - topMargin - divider.height
166 height: column.height - searchField.height - customMargin - topMargin - divider.height
156− model: timeDatePanel.timeZoneModel
167+ model: tzFilterModel
157 delegate: tzComponent
168 delegate: tzComponent
158 }
169 }
159 }
170 }
tests/mocks/Wizard/CMakeLists.txt+5−1
@@ -1,4 +1,7 @@
1+pkg_search_module(TIMEZONEMAP REQUIRED timezonemap)
2+
1include_directories(
3include_directories(
4+ ${GLIB_INCLUDE_DIRS} ${TIMEZONEMAP_INCLUDE_DIRS}
2 ${CMAKE_SOURCE_DIR}/plugins/Wizard
5 ${CMAKE_SOURCE_DIR}/plugins/Wizard
3)
6)
4
7
@@ -11,5 +14,6 @@ add_library(MockWizard-qml MODULE
11 ${CMAKE_SOURCE_DIR}/plugins/Wizard/Status.cpp
14 ${CMAKE_SOURCE_DIR}/plugins/Wizard/Status.cpp
12)
15)
13
16
14−qt5_use_modules(MockWizard-qml DBus Qml)
17+qt5_use_modules(MockWizard-qml DBus Qml Concurrent)
18+target_link_libraries(MockWizard-qml ${GLIB_LDFLAGS} ${TIMEZONEMAP_LDFLAGS})
15add_unity8_mock(Wizard 0.1 Wizard TARGETS MockWizard-qml)
19add_unity8_mock(Wizard 0.1 Wizard TARGETS MockWizard-qml)
tests/mocks/Wizard/mockplugin.cpp+1−1
@@ -29,7 +29,7 @@ void MockWizardPlugin::registerTypes(const char *uri)
29 qmlRegisterType<PageList>(uri, 0, 1, "PageList");
29 qmlRegisterType<PageList>(uri, 0, 1, "PageList");
30 qmlRegisterSingletonType<MockSystem>(uri, 0, 1, "System", [](QQmlEngine*, QJSEngine*) -> QObject* { return new MockSystem; });
30 qmlRegisterSingletonType<MockSystem>(uri, 0, 1, "System", [](QQmlEngine*, QJSEngine*) -> QObject* { return new MockSystem; });
31 qmlRegisterSingletonType<Status>(uri, 0, 1, "Status", [](QQmlEngine*, QJSEngine*) -> QObject* { return new Status; });
31 qmlRegisterSingletonType<Status>(uri, 0, 1, "Status", [](QQmlEngine*, QJSEngine*) -> QObject* { return new Status; });
32− qmlRegisterType<TimeZoneModel>(uri, 0, 1, "TimeZoneModel");
32+ qmlRegisterType<TimeZoneLocationModel>(uri, 0, 1, "TimeZoneModel");
33 qmlRegisterType<TimeZoneFilterModel>(uri, 0, 1, "TimeZoneFilterModel");
33 qmlRegisterType<TimeZoneFilterModel>(uri, 0, 1, "TimeZoneFilterModel");
34 qmlRegisterType<LocalePlugin>(uri, 0, 1, "LocalePlugin");
34 qmlRegisterType<LocalePlugin>(uri, 0, 1, "LocalePlugin");
35}
35}