[gnome-weather/wip/ewlsh/gtk4] Lots of design alignment, cleanup, entry fixes, and model reworking for GTK4



commit bb7bb6d6e81be8e4c28f3d8bb0dfd20848fd1991
Author: Evan Welsh <contact evanwelsh com>
Date:   Sat Feb 12 21:36:35 2022 -0800

    Lots of design alignment, cleanup, entry fixes, and model reworking for GTK4

 data/city.ui                               |  10 +-
 data/org.gnome.Weather.data.gresource.xml  |   1 +
 data/places-popover.ui                     | 118 ++++++-----
 data/style-dark.css                        |   5 +-
 data/style.css                             |  39 ++--
 data/weather-widget.ui                     |   1 -
 data/window.ui                             |  19 +-
 src/app/application.js                     | 330 +++++++++++++++--------------
 src/app/city.js                            |  46 ++--
 src/app/dailyForecast.js                   |  19 +-
 src/app/entry.js                           | 104 ++++-----
 src/app/hourlyForecast.js                  |  24 ++-
 src/app/locationRow.js                     |  10 +-
 src/app/locationRow.ui                     |  11 +-
 src/app/main.js                            |   4 +-
 src/app/thermometer.js                     |  14 +-
 src/app/window.js                          |  23 +-
 src/app/world.js                           | 137 +++++-------
 src/misc/util.js                           |  29 ++-
 src/org.gnome.Weather.BackgroundService.in |   6 +-
 src/service/main.js                        |   4 +-
 src/shared/world.js                        | 118 +++++++----
 22 files changed, 562 insertions(+), 510 deletions(-)
---
diff --git a/data/city.ui b/data/city.ui
index 5bc7e55..6792586 100644
--- a/data/city.ui
+++ b/data/city.ui
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <requires lib="gtk" version="4.0"/>
+  <requires lib="gtk" version="4.0" />
   <template class="Gjs_WeatherView">
     <child>
       <object class="GtkStack" id="stack">
@@ -8,18 +8,20 @@
           <object class="GtkStackPage">
             <property name="name">loading</property>
             <property name="child">
-              <object class="GtkGrid" id="loading-grid">
+              <object class="GtkBox" id="loading-grid">
                 <property name="orientation">vertical</property>
+                <property name="spacing">20</property>
                 <property name="halign">center</property>
                 <property name="valign">center</property>
                 <child>
                   <object class="GtkSpinner" id="spinner">
-                    <property name="height_request">128</property>
-                    <property name="width_request">128</property>
+                    <property name="height_request">64</property>
+                    <property name="width_request">64</property>
                   </object>
                 </child>
                 <child>
                   <object class="GtkLabel">
+                    <property name="name">loadingLabel</property>
                     <property name="label" translatable="yes">Loading…</property>
                   </object>
                 </child>
diff --git a/data/org.gnome.Weather.data.gresource.xml b/data/org.gnome.Weather.data.gresource.xml
index 56934d0..2f525ee 100644
--- a/data/org.gnome.Weather.data.gresource.xml
+++ b/data/org.gnome.Weather.data.gresource.xml
@@ -8,6 +8,7 @@
     <file preprocess="xml-stripblanks">hour-entry.ui</file>
     <file preprocess="xml-stripblanks">day-entry.ui</file>
     <file>style.css</file>
+    <file>style-dark.css</file>
   </gresource>
   <gresource prefix="/org/gnome/shell">
     <file>ShellWeatherIntegration.xml</file>
diff --git a/data/places-popover.ui b/data/places-popover.ui
index 589969b..3d0862c 100644
--- a/data/places-popover.ui
+++ b/data/places-popover.ui
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <requires lib="gtk" version="4.0"/>
+  <requires lib="gtk" version="4.0" />
   <object class="GtkGrid" id="popover-grid">
     <property name="name">popoverGrid</property>
     <property name="hexpand">1</property>
@@ -10,6 +10,7 @@
         <property name="name">locationEntry</property>
         <property name="focusable">1</property>
         <property name="width-request">300</property>
+        <property name="placeholder-text" translatable="yes">Search for a city</property>
         <layout>
           <property name="column">0</property>
           <property name="row">0</property>
@@ -18,69 +19,80 @@
     </child>
     <child>
       <object class="GtkStack" id="popover-stack">
-        <property name="vhomogeneous">0</property>
-        <property name="hhomogeneous">0</property>
+        <property name="vhomogeneous">True</property>
+        <property name="hhomogeneous">True</property>
         <child>
-          <object class="GtkGrid" id="empty-search-grid">
-            <property name="name">search-city-grid</property>
-            <property name="orientation">vertical</property>
-            <property name="margin_top">25</property>
-            <property name="margin_bottom">25</property>
-            <property name="halign">center</property>
-            <property name="valign">center</property>
-            <property name="row_homogeneous">1</property>
-
-
-            <child>
-              <object class="GtkImage" id="search-image">
-                <property name="icon_name">edit-find-symbolic</property>
-                <property name="icon_size">2</property>
-                <property name="use_fallback">1</property>
-                <property name="halign">center</property>
-                <property name="valign">center</property>
-                <layout>
-                  <property name="column">0</property>
-                  <property name="row">0</property>
-                </layout>
-              </object>
-            </child>
+          <object class="GtkScrolledWindow" id="search-list-scroll-window">
+            <property name="hscrollbar-policy">never</property>
+            <property name="max-content-width">200</property>
             <child>
-              <object class="GtkLabel" id="search-label">
-                <property name="label" translatable="yes">Search for a city</property>
-                <property name="halign">center</property>
-                <property name="valign">center</property>
-                <style>
-                  <class name="dim-label"/>
-                </style>
-                <layout>
-                  <property name="column">0</property>
-                  <property name="row">1</property>
-                </layout>
+              <object class="GtkViewport">
+                <child>
+                  <object class="GtkListView" id="search-list-view">
+                    <property name="name">search-list-view</property>
+                    <!-- <property name="hexpand">1</property> -->
+                  </object>
+                </child>
               </object>
-
             </child>
-
           </object>
         </child>
         <child>
-          <object class="GtkScrolledWindow" id="search-list-scroll-window">
+          <object class="GtkScrolledWindow" id="locations-list-scroll-window">
+            <property name="hscrollbar-policy">never</property>
+            <property name="max-content-width">200</property>
             <child>
-              <object class="GtkListView" id="search-list-view">
-                <property name="name">search-list-view</property>
-                <property name="hexpand">1</property>
+              <object class="GtkViewport">
+                <child>
+                  <object class="GtkListBox" id="locations-list-box">
+                    <property name="name">locations-list-box</property>
+                    <property name="hexpand">True</property>
+                    <property name="selection-mode">none</property>
+                    <property name="show-separators">False</property>
+                    <child type="placeholder">
+                      <object class="GtkGrid" id="empty-search-grid">
+                        <property name="name">search-city-grid</property>
+                        <property name="orientation">vertical</property>
+                        <property name="margin_top">25</property>
+                        <property name="margin_bottom">25</property>
+                        <property name="halign">center</property>
+                        <property name="valign">center</property>
+                        <property name="row_homogeneous">1</property>
+                        <child>
+                          <object class="GtkImage" id="search-image">
+                            <property name="icon_name">edit-find-symbolic</property>
+                            <property name="icon_size">2</property>
+                            <property name="use_fallback">1</property>
+                            <property name="halign">center</property>
+                            <property name="valign">center</property>
+                            <layout>
+                              <property name="column">0</property>
+                              <property name="row">0</property>
+                            </layout>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel" id="search-label">
+                            <property name="label" translatable="yes">Search for a city</property>
+                            <property name="halign">center</property>
+                            <property name="valign">center</property>
+                            <style>
+                              <class name="dim-label" />
+                            </style>
+                            <layout>
+                              <property name="column">0</property>
+                              <property name="row">1</property>
+                            </layout>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
               </object>
             </child>
           </object>
         </child>
-        <child>
-          <object class="GtkListBox" id="locations-list-box">
-            <property name="name">locations-list-box</property>
-            <property name="hexpand">1</property>
-            <property name="selection-mode">none</property>
-            <property name="show-separators">false</property>
-          </object>
-        </child>
-
         <layout>
           <property name="column">0</property>
           <property name="row">2</property>
@@ -88,4 +100,4 @@
       </object>
     </child>
   </object>
-</interface>
+</interface>
\ No newline at end of file
diff --git a/data/style-dark.css b/data/style-dark.css
index e4b9fd7..b419d1e 100644
--- a/data/style-dark.css
+++ b/data/style-dark.css
@@ -1 +1,4 @@
-@define-color temp_chart_fill_color rgba(248, 228, 92, 0.15);
\ No newline at end of file
+@define-color weather_temp_chart_fill_color rgba(248, 228, 92, 0.15);
+@define-color weather_thermometer_high_color @yellow_1;
+@define-color weather_thermometer_low_color @blue_1;
+@define-color weather_forecast_color @light_1;
diff --git a/data/style.css b/data/style.css
index 2d1d4f0..ca1b660 100644
--- a/data/style.css
+++ b/data/style.css
@@ -1,8 +1,12 @@
-@define-color temp_chart_fill_color rgba(248, 228, 92, 0.5);
-@define-color temp_chart_stroke_color rgba(246, 211, 45, 1.0);
+@define-color weather_temp_chart_fill_color rgba(248, 228, 92, 0.5);
+@define-color weather_temp_chart_stroke_color rgba(246, 211, 45, 1.0);
 
-@define-color thermometer_warm_color rgb(245, 194, 17);
-@define-color thermometer_cold_color rgb(28, 113, 216);
+@define-color weather_thermometer_warm_color rgb(245, 194, 17);
+@define-color weather_thermometer_cold_color rgb(28, 113, 216);
+
+@define-color weather_thermometer_high_color #c89009;
+@define-color weather_thermometer_low_color #2174d9;
+@define-color weather_forecast_color #c89009;
 
 #places-label {
     font-weight: bold;
@@ -14,6 +18,10 @@
     margin-left: 16px;
 }
 
+#loadingLabel {
+    font-size: 16pt;
+}
+
 #apparent-label {
     font-size: 9pt;
 }
@@ -44,21 +52,20 @@
     margin: 10px;
 }
 
-WeatherLocationRow {
-    padding: 10px;
+.weather-popover {
+    margin-top: 10px;
 }
 
-WeatherLocationRow #label {
-    margin-bottom: 10px;
-    font-size: 11pt;
+.weather-popover contents {
+    padding: 0;
 }
 
-WeatherLocationRow #countryLabel {
-    font-size: 9pt;
+WeatherLocationRow {
+    padding: 10px;
 }
 
-.weather-popover contents {
-    padding: 0;
+WeatherLocationRow #label {
+    margin-bottom: 10px;
 }
 
 #currentIcon {
@@ -89,19 +96,19 @@ WeatherLocationRow #countryLabel {
 .forecast-temperature-label {
     font-weight: bold;
     font-size: 12pt;
-    color: #c89009;
+    color: @weather_forecast_color;
 }
 
 WeatherThermometer > label.high {
     font-weight: bold;
     font-size: 13pt;
-    color: #c89009;
+    color: @weather_thermometer_high_color;
 }
 
 WeatherThermometer > label.low {
     font-weight: bold;
     font-size: 13pt;
-    color: #2174d9;
+    color: @weather_thermometer_low_color;
 }
 
 .day-label {
diff --git a/data/weather-widget.ui b/data/weather-widget.ui
index cc84729..2eb04c8 100644
--- a/data/weather-widget.ui
+++ b/data/weather-widget.ui
@@ -16,7 +16,6 @@
             <property name="column_spacing">10</property>
             <child>
               <object class="GtkImage" id="conditionsImage">
-                <!-- <property name="name">conditions-image</property> -->
                 <property name="halign">start</property>
                 <property name="valign">center</property>
                 <property name="pixel_size">84</property>
diff --git a/data/window.ui b/data/window.ui
index 44aa22a..39a906f 100644
--- a/data/window.ui
+++ b/data/window.ui
@@ -23,7 +23,6 @@
     </section>
   </menu>
   <template class="Gjs_MainWindow">
-
     <property name="default_width">760</property>
     <property name="default_height">520</property>
     <child>
@@ -102,19 +101,11 @@
                     <property name="title" translatable="yes">Welcome to Weather!</property>
                     <property name="description" translatable="yes">To get started, select a 
location.</property>
                     <child>
-                      <object class="GtkBox">
-                        <property name="orientation">vertical</property>
-                        <child>
-                          <object class="Gjs_LocationSearchEntry" id="searchEntry">
-                            <!-- TODO: Our custom widget doesn't have these properties <property 
name="hexpand">False</property>
-                            <property name="halign">center</property>
-                            <property name="width-request">246</property> -->
-                            <!-- <property name="placeholder_text" translatable="yes">Search for a city or 
country</property> -->
-                          </object>
-                        </child>
-                        <child>
-                          <object class="GtkListView" id="searchListView"></object>
-                        </child>
+                      <object class="GtkMenuButton" id="searchButton">
+                        <property name="hexpand">False</property>
+                        <property name="halign">center</property>
+                        <property name="width-request">146</property>
+                        <property name="label" translatable="yes">Search for a city or country</property>
                       </object>
                     </child>
 
diff --git a/src/app/application.js b/src/app/application.js
index 3d71dc8..06f608c 100644
--- a/src/app/application.js
+++ b/src/app/application.js
@@ -25,196 +25,206 @@ import GWeather from 'gi://GWeather';
 // ensure the type before we call to GtkBuilder
 import './entry.js';
 
-import * as Util from '../misc/util.js';
 import * as Window from './window.js';
 import * as World from '../shared/world.js';
 import * as CurrentLocationController from './currentLocationController.js';
 
 import { ShellIntegration } from './shell.js';
 
-export const Application = GObject.registerClass(
-    class WeatherApplication extends Adw.Application {
+export class WeatherApplication extends Adw.Application {
 
-        _init() {
-            super._init({
-                application_id: pkg.name,
-                flags: Gio.ApplicationFlags.CAN_OVERRIDE_APP_ID,
-            });
-            let name_prefix = '';
+    constructor() {
+        super({
+            application_id: pkg.name,
+            flags: Gio.ApplicationFlags.CAN_OVERRIDE_APP_ID,
+        });
+        let name_prefix = '';
 
-            GLib.set_application_name(name_prefix + _("Weather"));
-            Gtk.Window.set_default_icon_name(pkg.name);
+        GLib.set_application_name(name_prefix + _("Weather"));
+        Gtk.Window.set_default_icon_name(pkg.name);
 
-            this._mainWindow = undefined;
-        }
+        this._mainWindow = undefined;
+    }
 
-        get mainWindow() {
-            return this._mainWindow;
-        }
+    get mainWindow() {
+        return this._mainWindow;
+    }
 
-        set mainWindow(value) {
-            this._mainWindow = value;
-        }
+    set mainWindow(value) {
+        this._mainWindow = value;
+    }
 
-        _onQuit() {
-            this.quit();
-        }
+    _onQuit() {
+        this.quit();
+    }
 
-        _onShowLocation(action, parameter) {
-            let location = this.world.deserialize(parameter.deep_unpack());
-            let win = this._createWindow();
+    _onShowLocation(action, parameter) {
+        let location = this.world.deserialize(parameter.deep_unpack());
+        let win = this._createWindow();
 
-            let info = this.model.addNewLocation(location, false);
-            win.showInfo(info, false);
-            this._showWindowWhenReady(win);
-        }
+        let info = this.model.addNewLocation(location, false);
+        win.showInfo(info, false);
+        this._showWindowWhenReady(win);
+    }
 
-        _onShowSearch(action, parameter) {
-            let text = parameter.deep_unpack();
-            let win = this._createWindow();
+    _onShowSearch(action, parameter) {
+        let text = parameter.deep_unpack();
+        let win = this._createWindow();
 
-            win.showSearch(text);
-            this._showWindowWhenReady(win);
-        }
+        win.showSearch(text);
+        this._showWindowWhenReady(win);
+    }
 
-        vfunc_startup() {
-            super.vfunc_startup();
+    vfunc_startup() {
+        super.vfunc_startup();
 
-            this.world = GWeather.Location.get_world();
-            this.model = new World.WorldModel(this.world, true);
-            this.currentLocationController = new 
CurrentLocationController.CurrentLocationController(this.model);
+        this.world = GWeather.Location.get_world();
+        this.model = new World.WorldModel(this.world, true);
+        this.currentLocationController = new CurrentLocationController.CurrentLocationController(this.model);
 
-            this.model.load();
+        this.model.load();
 
 
-            this.model.connect('notify::loading', () => {
-                if (this.model.loading)
-                    this.mark_busy();
-                else
-                    this.unmark_busy();
-            });
+        this.model.connect('notify::loading', () => {
             if (this.model.loading)
                 this.mark_busy();
-
-            let quitAction = new Gio.SimpleAction({
-                enabled: true,
-                name: 'quit'
-            });
-            quitAction.connect('activate', () => this._onQuit());
-            this.add_action(quitAction);
-
-            let showLocationAction = new Gio.SimpleAction({
-                enabled: true,
-                name: 'show-location',
-                parameter_type: new GLib.VariantType('v'),
-            });
-            showLocationAction.connect('activate', (action, parameter) => {
-                this._onShowLocation(action, parameter);
-            });
-            this.add_action(showLocationAction);
-
-            let showSearchAction = new Gio.SimpleAction({
-                enabled: true,
-                name: 'show-search',
-                parameter_type: new GLib.VariantType('v'),
-            })
-            showSearchAction.connect('activate', (action, parameter) => {
-                this._onShowSearch(action, parameter);
-            });
-            this.add_action(showSearchAction);
-
-            let gwSettings = new Gio.Settings({ schema_id: 'org.gnome.GWeather4' });
-            // we would like to use g_settings_create_action() here
-            // but that does not handle correctly the case of 'default'
-            // we would also like to use g_settings_bind_with_mapping(), but that
-            // function is not introspectable (two callbacks, one destroy notify)
-            // so we hand code the behavior we want
-            function resolveDefaultTemperatureUnit(unit) {
-                unit = GWeather.TemperatureUnit.to_real(unit);
-                if (unit == GWeather.TemperatureUnit.CENTIGRADE)
-                    return new GLib.Variant('s', 'centigrade');
-                else if (unit == GWeather.TemperatureUnit.FAHRENHEIT)
-                    return new GLib.Variant('s', 'fahrenheit');
-                else
-                    return new GLib.Variant('s', 'default');
+            else
+                this.unmark_busy();
+        });
+        if (this.model.loading)
+            this.mark_busy();
+
+        let quitAction = new Gio.SimpleAction({
+            enabled: true,
+            name: 'quit'
+        });
+        quitAction.connect('activate', () => this._onQuit());
+        this.add_action(quitAction);
+
+        let showLocationAction = new Gio.SimpleAction({
+            enabled: true,
+            name: 'show-location',
+            parameter_type: new GLib.VariantType('v'),
+        });
+        showLocationAction.connect('activate', (action, parameter) => {
+            this._onShowLocation(action, parameter);
+        });
+        this.add_action(showLocationAction);
+
+        let showSearchAction = new Gio.SimpleAction({
+            enabled: true,
+            name: 'show-search',
+            parameter_type: new GLib.VariantType('v'),
+        })
+        showSearchAction.connect('activate', (action, parameter) => {
+            this._onShowSearch(action, parameter);
+        });
+        this.add_action(showSearchAction);
+
+        let gwSettings = new Gio.Settings({ schema_id: 'org.gnome.GWeather4' });
+        // Sync settings changes to the legacy GTK3 GWeather interface if it is
+        // available
+        let legacyGwSettings;
+        try {
+            legacyGwSettings = new Gio.Settings({ schema_id: 'org.gnome.GWeather' });
+        } catch {}
+
+        // we would like to use g_settings_create_action() here
+        // but that does not handle correctly the case of 'default'
+        // we would also like to use g_settings_bind_with_mapping(), but that
+        // function is not introspectable (two callbacks, one destroy notify)
+        // so we hand code the behavior we want
+        function resolveDefaultTemperatureUnit(unit) {
+            unit = GWeather.TemperatureUnit.to_real(unit);
+            if (unit == GWeather.TemperatureUnit.CENTIGRADE)
+                return new GLib.Variant('s', 'centigrade');
+            else if (unit == GWeather.TemperatureUnit.FAHRENHEIT)
+                return new GLib.Variant('s', 'fahrenheit');
+            else
+                return new GLib.Variant('s', 'default');
+        }
+        let temperatureAction = new Gio.SimpleAction({
+            enabled: true,
+            name: 'temperature-unit',
+            state: resolveDefaultTemperatureUnit(gwSettings.get_enum('temperature-unit')),
+            parameter_type: new GLib.VariantType('s')
+        });
+        temperatureAction.connect('activate', function (_, parameter) {
+            gwSettings.set_value('temperature-unit', parameter);
+            if (legacyGwSettings) {
+                legacyGwSettings.set_value('temperature-unit', parameter);
             }
-            let temperatureAction = new Gio.SimpleAction({
-                enabled: true,
-                name: 'temperature-unit',
-                state: resolveDefaultTemperatureUnit(gwSettings.get_enum('temperature-unit')),
-                parameter_type: new GLib.VariantType('s')
+        });
+        gwSettings.connect('changed::temperature-unit', function () {
+            temperatureAction.state = resolveDefaultTemperatureUnit(gwSettings.get_enum('temperature-unit'));
+        });
+        this.add_action(temperatureAction);
+
+        this.set_accels_for_action("win.selection-mode", ["Escape"]);
+        this.set_accels_for_action("win.select-all", ["<Control>a"]);
+        this.set_accels_for_action("app.quit", ["<Control>q"]);
+    }
+
+    vfunc_dbus_register(conn, path) {
+        this._shellIntegration = new ShellIntegration();
+        this._shellIntegration.export(conn, path);
+        return true;
+    }
+
+    vfunc_dbus_unregister(conn, path) {
+        this._shellIntegration.unexport(conn);
+    }
+
+    _createWindow() {
+        const window = new Window.MainWindow({ application: this });
+
+        // Store a weak reference to the window for cleanup...
+        this.mainWindow = window;
+
+        return window;
+    }
+
+    _showWindowWhenReady(win) {
+        let notifyId;
+        win.present();
+        if (this.model.loading) {
+            let timeoutId;
+            let model = this.model;
+
+            timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function () {
+                log('Timeout during model load, perhaps the network is not available?');
+                model.disconnect(notifyId);
+
+                return false;
             });
-            temperatureAction.connect('activate', function (_, parameter) {
-                gwSettings.set_value('temperature-unit', parameter);
-            });
-            gwSettings.connect('changed::temperature-unit', function () {
-                temperatureAction.state = 
resolveDefaultTemperatureUnit(gwSettings.get_enum('temperature-unit'));
-            });
-            this.add_action(temperatureAction);
-
-            this.set_accels_for_action("win.selection-mode", ["Escape"]);
-            this.set_accels_for_action("win.select-all", ["<Primary>a"]);
-            this.set_accels_for_action("app.quit", ["<Primary>q"]);
-        }
-
-        vfunc_dbus_register(conn, path) {
-            this._shellIntegration = new ShellIntegration();
-            this._shellIntegration.export(conn, path);
-            return true;
-        }
-
-        vfunc_dbus_unregister(conn, path) {
-            this._shellIntegration.unexport(conn);
-        }
+            notifyId = this.model.connect('notify::loading', function (model) {
+                if (model.loading)
+                    return;
 
-        _createWindow() {
-            const window = new Window.MainWindow({ application: this });
-
-            // Store a weak reference to the window for cleanup...
-            this.mainWindow = window;
-
-            return window;
+                model.disconnect(notifyId);
+                GLib.source_remove(timeoutId);
+            });
         }
 
-        _showWindowWhenReady(win) {
-            let notifyId;
-            win.present();
-            if (this.model.loading) {
-                let timeoutId;
-                let model = this.model;
-
-                timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function () {
-                    log('Timeout during model load, perhaps the network is not available?');
-                    model.disconnect(notifyId);
-
-                    return false;
-                });
-                notifyId = this.model.connect('notify::loading', function (model) {
-                    if (model.loading)
-                        return;
-
-                    model.disconnect(notifyId);
-                    GLib.source_remove(timeoutId);
-                });
-            }
+        return win;
+    }
 
-            return win;
-        }
+    vfunc_activate() {
+        let win = this._createWindow();
+        win.showDefault();
+        this._showWindowWhenReady(win);
+    }
 
-        vfunc_activate() {
-            let win = this._createWindow();
-            win.showDefault();
-            this._showWindowWhenReady(win);
-        }
+    vfunc_shutdown() {
+        GWeather.Info.store_cache();
+        this.model.saveSettingsNow();
 
-        vfunc_shutdown() {
-            GWeather.Info.store_cache();
-            this.model.saveSettingsNow();
+        // Ensure our main window is cleaned up before we exit.
+        this.mainWindow?.run_dispose();
+        this.mainWindow = undefined;
 
-            // Ensure our main window is cleaned up before we exit.
-            this.mainWindow?.run_dispose();
-            this.mainWindow = undefined;
+        super.vfunc_shutdown();
+    }
+};
 
-            super.vfunc_shutdown();
-        }
-    });
+GObject.registerClass(WeatherApplication);
diff --git a/src/app/city.js b/src/app/city.js
index 8bcacb5..a219e4e 100644
--- a/src/app/city.js
+++ b/src/app/city.js
@@ -53,8 +53,8 @@ export const WeatherWidget = GObject.registerClass({
         'attributionLabel'
     ],
 }, class WeatherWidget extends Gtk.Widget {
-    _init(application, window) {
-        super._init({
+    constructor(application, window) {
+        super({
             name: 'weather-page'
         });
 
@@ -66,7 +66,9 @@ export const WeatherWidget = GObject.registerClass({
 
         this._info = null;
 
-        this._worldView = new WorldView.WorldContentView(application, window, {}, this);
+        this._worldView = new WorldView.WorldContentView(application, window,  {
+            align: Gtk.Align.START,
+        });
         this._placesButton.set_popover(this._worldView);
 
         for (const adjustment of [this._forecastHourlyAdjustment, this._forecastDailyAdjustment]) {
@@ -188,26 +190,14 @@ export const WeatherWidget = GObject.registerClass({
     update(info) {
         this._info = info;
 
-        let location = info.location;
-        let city = location;
-        if (location.get_level() == GWeather.LocationLevel.WEATHER_STATION)
-            city = location.get_parent();
-
-        let country = city.get_parent();
-        while (country && country.get_level() > GWeather.LocationLevel.COUNTRY)
-            country = country.get_parent();
-
-        if (country)
-            this._placesButton.set_label(city.get_name() + ', ' + country.get_name());
-        else
-            this._placesButton.set_label(city.get_name());
+        const label = Util.getNameAndCountry(info.location);
+        this._placesButton.set_label(label.join(', '));
 
         this._worldView.refilter();
 
         this._conditionsImage.iconName = `${info.get_icon_name()}-large`;
 
         const [, tempValue] = info.get_value_temp(GWeather.TemperatureUnit.DEFAULT);
-        console.log(tempValue);
         this._temperatureLabel.label = '%d°'.format(Math.round(tempValue));
 
         const [, apparentValue] = info.get_value_apparent(GWeather.TemperatureUnit.DEFAULT);
@@ -281,16 +271,14 @@ export const WeatherView = GObject.registerClass({
     InternalChildren: ['spinner', 'stack']
 }, class WeatherView extends Gtk.Widget {
 
-    _init(application, window, params) {
-        super._init(params);
+    constructor(application, window, params) {
+        super(params);
 
         this._infoPage = new WeatherWidget(application, window);
         this._stack.add_named(this._infoPage, 'info');
 
         this._info = null;
         this._updateId = 0;
-
-        this._desktopSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' });
     }
 
     vfunc_unroot() {
@@ -315,20 +303,34 @@ export const WeatherView = GObject.registerClass({
         this._info = info;
 
         if (info) {
+            this._stack.visible_child_name = 'loading';
+            this._spinner.start();
             this._updateId = this._info.connect('updated', (info) => {
                 this._onUpdate(info)
             });
-            if (info.is_valid())
+
+            if (info.is_valid()) {
                 this._onUpdate(info);
+            } else {
+                info.update();
+            }
         }
     }
 
+    vfunc_map() {
+        super.vfunc_map();
+
+        this._spinner.start();
+    }
+
     vfunc_unmap() {
         if (this._updateId) {
             this._info.disconnect(this._updateId);
             this._updateId = 0;
         }
 
+        this._spinner.stop();
+
         super.vfunc_unmap();
     }
 
diff --git a/src/app/dailyForecast.js b/src/app/dailyForecast.js
index aab4d00..0168c43 100644
--- a/src/app/dailyForecast.js
+++ b/src/app/dailyForecast.js
@@ -24,10 +24,10 @@ import GWeather from 'gi://GWeather';
 import * as Thermometer from './thermometer.js';
 import * as Util from '../misc/util.js';
 
-export const DailyForecastBox = GObject.registerClass(class DailyForecastBox extends Gtk.Box {
+export class DailyForecastBox extends Gtk.Box {
 
-    _init() {
-        super._init({
+    constructor() {
+        super({
             orientation: Gtk.Orientation.HORIZONTAL,
             spacing: 0,
             name: 'daily-forecast-box',
@@ -160,7 +160,8 @@ export const DailyForecastBox = GObject.registerClass(class DailyForecastBox ext
             entry.unparent();
         }
     }
-});
+};
+GObject.registerClass(DailyForecastBox);
 
 export const DayEntry = GObject.registerClass({
     Template: 'resource:///org/gnome/Weather/day-entry.ui',
@@ -176,7 +177,7 @@ export const DayEntry = GObject.registerClass({
         'eveningHumidity', 'eveningWind'],
 }, class DayEntry extends Gtk.Widget {
 
-    _init(params) {
+    constructor(params) {
         const {
             datetime,
             maxTemp,
@@ -190,7 +191,7 @@ export const DayEntry = GObject.registerClass({
             evening
         } = params;
 
-        super._init();
+        super();
 
 
         this.datetime = datetime;
@@ -243,6 +244,12 @@ export const DayEntry = GObject.registerClass({
         this._setWindInfo(eveningInfo, this._eveningWind);
     }
 
+    vfunc_unroot() {
+        [...this].forEach(child => child.unparent());
+
+        super.vfunc_unroot();
+    }
+
     _setWindInfo(info, label) {
         let [ok, speed, direction] = info.get_value_wind(GWeather.SpeedUnit.DEFAULT);
         if (ok) {
diff --git a/src/app/entry.js b/src/app/entry.js
index c5ee54d..83d9a15 100644
--- a/src/app/entry.js
+++ b/src/app/entry.js
@@ -4,7 +4,7 @@ import GObject from 'gi://GObject';
 import Gtk from 'gi://Gtk';
 import GWeather from 'gi://GWeather';
 
-import * as World from './world.js';
+import * as Util from '../misc/util.js';
 import { LocationRow } from './locationRow.js';
 
 GWeather.Location.prototype[Symbol.iterator] = function* () {
@@ -16,33 +16,33 @@ GWeather.Location.prototype[Symbol.iterator] = function* () {
     }
 }
 
-/** @typedef {Gio.ListModel} ListModel */
-
 function getAllCitiesAndWeatherStations() {
-    const locations = [];
+    const locations = new Set();
     for (const region of GWeather.Location.get_world()) {
         for (const country of region) {
             for (const location of country) {
                 const level = location.get_level();
                 if (level === GWeather.LocationLevel.ADM1) {
-                    locations.push(location);
-
                     for (const cityOrStation of location) {
-                        const level = cityOrStation.get_level()
+                        const level = cityOrStation.get_level();
 
-                        if (level === GWeather.LocationLevel.WEATHER_STATION || level === 
GWeather.LocationLevel.CITY) {
-                            locations.push(cityOrStation);
+                        if (level === GWeather.LocationLevel.CITY) {
+                            locations.add(cityOrStation);
+                        } else if (level === GWeather.LocationLevel.WEATHER_STATION) {
+                            locations.add(cityOrStation);
                         }
                     }
+
                 } else if (level === GWeather.LocationLevel.CITY) {
-                    locations.push(location);
+                    locations.add(location);
                 } else if (level === GWeather.LocationLevel.WEATHER_STATION) {
-                    locations.push(location);
+                    locations.add(location);
                 }
             }
         }
     }
-    return locations;
+
+    return [...locations.values()];
 }
 
 const LocationListModel = GObject.registerClass(
@@ -50,8 +50,8 @@ const LocationListModel = GObject.registerClass(
         Implements: [Gio.ListModel]
     },
     class LocationListModel extends GObject.Object {
-        _init() {
-            super._init();
+        constructor() {
+            super();
 
             this._show_named_timezones = false;
 
@@ -61,8 +61,9 @@ const LocationListModel = GObject.registerClass(
         /**
          * @this {ListModel & this}
          */
-        fill() {
-            this._list.push(...getAllCitiesAndWeatherStations());
+        load() {
+            const items = getAllCitiesAndWeatherStations()
+            this._list.push(...items);
 
             this.items_changed(0, 0, this._list.length);
         }
@@ -84,18 +85,30 @@ const LocationListModel = GObject.registerClass(
     }
 );
 
+const locationListModel = new LocationListModel();
+imports.mainloop.idle_add(() => {
+    try {
+        locationListModel.load();
+    } catch (error) {
+        console.error(error);
+    }
+
+    return false;
+});
+
 // Avoid the overhead of closures and Gtk.StringFilter
 
 const LocationFilter = GObject.registerClass(
     class LocationFilter extends Gtk.Filter {
-        _init() {
-            super._init();
+        constructor() {
+            super();
 
             /** @type {WeakMap<GWeather.Location, string>} */
             this._itemMap = new WeakMap();
             this._filter = null;
             this._filterLowerCase = null;
         }
+
         setFilterString(filter) {
             if (filter !== this._filter) {
                 this._filter = filter;
@@ -120,7 +133,8 @@ const LocationFilter = GObject.registerClass(
 export const LocationSearchEntry = GObject.registerClass(
     {
         Properties: {
-            'location': GObject.ParamSpec.object("location", "location", "location", 
GObject.ParamFlags.READWRITE, GWeather.Location.$gtype)
+            'placeholder-text': GObject.ParamSpec.string('placeholder-text', 'placeholder-text', 
'placeholder-text', GObject.ParamFlags.READWRITE, ''),
+            'location': GObject.ParamSpec.object('location', 'location', 'location', 
GObject.ParamFlags.READWRITE, GWeather.Location.$gtype)
         },
         Signals: {
             'search-started': { param_types: [] },
@@ -128,20 +142,20 @@ export const LocationSearchEntry = GObject.registerClass(
         }
     },
     class LocationSearchEntry extends Gtk.Widget {
-        _init() {
-            super._init();
+        constructor() {
+            super();
 
-            this._model = new LocationListModel();
             this._location = null;
 
             this._entry = new Gtk.SearchEntry();
-
             this._entry.set_parent(this);
             this._entry.set_hexpand(true);
             this._popup = null;
 
             this.text = '';
-            this._entry.connect("search-changed", (source) => {
+
+            this.bind_property('placeholder-text', this._entry, 'placeholder-text', 
GObject.BindingFlags.DEFAULT);
+            this._entry.connect('search-changed', (source) => {
                 const text = source.get_text();
 
                 if (text === null || text === '') {
@@ -153,20 +167,10 @@ export const LocationSearchEntry = GObject.registerClass(
 
                 if (!this.text) this.emit('search-started');
                 this.text = text;
+
                 this._filter?.setFilterString(text);
                 this.emit('search-updated', text);
             });
-
-            imports.mainloop.idle_add(() => {
-                try {
-                    this._model.fill();
-
-                } catch (error) {
-                    console.error(error);
-                }
-
-                return false;
-            });
         }
 
         get searchText() {
@@ -190,13 +194,13 @@ export const LocationSearchEntry = GObject.registerClass(
          * @param {Gtk.ListView} listview 
          */
         setListView(listview) {
-            if (this._listview) {
-                this._listview.set_factory(null);
-                this._listview.set_model(null);
-                this._listview.unparent();
+            if (this._listView) {
+                this._listView.set_factory(null);
+                this._listView.set_model(null);
+                this._listView.unparent();
             }
 
-            this._listview = listview;
+            this._listView = listview;
             const factory = this._buildFactory();
             listview.set_factory(factory);
             const selection = this._populateModel();
@@ -208,8 +212,9 @@ export const LocationSearchEntry = GObject.registerClass(
             this._filter = filter;
 
             let filter_model = new Gtk.FilterListModel({
-                model: this._model,
-                filter: this._filter
+                model: locationListModel,
+                filter: this._filter,
+                incremental: true,
             });
             let selection = new Gtk.SingleSelection({
                 model: filter_model
@@ -228,20 +233,19 @@ export const LocationSearchEntry = GObject.registerClass(
 
         _buildFactory() {
             let factory = new Gtk.SignalListItemFactory();
-            this._setupId = factory.connect("setup", (source, item) => {
-
+            this._setupId = factory.connect('setup', (_, item) => {
                 item.set_child(new LocationRow({ name: '', countryName: '' }));
             });
-            this._bindId = factory.connect("bind", (source, listitem) => {
+            this._bindId = factory.connect('bind', (_, listitem) => {
                 const row = listitem.get_child();
                 /** @type {GWeather.Location} */
                 const location = listitem.get_item();
 
                 if (row instanceof LocationRow) {
-                    const parentName = location.get_parent().get_name();
-                    const locationName = location.get_name();
-                    row.name = locationName;
-                    row.countryName = parentName ?? '';
+                    const [name, countryName = ''] = Util.getNameAndCountry(location);
+
+                    row.name = name;
+                    row.countryName = countryName;
                 }
             });
 
@@ -249,7 +253,7 @@ export const LocationSearchEntry = GObject.registerClass(
         }
 
         vfunc_unroot() {
-            this._listview?.set_model(null);
+            this._listView?.set_model(null);
 
             super.vfunc_unroot();
         }
diff --git a/src/app/hourlyForecast.js b/src/app/hourlyForecast.js
index ca6e07e..5fd9d7b 100644
--- a/src/app/hourlyForecast.js
+++ b/src/app/hourlyForecast.js
@@ -29,9 +29,9 @@ import * as Util from '../misc/util.js';
 // In microseconds
 const TWENTY_FOUR_HOURS = 24 * 3600 * 1000 * 1000;
 
-export const HourlyForecastBox = GObject.registerClass(class HourlyForecastBox extends Gtk.Box {
-    _init() {
-        super._init({
+export class HourlyForecastBox extends Gtk.Box {
+    constructor() {
+        super({
             orientation: Gtk.Orientation.HORIZONTAL,
             spacing: 0,
             name: 'hourly-forecast-box',
@@ -180,7 +180,7 @@ export const HourlyForecastBox = GObject.registerClass(class HourlyForecastBox e
         const graphMaxY = height - lineWidth / 2 - spacing - entryTemperatureLabelHeight - spacing;
         const graphHeight = graphMaxY - graphMinY;
 
-        let [, strokeColor] = this.get_style_context().lookup_color('temp_chart_stroke_color');
+        let [, strokeColor] = this.get_style_context().lookup_color('weather_temp_chart_stroke_color');
         Gdk.cairo_set_source_rgba(cr, strokeColor);
 
         let x = 0;
@@ -199,7 +199,7 @@ export const HourlyForecastBox = GObject.registerClass(class HourlyForecastBox e
         cr.setLineWidth(lineWidth);
         cr.strokePreserve();
 
-        let [, fillColor] = this.get_style_context().lookup_color('temp_chart_fill_color');
+        let [, fillColor] = this.get_style_context().lookup_color('weather_temp_chart_fill_color');
 
         Gdk.cairo_set_source_rgba(cr, fillColor);
 
@@ -210,16 +210,16 @@ export const HourlyForecastBox = GObject.registerClass(class HourlyForecastBox e
         super.vfunc_snapshot(snapshot);
         cr.$dispose();
     }
-});
+};
+GObject.registerClass(HourlyForecastBox);
 
 export const HourEntry = GObject.registerClass({
     Template: 'resource:///org/gnome/Weather/hour-entry.ui',
 
     InternalChildren: ['timeLabel', 'image', 'temperatureLabel'],
 }, class HourEntry extends Gtk.Widget {
-
-    _init({ timeLabel, info, ...params }) {
-        super._init({ ...params });
+    constructor({ timeLabel, info, ...params }) {
+        super({ ...params });
 
         Object.assign(this.layoutManager, {
             orientation: Gtk.Orientation.VERTICAL,
@@ -229,6 +229,12 @@ export const HourEntry = GObject.registerClass({
         this._image.iconName = info.get_icon_name() + '-small';
         this._temperatureLabel.label = Util.getTempString(info);
     }
+
+    vfunc_unroot() {
+        [...this].forEach(child => child.unparent());
+
+        super.vfunc_unroot();
+    }
 });
 
 HourEntry.set_layout_manager_type(Gtk.BoxLayout);
\ No newline at end of file
diff --git a/src/app/locationRow.js b/src/app/locationRow.js
index 0fdf1c0..c0f4459 100644
--- a/src/app/locationRow.js
+++ b/src/app/locationRow.js
@@ -8,8 +8,8 @@ export const LocationRow = GObject.registerClass({
     Template: GLib.Uri.resolve_relative(import.meta.url, './locationRow.ui', 0),
     InternalChildren: ['label', 'countryLabel', 'locationIcon', 'currentIcon'],
 }, class LocationRow extends Gtk.Widget {
-    _init({ name, countryName, isSelected = false, isCurrentLocation = false }) {
-        super._init({});
+    constructor({ name, countryName, isSelected = false, isCurrentLocation = false }) {
+        super();
 
         Object.assign(this.layoutManager, {
             orientation: Gtk.Orientation.HORIZONTAL,
@@ -36,5 +36,11 @@ export const LocationRow = GObject.registerClass({
     set isSelected(is) {
         this._currentIcon.visible = is;
     }
+
+    vfunc_unroot() {
+        [...this].forEach(child => child.unparent());
+
+        super.vfunc_unroot();
+    }
 });
 LocationRow.set_layout_manager_type(Gtk.BoxLayout);
diff --git a/src/app/locationRow.ui b/src/app/locationRow.ui
index cea4e77..56b31fd 100644
--- a/src/app/locationRow.ui
+++ b/src/app/locationRow.ui
@@ -10,6 +10,9 @@
             <property name="name">label</property>
             <property name="justify">left</property>
             <property name="halign">start</property>
+            <style>
+              <class name="title-4" />
+            </style>
           </object>
         </child>
         <child>
@@ -17,6 +20,9 @@
             <property name="name">countryLabel</property>
             <property name="justify">left</property>
             <property name="halign">start</property>
+            <style>
+              <class name="body" />
+            </style>
           </object>
         </child>
       </object>
@@ -24,14 +30,17 @@
     <child>
       <object class="GtkImage" id="currentIcon">
         <property name="name">currentIcon</property>
-        <property name="hexpand">1</property>
+        <property name="visible">False</property>
         <property name="icon-name">emblem-ok-symbolic</property>
+        <property name="margin-start">12</property>
         <property name="halign">start</property>
       </object>
     </child>
     <child>
       <object class="GtkImage" id="locationIcon">
         <property name="name">locationIcon</property>
+        <property name="visible">False</property>
+        <property name="hexpand">True</property>
         <property name="icon-name">find-location-symbolic</property>
         <property name="halign">end</property>
       </object>
diff --git a/src/app/main.js b/src/app/main.js
index 22ba956..52adc9b 100644
--- a/src/app/main.js
+++ b/src/app/main.js
@@ -28,7 +28,7 @@ import * as system from 'system';
 
 import Gio from 'gi://Gio';
 
-import {Application} from './application.js';
+import {WeatherApplication} from './application.js';
 
 pkg.initFormat();
 pkg.initGettext();
@@ -38,4 +38,4 @@ globalThis.getApp = function () {
     return Gio.Application.get_default();
 };
 
-new Application().run([system.programInvocationName, ...system.programArgs]);
+new WeatherApplication().run([system.programInvocationName, ...system.programArgs]);
diff --git a/src/app/thermometer.js b/src/app/thermometer.js
index 641622a..d040378 100644
--- a/src/app/thermometer.js
+++ b/src/app/thermometer.js
@@ -41,7 +41,7 @@ export class TemperatureRange {
   }
 }
 
-const ThermometerScale = GObject.registerClass({
+GObject.registerClass({
   CssName: 'WeatherThermometerScale',
   Properties: {
     'range': GObject.ParamSpec.jsobject(
@@ -53,8 +53,8 @@ const ThermometerScale = GObject.registerClass({
   },
 }, class ThermometerScale extends Gtk.Widget {
 
-  _init({ range = null, ...params }) {
-    super._init({
+  constructor({ range = null, ...params }) {
+    super({
       vexpand: true,
       halign: Gtk.Align.FILL,
       overflow: Gtk.Overflow.HIDDEN,
@@ -107,8 +107,8 @@ const ThermometerScale = GObject.registerClass({
 
     snapshot.push_rounded_clip(outline);
 
-    const [, warmColor] = this.get_style_context().lookup_color('thermometer_warm_color');
-    const [, coolColor] = this.get_style_context().lookup_color('thermometer_cold_color');
+    const [, warmColor] = this.get_style_context().lookup_color('weather_thermometer_warm_color');
+    const [, coolColor] = this.get_style_context().lookup_color('weather_thermometer_cold_color');
 
     snapshot.append_linear_gradient(
       bounds,
@@ -137,8 +137,8 @@ export const Thermometer = GObject.registerClass({
     ),
   },
 }, class Thermometer extends Gtk.Widget {
-  _init({ ...params }) {
-    super._init(params);
+  constructor({ ...params }) {
+    super(params);
 
     Object.assign(this.layoutManager, {
       orientation: Gtk.Orientation.VERTICAL,
diff --git a/src/app/window.js b/src/app/window.js
index 32d1d77..fe8409d 100644
--- a/src/app/window.js
+++ b/src/app/window.js
@@ -22,7 +22,7 @@ import GObject from 'gi://GObject';
 import Gtk from 'gi://Gtk';
 
 import * as City from './city.js';
-import * as CurrentLocationController from './currentLocationController.js';
+import { WorldContentView } from './world.js';
 
 const Page = {
     SEARCH: 0,
@@ -32,10 +32,10 @@ const Page = {
 export const MainWindow = GObject.registerClass({
     Template: 'resource:///org/gnome/Weather/window.ui',
     InternalChildren: ['header', 'refreshRevealer', 'refresh', 'forecastStackSwitcher', 'stack',
-        'titleStack', 'searchEntry', 'searchListView', 'searchView', 'forecastStackSwitcherBar']
+        'titleStack', 'searchButton', 'searchView', 'forecastStackSwitcherBar']
 }, class MainWindow extends Adw.ApplicationWindow {
-    _init(params) {
-        super._init(params);
+    constructor(params) {
+        super(params);
 
         this._world = this.application.world;
         this.currentInfo = null;
@@ -60,12 +60,10 @@ export const MainWindow = GObject.registerClass({
 
         this._searchView.icon_name = pkg.name;
 
-        this._searchEntry.connect('notify::location', (entry) => {
-            if (entry.location) {
-                let info = this._model.addNewLocation(entry.location);
-                this._model.setSelectedLocation(info);
-            }
+        this._worldView = new WorldContentView(this.application, this, {
+            align: Gtk.Align.CENTER,
         });
+        this._searchButton.set_popover(this._worldView);
 
         this._pageWidgets[Page.CITY].push(this._refresh);
 
@@ -88,6 +86,8 @@ export const MainWindow = GObject.registerClass({
     vfunc_unroot() {
         this._cityView.unparent();
         this._cityView = null;
+        this._worldView.unparent();
+        this._worldView = null;
 
         super.vfunc_unroot();
     }
@@ -122,13 +122,8 @@ export const MainWindow = GObject.registerClass({
     showSearch(text) {
         this._showingDefault = false;
         this._refreshRevealer.reveal_child = true;
-        // this._cityView.setTimeVisible(false);
         this._stack.set_visible_child(this._searchView);
         this._goToPage(Page.SEARCH);
-        this._searchEntry.text = text;
-        this._searchEntry.setListView(this._searchListView);
-        // if (text.length > 0)
-        // this._searchEntry.get_completion().complete();
     }
 
     updateCurrentLocation(info) { }
diff --git a/src/app/world.js b/src/app/world.js
index 383213e..d757667 100644
--- a/src/app/world.js
+++ b/src/app/world.js
@@ -20,17 +20,17 @@
 import GObject from 'gi://GObject';
 import Gtk from 'gi://Gtk';
 
+import * as Util from '../misc/util.js';
 import { LocationRow } from './locationRow.js';
 
-class _WorldContentView extends Gtk.Popover {
-    static [GObject.TypeName] = 'WorldContentView';
-
-    _init(application, window, params = {}) {
-        super._init({
-            // halign: Gtk.Align.START,
+export class WorldContentView extends Gtk.Popover {
+    constructor(application, window, { align, ...params } = {}) {
+        super({
+            ...params,
             hexpand: false,
+            halign: align,
             vexpand: false,
-            ...params,
+            hasArrow: false,
         });
 
         this.add_css_class('weather-popover');
@@ -43,38 +43,44 @@ class _WorldContentView extends Gtk.Popover {
 
         this._searchListView = builder.get_object('search-list-view');
         this._searchListScrollWindow = builder.get_object('search-list-scroll-window');
-        this._searchListScrollWindow.set_size_request(300, 400);
+        this._searchListScrollWindow.set_size_request(320, 200);
 
         this.model = application.model;
         this._window = window;
 
+        this._listboxScrollWindow = builder.get_object('locations-list-scroll-window');
         this._listbox = builder.get_object('locations-list-box');
-        this._listbox.set_header_func((row, previous) => {
-            let hasHeader = row.get_header() != null;
-            let shouldHaveHeader = previous != null;
-            if (hasHeader != shouldHaveHeader) {
-                if (shouldHaveHeader)
-                    row.set_header(new Gtk.Separator());
-                else
-                    row.set_header(null);
-            }
+        this._listbox.bind_model(this.model, (info) => {
+            return this._buildLocation(this.model, info);
         });
 
         this._locationEntry = builder.get_object('location-entry');
 
         this._locationEntry.setListView(this._searchListView);
-        this._locationEntry.connect('search-started', (entry, term) => {
-            this._syncStackPopover();
+        this._locationEntry.connect('search-started', () => {
+            this._stackPopover.set_visible_child(this._searchListScrollWindow);
+        });
+        this._locationEntry.connect('search-updated', (entry) => {
+            if (!entry.searchText) {
+                this._stackPopover.set_visible_child(this._listboxScrollWindow);
+                return;
+            }
+
+            this._stackPopover.set_visible_child(this._searchListScrollWindow);
         });
         this._locationEntry.connect('notify::location', (entry) => {
-            if (entry.searchText)
-                entry.searchText = '';
+            const location = entry.location;
+            entry.searchText = '';
+
+            this._locationChanged(location);
 
-            this._locationChanged(entry.location);
-            this._syncStackPopover();
+            this._stackPopover.set_visible_child(this._listboxScrollWindow);
 
-            if (!entry.searchText)
+            // Defer the popdown to allow the stack to re-render
+            imports.mainloop.idle_add(() => {
                 this.popdown();
+                return false;
+            });
         });
 
         this.connect('show', () => {
@@ -86,47 +92,29 @@ class _WorldContentView extends Gtk.Popover {
         this._listbox.connect('row-activated', (listbox, row) => {
             if (row._info)
                 this.model.setSelectedLocation(row._info);
-            this.popdown();
-        });
 
-        this.model.connect('selected-location-changed', (model, info) => {
-            console.log('selected')
-            this._window.showInfo(info);
-            this._onLocationAdded(model);
+            // Defer the popdown to allow the stack to re-render
+            imports.mainloop.idle_add(() => {
+                this.popdown();
+                return false;
+            });
         });
 
-        this.model.connect('current-location-changed', (model, info) => {
-            this._onLocationAdded(model);
+        this.model.connect('selected-location-changed', (_, info) => {
+            this._window.showInfo(info);
         });
 
-
         this._stackPopover = builder.get_object('popover-stack');
-        this._stackPopover.set_size_request(350, 400);
-        this._listbox.set_filter_func((row) => this._filterListbox(row));
-
-        this.model.connect('location-added', (model) => {
-            this._onLocationAdded(model);
-        });
-
-        this.model.connect('location-removed', (model) => {
-            this._onLocationAdded(model);
-        });
+        this._stackPopover.set_visible_child(this._listboxScrollWindow);
 
         this._currentLocationAdded = false;
 
-        this._onLocationAdded(this.model);
     }
 
     vfunc_unroot() {
-        this._listbox.set_header_func(null);
-        this._window = null;
-        // // TODO
-        // [...this._listbox].forEach(row => {
+        this._listbox.bind_model(null, null);
 
-        //     row._info = null;
-        //     row.child.unparent();
-        // });
-        // this._listbox = null;
+        this._window = null;
 
         super.vfunc_unroot();
     }
@@ -135,19 +123,6 @@ class _WorldContentView extends Gtk.Popover {
         this._listbox.invalidate_filter();
     }
 
-    _syncStackPopover() {
-        if (this._locationEntry.searchText) {
-            this._stackPopover.set_visible_child(this._searchListScrollWindow);
-        } else {
-            this._stackPopover.set_visible_child(this._listbox);
-        }
-    }
-
-    _filterListbox(row) {
-        return this._window.currentInfo == null ||
-            row._info != this._window.currentInfo;
-    }
-
     _locationChanged(location) {
         if (location) {
             let info = this.model.addNewLocation(location);
@@ -155,32 +130,18 @@ class _WorldContentView extends Gtk.Popover {
         }
     }
 
-    _onLocationAdded(model) {
-        const infos = model.getAll();
-
-        // TODO: This just re-populates the listbox on each updated.
-        // Working on a model-backed version to re-use components but
-        // the current "model" can't simply be converted as the code
-        // is shared across the service and UI and it would complicate
-        // the service
-        [...this._listbox].forEach(row => this._listbox.remove(row));
+    _buildLocation(model, info) {
+        if (!info) return null;
 
-        for (const info of infos) {
-            let location = info.location;
+        let location = info.location;
 
-            let name = location.get_city_name() ?? location.get_name();
-            let countryName = location.get_country_name();
-
-            const grid = new LocationRow({ name, countryName, isSelected: model.isSelectedLocation(info), 
isCurrentLocation: model.isCurrentLocation(info) });
-            const row = new Gtk.ListBoxRow({ child: grid });
-            row._info = info;
-
-            this._listbox.append(row);
-
-        }
+        const [name, countryName = ''] = Util.getNameAndCountry(location);
 
-        this._syncStackPopover();
+        const grid = new LocationRow({ name, countryName, isSelected: model.isSelectedLocation(info), 
isCurrentLocation: model.isCurrentLocation(info) });
+        const row = new Gtk.ListBoxRow({ child: grid });
+        row._info = info;
+        return row;
     }
 };
 
-export const WorldContentView = GObject.registerClass(_WorldContentView);
+GObject.registerClass(WorldContentView);
diff --git a/src/misc/util.js b/src/misc/util.js
index 33bc7e2..973ec69 100644
--- a/src/misc/util.js
+++ b/src/misc/util.js
@@ -164,14 +164,30 @@ function getTemp(info) {
 }
 
 function formatTemperature(value) {
-    return value ? `${Math.round(value).toFixed(0)}°` : undefined;
+    return typeof value === 'number' ? `${Math.round(value).toFixed(0)}°` : undefined;
 };
 
 function getTempString(info) {
-    let [ok, temp] = info.get_value_temp(GWeather.TemperatureUnit.DEFAULT);
-    if (!ok)
-        return "--";
-    return formatTemperature(temp);
+    try {
+        let [, temp] = info.get_value_temp(GWeather.TemperatureUnit.DEFAULT);
+        return formatTemperature(temp);
+    } catch {
+        return "";
+    }
+}
+
+/**
+ * @returns {[string] | [string, string]}
+ */
+function getNameAndCountry(location) {
+    let country = location.get_parent();
+    while (country && country.get_level() > GWeather.LocationLevel.COUNTRY)
+        country = country.get_parent();
+
+    if (country)
+       return [location.get_name(), country.get_name()];
+    else
+        return [location.get_name()];
 }
 
 export {
@@ -191,5 +207,6 @@ export {
     getDay,
     easeOutCubic,
     getEnabledProviders,
-    getWeatherConditions
+    getWeatherConditions,
+    getNameAndCountry
 }
\ No newline at end of file
diff --git a/src/org.gnome.Weather.BackgroundService.in b/src/org.gnome.Weather.BackgroundService.in
index c7bda28..53c9ccc 100755
--- a/src/org.gnome.Weather.BackgroundService.in
+++ b/src/org.gnome.Weather.BackgroundService.in
@@ -7,10 +7,6 @@ imports.package.init({ name: "@APP_ID@",
 import('resource:///org/gnome/Weather/js/service/main.js').then(({ main }) => {
     main([imports.system.programInvocationName, ...imports.system.programArgs]);
 }).catch(error => {
-    logError(error);
+    console.error(error);
     System.exit(1);
-}).finally(() => {
-    imports.mainloop.quit();
 });
-
-imports.mainloop.run();
\ No newline at end of file
diff --git a/src/service/main.js b/src/service/main.js
index db74c10..b0ace5e 100644
--- a/src/service/main.js
+++ b/src/service/main.js
@@ -40,8 +40,8 @@ function initEnvironment() {
 const BackgroundService = GObject.registerClass(
     class WeatherBackgroundService extends Gio.Application {
 
-    _init() {
-        super._init({ application_id: pkg.name,
+    constructor() {
+        super({ application_id: pkg.name,
                       flags: Gio.ApplicationFlags.IS_SERVICE,
                       inactivity_timeout: 60000 });
         GLib.set_application_name(_("Weather"));
diff --git a/src/shared/world.js b/src/shared/world.js
index 7b96a2a..633f508 100644
--- a/src/shared/world.js
+++ b/src/shared/world.js
@@ -18,6 +18,7 @@
 
 import GObject from 'gi://GObject';
 import GLib from 'gi://GLib';
+import Gio from 'gi://Gio';
 import GWeather from 'gi://GWeather';
 
 import * as Util from '../misc/util.js';
@@ -25,17 +26,15 @@ import * as Util from '../misc/util.js';
 export const WorldModel = GObject.registerClass({
     Signals: {
         'selected-location-changed': { param_types: [GWeather.Info] },
-        'current-location-changed': { param_types: [GWeather.Info] },
-        'location-added': { param_types: [GWeather.Info] },
-        'location-removed': { param_types: [GWeather.Info] }
     },
     Properties: {
         'loading': GObject.ParamSpec.boolean('loading', '', '', GObject.ParamFlags.READABLE, false)
     },
+    Implements: [Gio.ListModel]
 }, class WorldModel extends GObject.Object {
 
-    _init(world, enableGtk) {
-        super._init();
+    constructor(world) {
+        super();
 
         this._world = world;
 
@@ -45,31 +44,27 @@ export const WorldModel = GObject.registerClass({
         this._loadingCount = 0;
 
         this._currentLocationInfo = null;
+        this._selectedLocation = null;
         this._infoList = [];
+        this.getAll();
     }
 
     get length() {
-        return this.getAll().length
+        return this._allInfos.length
     }
 
     getAll() {
-        // TODO: Clean this up
-        const infos = [...this._infoList];
-        const currentIndex = infos.findIndex(info => this._currentLocationInfo && 
info.get_location().equal(this._currentLocationInfo.get_location()))
-        const selectIndex = infos.findIndex(info => this._selectedLocation && 
info.get_location().equal(this._selectedLocation.get_location()));
-        if (this._currentLocationInfo && currentIndex > 0) {
-            infos.splice(currentIndex, 1);
-            infos.unshift(this._currentLocationInfo)
-        }
+        // Ensure the current location and selected location are returned first...
+        const infos = [...this._infoList].filter(info => !this.isCurrentLocation(info) && 
!this.isSelectedLocation(info));
 
-        if (this._selectedLocation && selectIndex > 0) {
-            infos.splice(selectIndex, 1);
+        if (this._currentLocationInfo)
+            infos.unshift(this._currentLocationInfo);
 
-            infos.unshift(this._selectedLocation)
+        if (this._selectedLocation && this._currentLocationInfo !== this._selectedLocation) {
+            infos.unshift(this._selectedLocation);
         }
 
-
-
+        this._allInfos = infos;
         return infos;
     }
 
@@ -95,10 +90,11 @@ export const WorldModel = GObject.registerClass({
     }
 
     currentLocationChanged(location) {
-        if (location)
+        if (location) {
             this._currentLocationInfo = this.buildInfo(location);
-        if (this._currentLocationInfo)
-            this.emit('current-location-changed', this._currentLocationInfo);
+            this.addCurrentLocation(this._currentLocationInfo);
+            this.#invalidate();
+        }
     }
 
     getRecent() {
@@ -123,8 +119,17 @@ export const WorldModel = GObject.registerClass({
 
             info = this._addLocationInternal(location);
         }
-        if (info)
-            this.setSelectedLocation(info)
+
+        if (info) {
+            this.setSelectedLocation(info);
+        }
+
+        this.#invalidate();
+    }
+
+    #invalidate() {
+        this.getAll();
+        this.items_changed(0, this._allInfos.length, this._allInfos.length);
     }
 
     _updateLoadingCount(delta) {
@@ -162,18 +167,21 @@ export const WorldModel = GObject.registerClass({
     }
 
     isSelectedLocation(info) {
-        return !!this._selectedLocation && 
((this._selectedLocation.get_location()?.equal(info.get_location())) ?? false);
+        return !!this._selectedLocation && this._selectedLocation === info;
     }
 
     isCurrentLocation(info) {
-        return !!this._currentLocationInfo && 
(this._currentLocationInfo.get_location()?.equal(info.get_location()) ?? false);
+        return !!this._currentLocationInfo && this._currentLocationInfo === info;
     }
 
     addNewLocation(newLocation) {
         let info = this._addLocationInternal(newLocation);
+        this._setSelectedLocation = info;
+        info._isCurrentLocation = false;
 
-        this._queueSaveSettings();
+        this.#invalidate();
 
+        this._queueSaveSettings();
         return info;
     }
 
@@ -193,8 +201,17 @@ export const WorldModel = GObject.registerClass({
         let locations = [];
 
         for (let i = 0; i < this._infoList.length; i++) {
-            if (!this._infoList[i]._isCurrentLocation)
-                locations.push(this._infoList[i].location.serialize());
+            if (!this._infoList[i]._isCurrentLocation) {
+                let serialized = null;
+                try {
+                    serialized = this._infoList[i].location.serialize();
+                } catch (error) {
+                    console.error(error);
+                }
+
+                if (serialized)
+                    locations.push(serialized);
+            }
         }
 
         this._settings.set_value('locations', new GLib.Variant('av', locations));
@@ -210,18 +227,18 @@ export const WorldModel = GObject.registerClass({
         this._saveSettingsInternal();
     }
 
-    moveLocationToFront(info) {
-        if (this._infoList.length == 0 || this._infoList[0] == info)
+    addCurrentLocation(info) {
+        if (this._infoList.includes(info))
             return;
 
-        this._removeLocationInternal(info, true);
-        this._addInfoInternal(info, info._isCurrentLocation);
-
-        // mark info as a manually chosen location so that we
-        // save it
-        info._isCurrentLocation = false;
+        const existingInfo = this._infoList.find(info => info.get_location().equal(info.location));
+        if (existingInfo) {
+            this._currentLocationInfo = existingInfo;
+            return;
+        }
 
-        this._queueSaveSettings();
+        info._isCurrentLocation = true;
+        this._addInfoInternal(info);
     }
 
     _removeLocationInternal(oldInfo, skipDisconnect) {
@@ -243,7 +260,7 @@ export const WorldModel = GObject.registerClass({
             }
         }
 
-        this.emit('location-removed', oldInfo);
+        this.#invalidate();
     }
 
     buildInfo(location) {
@@ -256,11 +273,9 @@ export const WorldModel = GObject.registerClass({
     }
 
     _addLocationInternal(newLocation) {
-        for (let i = 0; i < this._infoList.length; i++) {
-            let info = this._infoList[i];
-            if (info.get_location().equal(newLocation))
-                return info;
-        }
+        const existingInfo = this._infoList.find(info => info.get_location().equal(newLocation));
+        if (existingInfo)
+            return existingInfo;
 
         let info = this.buildInfo(newLocation);
         this._addInfoInternal(info);
@@ -269,15 +284,24 @@ export const WorldModel = GObject.registerClass({
     }
 
     _addInfoInternal(info) {
-
         this._infoList.unshift(info);
         this.updateInfo(info);
 
-        this.emit('location-added', info);
-
         if (this._infoList.length > 10) {
             let oldInfo = this._infoList.pop();
             this._removeLocationInternal(oldInfo);
         }
     }
+
+    vfunc_get_item_type() {
+        return GWeather.Info.$gtype;
+    }
+
+    vfunc_get_n_items() {
+        return this._allInfos.length;
+    }
+
+    vfunc_get_item(n) {
+        return this._allInfos[n] ?? null;
+    }
 });


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]