[gnome-weather/wip/ewlsh/gtk4] Migrate widgets to templates and GTK4 hooks



commit fb1cf51b8b2e1e543ebba53f42de47842b01a5a0
Author: Evan Welsh <contact evanwelsh com>
Date:   Sun Jan 2 00:25:24 2022 -0600

    Migrate widgets to templates and GTK4 hooks

 data/application.css                       |   9 +
 data/weather-widget.ui                     | 366 ++++++++++++++---------------
 data/window.ui                             |  15 +-
 src/app/city.js                            | 101 +++-----
 src/app/dailyForecast.js                   | 328 ++++++++++----------------
 src/app/entry.js                           |  12 +-
 src/app/hourlyForecast.js                  |   2 -
 src/app/main.js                            | 308 ++++++++++++------------
 src/app/thermometer.js                     | 231 +++++++++---------
 src/app/thermometer.ui                     |  20 ++
 src/app/window.js                          |  29 +--
 src/misc/util.js                           |   8 +-
 src/org.gnome.Weather.src.gresource.xml.in |   1 +
 13 files changed, 665 insertions(+), 765 deletions(-)
---
diff --git a/data/application.css b/data/application.css
index 6aaf9a3..f6c7cc1 100644
--- a/data/application.css
+++ b/data/application.css
@@ -51,6 +51,15 @@
     color: #c89009;
 }
 
+WeatherThermometerScale {
+    margin: 12px;   
+}
+
+WeatherThermometerScale > .inner {
+    border-radius: 50% / 12px;
+    background-image: linear-gradient(#2174d9, #c89009);
+}
+
 WeatherThermometer > label.high {
     font-weight: bold;
     color: #c89009;
diff --git a/data/weather-widget.ui b/data/weather-widget.ui
index 2acc50f..372772f 100644
--- a/data/weather-widget.ui
+++ b/data/weather-widget.ui
@@ -2,229 +2,229 @@
 <interface>
   <requires lib="gtk" version="4.0"/>
   <template class="Gjs_WeatherWidget">
-
-
     <child>
-      <object class="AdwClamp" id="clamp">
-        <property name="maximum_size">1010</property>
-        <property name="tightening_threshold">600</property>
+      <object class="GtkBox" id="outerBox">
+        <property name="orientation">vertical</property>
+        <property name="margin-start">18</property>
+        <property name="margin-end">18</property>
+        <property name="margin-top">18</property>
+        <property name="margin-bottom">18</property>
+        <!-- <property name="spacing">18</property> -->
         <child>
-          <object class="GtkBox" id="outerBox">
-            <property name="orientation">vertical</property>
-            <property name="margin-start">18</property>
-            <property name="margin-end">18</property>
-            <property name="margin-top">18</property>
-            <property name="margin-bottom">18</property>
-            <!-- <property name="spacing">18</property> -->
+          <object class="GtkGrid">
+            <property name="name">conditions-grid</property>
+            <property name="column_spacing">10</property>
             <child>
-              <object class="GtkGrid">
-                <property name="name">conditions-grid</property>
-                <property name="column_spacing">10</property>
+              <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>
+                <style>
+                  <class name="icon-dropshadow"/>
+                </style>
+                <layout>
+                  <property name="column">0</property>
+                  <property name="row">0</property>
+                  <property name="row-span">2</property>
+                </layout>
+              </object>
+            </child>
+            <child>
+              <object class="GtkMenuButton" id="placesButton">
+                <property name="receives_default">1</property>
+                <property name="halign">start</property>
+                <property name="valign">start</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>
-                    <style>
-                      <class name="icon-dropshadow"/>
-                    </style>
-                    <layout>
-                      <property name="column">0</property>
-                      <property name="row">0</property>
-                      <property name="row-span">2</property>
-                    </layout>
+                  <object class="GtkBox" id="placesBox">
+                    <property name="spacing">12</property>
+                    <child>
+                      <object class="GtkLabel" id="placesLabel">
+                        <property name="name">places-label</property>
+                        <property name="wrap">1</property>
+                        <property name="wrap-mode">word-char</property>
+                        <property name="label" translatable="yes">Places</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkImage">
+                        <property name="icon_name">pan-down-symbolic</property>
+                      </object>
+                    </child>
                   </object>
                 </child>
+                <style>
+                  <class name="text-button"/>
+                  <class name="flat"/>
+                </style>
+                <layout>
+                  <property name="column">1</property>
+                  <property name="row">0</property>
+                </layout>
+              </object>
+            </child>
+            <child>
+              <object class="GtkBox" id="temperatureBox">
+                <property name="halign">start</property>
+                <property name="valign">start</property>
+                <property name="spacing">8</property> -->
+                <property name="baseline_position">bottom</property>
                 <child>
-                  <object class="GtkMenuButton" id="placesButton">
-                    <property name="receives_default">1</property>
+                  <object class="GtkLabel" id="temperatureLabel">
+                    <property name="name">temperature-label</property>
                     <property name="halign">start</property>
-                    <property name="valign">start</property>
-                    <child>
-                      <object class="GtkBox" id="placesBox">
-                        <property name="spacing">12</property>
-                        <child>
-                          <object class="GtkLabel" id="placesLabel">
-                            <property name="name">places-label</property>
-                            <property name="wrap">1</property>
-                            <property name="wrap-mode">word-char</property>
-                            <property name="label" translatable="yes">Places</property>
-                          </object>
-                        </child>
-                        <child>
-                          <object class="GtkImage">
-                            <property name="icon_name">pan-down-symbolic</property>
-                          </object>
-                        </child>
-                      </object>
-                    </child>
-                    <style>
-                      <class name="text-button"/>
-                      <class name="flat"/>
-                    </style>
-                    <layout>
-                      <property name="column">1</property>
-                      <property name="row">0</property>
-                    </layout>
+                    <property name="valign">baseline</property>
                   </object>
                 </child>
                 <child>
-                  <object class="GtkBox" id="temperatureBox">
+                  <object class="GtkLabel" id="apparentLabel">
+                    <property name="name">apparent-label</property>
                     <property name="halign">start</property>
-                    <property name="valign">start</property>
-                    <property name="spacing">8</property> -->
-                    <property name="baseline_position">bottom</property>
-                    <child>
-                      <object class="GtkLabel" id="temperatureLabel">
-                        <property name="name">temperature-label</property>
-                        <property name="halign">start</property>
-                        <property name="valign">baseline</property>
-                      </object>
-                    </child>
-                    <child>
-                      <object class="GtkLabel" id="apparentLabel">
-                        <property name="name">apparent-label</property>
-                        <property name="halign">start</property>
-                        <property name="valign">baseline</property>
-                      </object>
-                    </child>
-                    <layout>
-                      <property name="column">1</property>
-                      <property name="row">1</property>
-                    </layout>
+                    <property name="valign">baseline</property>
                   </object>
                 </child>
+                <layout>
+                  <property name="column">1</property>
+                  <property name="row">1</property>
+                </layout>
               </object>
             </child>
-            <child>
-              <object class="GtkOverlay">
-                <property name="child">
-                  <!-- <object class="GtkFrame" id="forecastFrame">
+          </object>
+        </child>
+        <child>
+          <object class="GtkOverlay">
+            <property name="child">
+              <!-- <object class="GtkFrame" id="forecastFrame">
                         <property name="name">forecast-frame</property>
                         <property name="child"> -->
-                  <object class="AdwViewStack" id="forecastStack">
-                    <!-- <property name="transition_type">crossfade</property> -->
-                    <child>
-                      <object class="AdwViewStackPage">
-                        <property name="name">hourly</property>
-                        <property name="title" translatable="yes">Hourly</property>
-                        <property name="icon-name">preferences-system-time-symbolic</property>
-                        <property name="child">
-                          <object class="GtkScrolledWindow" id="forecastHourly">
-                            <property name="focusable">1</property>
-                            <property name="vscrollbar_policy">never</property>
-                            <property name="min_content_width">308</property>
-                            <property name="child">
-                              <object class="GtkViewport" id="forecastHourlyViewport">
-                                <property name="hscroll_policy">natural</property>
-                                <property name="vscroll_policy">natural</property>
-                              </object>
-                            </property>
-                          </object>
+              <object class="AdwViewStack" id="forecastStack">
+                <child>
+                  <object class="AdwViewStackPage">
+                    <property name="name">hourly</property>
+                    <property name="title" translatable="yes">Hourly</property>
+                    <property name="icon-name">preferences-system-time-symbolic</property>
+                    <property name="child">
+                      <object class="GtkScrolledWindow">
+                        <property name="focusable">1</property>
+                        <property name="vscrollbar_policy">never</property>
+                        <property name="hscrollbar_policy">external</property>
+                        <property name="hadjustment">
+                          <object class="GtkAdjustment" id="forecastHourlyAdjustment" />
                         </property>
-                      </object>
-                    </child>
-                    <child>
-                      <object class="AdwViewStackPage">
-                        <property name="name">daily</property>
-                        <property name="title" translatable="yes">Daily</property>
-                        <property name="icon-name">x-office-calendar-symbolic</property>
+                        <property name="min_content_width">308</property>
                         <property name="child">
-                          <object class="GtkScrolledWindow" id="forecastDaily">
-                            <property name="focusable">1</property>
-                            <property name="vscrollbar_policy">never</property>
-                            <property name="min_content_width">308</property>
-                            <property name="child">
-                              <object class="GtkViewport" id="forecastDailyViewport">
-                                <property name="hscroll_policy">natural</property>
-                                <property name="vscroll_policy">natural</property>
-                              </object>
-                            </property>
+                          <object class="GtkViewport">
+                            <property name="hscroll_policy">natural</property>
+                            <property name="vscroll_policy">natural</property>
+                            <child>
+                              <object class="Gjs_HourlyForecastBox" id="forecastHourly" />
+                            </child>
                           </object>
                         </property>
                       </object>
-                    </child>
-                  </object>
-                  <!-- </property>
-                      </object> -->
-                </property>
-                <child type="overlay">
-                  <object class="GtkButton" id="rightButton">
-                    <property name="focusable">1</property>
-                    <property name="receives_default">1</property>
-                    <property name="halign">end</property>
-                    <property name="valign">center</property>
-                    <property name="margin_end">28</property>
-                    <child>
-                      <object class="GtkImage" id="right-image">
-                        <property name="icon_name">go-next-symbolic</property>
-                      </object>
-                    </child>
-                    <style>
-                      <class name="osd"/>
-                      <class name="circular"/>
-                    </style>
+                    </property>
                   </object>
                 </child>
-                <child type="overlay">
-                  <object class="GtkButton" id="leftButton">
-                    <property name="focusable">1</property>
-                    <property name="receives_default">1</property>
-                    <property name="halign">start</property>
-                    <property name="valign">center</property>
-                    <property name="margin_start">28</property>
-                    <child>
-                      <object class="GtkImage" id="left-image">
-                        <property name="icon_name">go-previous-symbolic</property>
-                        <property name="icon_size">1</property>
+                <child>
+                  <object class="AdwViewStackPage">
+                    <property name="name">daily</property>
+                    <property name="title" translatable="yes">Daily</property>
+                    <property name="icon-name">x-office-calendar-symbolic</property>
+                    <property name="child">
+                      <object class="GtkScrolledWindow">
+                        <property name="focusable">1</property>
+                        <property name="vscrollbar_policy">never</property>
+                        <property name="hscrollbar_policy">external</property>
+                        <property name="hadjustment">
+                          <object class="GtkAdjustment" id="forecastDailyAdjustment" />
+                        </property>
+                        <property name="min_content_width">308</property>
+                        <property name="child">
+                          <object class="GtkViewport">
+                            <property name="hscroll_policy">natural</property>
+                            <property name="vscroll_policy">natural</property>
+                            <child>
+                              <object class="Gjs_DailyForecastBox" id="forecastDaily" />
+                            </child>
+                          </object>
+                        </property>
                       </object>
-                    </child>
-                    <style>
-                      <class name="osd"/>
-                      <class name="circular"/>
-                    </style>
+                    </property>
                   </object>
                 </child>
               </object>
-            </child>
-            <child>
-              <object class="GtkGrid">
-                <property name="row_spacing">8</property>
+            </property>
+            <child type="overlay">
+              <object class="GtkButton" id="rightButton">
+                <property name="focusable">1</property>
+                <property name="receives_default">1</property>
+                <property name="halign">end</property>
+                <property name="valign">center</property>
+                <property name="margin_end">28</property>
                 <child>
-                  <object class="GtkLabel" id="updatedTimeLabel">
-                    <property name="name">updated-time-label</property>
-                    <property name="halign">start</property>
-                    <layout>
-                      <property name="column">0</property>
-                      <property name="row">0</property>
-                    </layout>
+                  <object class="GtkImage" id="right-image">
+                    <property name="icon_name">go-next-symbolic</property>
                   </object>
                 </child>
+                <style>
+                  <class name="osd"/>
+                  <class name="circular"/>
+                </style>
+              </object>
+            </child>
+            <child type="overlay">
+              <object class="GtkButton" id="leftButton">
+                <property name="focusable">1</property>
+                <property name="receives_default">1</property>
+                <property name="halign">start</property>
+                <property name="valign">center</property>
+                <property name="margin_start">28</property>
                 <child>
-                  <object class="GtkLabel" id="attributionLabel">
-                    <property name="name">attribution-label</property>
-                    <property name="use_markup">1</property>
-                    <property name="wrap">1</property>
-                    <!-- <property name="track_visited_links">False</property> -->
-                    <property name="xalign">0</property>
-                    <layout>
-                      <property name="column">0</property>
-                      <property name="row">1</property>
-                    </layout>
+                  <object class="GtkImage" id="left-image">
+                    <property name="icon_name">go-previous-symbolic</property>
+                    <property name="icon_size">1</property>
                   </object>
                 </child>
+                <style>
+                  <class name="osd"/>
+                  <class name="circular"/>
+                </style>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkGrid">
+            <property name="row_spacing">8</property>
+            <child>
+              <object class="GtkLabel" id="updatedTimeLabel">
+                <property name="name">updated-time-label</property>
+                <property name="halign">start</property>
+                <layout>
+                  <property name="column">0</property>
+                  <property name="row">0</property>
+                </layout>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="attributionLabel">
+                <property name="name">attribution-label</property>
+                <property name="use_markup">1</property>
+                <property name="wrap">1</property>
+                <property name="xalign">0</property>
+                <layout>
+                  <property name="column">0</property>
+                  <property name="row">1</property>
+                </layout>
               </object>
             </child>
-            <layout>
-              <property name="column">0</property>
-              <property name="row">2</property>
-            </layout>
           </object>
         </child>
+        <layout>
+          <property name="column">0</property>
+          <property name="row">2</property>
+        </layout>
       </object>
     </child>
-
-
   </template>
 </interface>
diff --git a/data/window.ui b/data/window.ui
index a46ec83..7478f6b 100644
--- a/data/window.ui
+++ b/data/window.ui
@@ -31,9 +31,8 @@
         <property name="orientation">vertical</property>
         <child>
           <object class="AdwHeaderBar" id="header">
-            <!-- <property name="show_close_button">True</property> -->
-            <!-- <property name="centering_policy">strict</property> -->
-            <child>
+            <property name="centering_policy">strict</property>
+            <child type="start">
               <object class="GtkRevealer" id="refreshRevealer">
                 <property name="transition_type">crossfade</property>
                 <property name="child">
@@ -77,7 +76,6 @@
                     <property name="name">city</property>
                     <property name="child">
                       <object class="AdwViewSwitcherTitle" id="forecastStackSwitcher">
-                     
                         <property name="title" translatable="yes">Weather</property>
                       </object>
                     </property>
@@ -85,7 +83,7 @@
                 </child>
               </object>
             </child>
-            <child>
+            <child type="end">
               <object class="GtkMenuButton">
                 <property name="focusable">1</property>
                 <property name="valign">center</property>
@@ -101,20 +99,20 @@
         </child>
         <child>
           <object class="GtkStack" id="stack">
-       
+
             <property name="transition_type">crossfade</property>
             <child>
               <object class="GtkStackPage">
                 <property name="name">search</property>
                 <property name="child">
                   <object class="AdwStatusPage" id="searchView">
-                
+
                     <property name="icon_name">mark-location-symbolic</property>
                     <property name="title" translatable="yes">Welcome to Weather!</property>
                     <property name="description" translatable="yes">To get started, select a 
location.</property>
                     <child>
                       <object class="Gjs_LocationSearchEntry" id="searchEntry">
-                        <!-- <property name="hexpand">False</property>
+                        <!-- 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> -->
@@ -132,7 +130,6 @@
         </child>
         <child>
           <object class="AdwViewSwitcherBar" id="forecastStackSwitcherBar">
-      
             <property name="reveal" bind-source="forecastStackSwitcher" bind-property="title-visible" 
bind-flags="sync-create"/>
           </object>
         </child>
diff --git a/src/app/city.js b/src/app/city.js
index 9fd535f..16d62a6 100644
--- a/src/app/city.js
+++ b/src/app/city.js
@@ -16,6 +16,7 @@
 // with Gnome Weather; if not, write to the Free Software Foundation,
 // Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 
+const Adw = imports.gi.Adw;
 const Gio = imports.gi.Gio;
 const GLib = imports.gi.GLib;
 const GObject = imports.gi.GObject;
@@ -37,7 +38,6 @@ const UPDATED_TIME_TIMEOUT = 60; //s
 var WeatherWidget = GObject.registerClass({
     Template: 'resource:///org/gnome/Weather/weather-widget.ui',
     InternalChildren: [
-        'clamp',
         'conditionsImage',
         'placesButton',
         'temperatureLabel',
@@ -46,37 +46,31 @@ var WeatherWidget = GObject.registerClass({
         'leftButton',
         'rightButton',
         'forecastHourly',
-        'forecastHourlyViewport',
+        'forecastHourlyAdjustment',
         'forecastDaily',
-        'forecastDailyViewport',
+        'forecastDailyAdjustment',
         'updatedTimeLabel',
-        'attributionLabel'],
+        'attributionLabel'
+    ],
 }, class WeatherWidget extends Gtk.Widget {
     _init(application, window) {
         super._init({
             name: 'weather-page'
         });
 
+        Object.assign(this.layoutManager, {
+            maximumSize: 1010,
+            tighteningThreshold: 600,
+        });
+
         this._info = null;
 
         this._worldView = new WorldView.WorldContentView(application, window);
         this._placesButton.set_popover(this._worldView);
 
-        this._forecasts = {
-            hourly: new HourlyForecast.HourlyForecastBox(),
-            daily: new DailyForecast.DailyForecastBox(),
-        };
-        this._forecastHourlyViewport.set_child(this._forecasts.hourly);
-        this._forecastDailyViewport.set_child(this._forecasts.daily);
-
-        for (const scrollWindow of [this._forecastHourly, this._forecastDaily]) {
-            let hscrollbar = scrollWindow.get_hscrollbar();
-            hscrollbar.set_opacity(0.0);
-            hscrollbar.hide();
-
-            let hadjustment = scrollWindow.get_hadjustment();
-            hadjustment.connect('changed', () => this._syncLeftRightButtons());
-            hadjustment.connect('value-changed', () => this._syncLeftRightButtons());
+        for (const adjustment of [this._forecastHourlyAdjustment, this._forecastDailyAdjustment]) {
+            adjustment.connect('changed', () => this._syncLeftRightButtons());
+            adjustment.connect('value-changed', () => this._syncLeftRightButtons());
         }
 
         this._forecastStack.connect('notify::visible-child', () => {
@@ -110,50 +104,24 @@ var WeatherWidget = GObject.registerClass({
             this._beginScrollAnimation(target);
         });
 
-        // this._forecastFrame.connect('draw', (frame, cr) => {
-        //     const width = frame.get_allocated_width();
-        //     const height = frame.get_allocated_height();
-
-        //     const borderRadius = 8;
-
-        //     const arc0 = 0.0;
-        //     const arc1 = Math.PI * 0.5
-        //     const arc2 = Math.PI;
-        //     const arc3 = Math.PI * 1.5
-
-        //     cr.newSubPath();
-        //     cr.arc(width - borderRadius, borderRadius, borderRadius, arc3, arc0);
-        //     cr.arc(width - borderRadius, height - borderRadius, borderRadius, arc0, arc1);
-        //     cr.arc(borderRadius, height - borderRadius, borderRadius, arc1, arc2);
-        //     cr.arc(borderRadius, borderRadius, borderRadius, arc2, arc3);
-        //     cr.closePath();
-
-        //     cr.clip();
-        //     cr.fill();
-
-        //     return false;
-        // });
-
         this._updatedTime = null;
         this._updatedTimeTimeoutId = 0;
-
-        this.connect('destroy', () => this._onDestroy());
     }
 
-    // vfunc_measure(orientation, for_size) {
-    //     return this._box.measure(orientation, for_size);
-    // }
+    vfunc_unroot() {
+        this._worldView.unparent();
+        this._worldView = null;
 
-    _cleanup() {
-        // this._contentGrid.unparent();
-        this._worldView._cleanup();
+        super.vfunc_unroot();
     }
 
-    _onDestroy() {
+    vfunc_unmap() {
         if (this._updatedTimeTimeoutId) {
             GLib.Source.remove(this._updatedTimeTimeoutId);
             this._updatedTimeTimeoutId = 0;
         }
+
+        super.vfunc_unmap();
     }
 
     _syncLeftRightButtons() {
@@ -202,8 +170,8 @@ var WeatherWidget = GObject.registerClass({
     }
 
     clear() {
-        for (let t of ['hourly', 'daily'])
-            this._forecasts[t].clear();
+        this._forecastHourly.clear();
+        this._forecastDaily.clear();
 
         if (this._tickId) {
             this.remove_tick_callback(this._tickId);
@@ -242,8 +210,8 @@ var WeatherWidget = GObject.registerClass({
         const [, apparentValue] = info.get_value_apparent(GWeather.TemperatureUnit.DEFAULT);
         this._apparentLabel.label = _('Feels like %.0f°').format(apparentValue);
 
-        for (let t of ['hourly', 'daily'])
-            this._forecasts[t].update(info);
+        this._forecastHourly.update(info);
+        this._forecastDaily.update(info);
 
         if (this._updatedTimeTimeoutId)
             GLib.Source.remove(this._updatedTimeTimeoutId);
@@ -303,12 +271,11 @@ var WeatherWidget = GObject.registerClass({
     }
 });
 
-WeatherWidget.set_layout_manager_type(Gtk.BoxLayout);
+WeatherWidget.set_layout_manager_type(Adw.ClampLayout);
 
 
 var WeatherView = GObject.registerClass({
     Template: 'resource:///org/gnome/Weather/city.ui',
-
     InternalChildren: ['spinner', 'stack']
 }, class WeatherView extends Gtk.Widget {
 
@@ -321,14 +288,14 @@ var WeatherView = GObject.registerClass({
         this._info = null;
         this._updateId = 0;
 
-        this.connect('destroy', () => this._onDestroy());
-
         this._desktopSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' });
-
     }
 
-    _cleanup() {
-        this._infoPage._cleanup();
+    vfunc_unroot() {
+        this._infoPage.unparent();
+        this._infoPage = null;
+
+        super.vfunc_unroot();
     }
 
     get info() {
@@ -354,11 +321,13 @@ var WeatherView = GObject.registerClass({
         }
     }
 
-    _onDestroy() {
+    vfunc_unmap() {
         if (this._updateId) {
             this._info.disconnect(this._updateId);
             this._updateId = 0;
         }
+
+        super.vfunc_unmap();
     }
 
     update() {
@@ -376,8 +345,8 @@ var WeatherView = GObject.registerClass({
         this._stack.visible_child_name = 'info';
     }
 
-    getInfoPage() {
-        return this._infoPage;
+    getForecastStack() {
+        return this._infoPage.getForecastStack();
     }
 });
 
diff --git a/src/app/dailyForecast.js b/src/app/dailyForecast.js
index a0e3280..881a91e 100644
--- a/src/app/dailyForecast.js
+++ b/src/app/dailyForecast.js
@@ -74,32 +74,31 @@ var DailyForecastBox = GObject.registerClass(class DailyForecastBox extends Gtk.
             weekInfos.push(dayInfos);
             day = day.add_days(1);
         }
-        return weekInfos;
-    }
-
-    update(info) {
-        let forecasts = info.get_forecast_list();
 
-        let weekInfos = this._preprocess(forecasts);
+        const temperatures = weekInfos.map(dayInfos => dayInfos.infos)
+            .flat()
+            .map(info => Util.getTemp(info));
 
-        if (weekInfos.length > 0) {
-            let weekHighestTemp = -Infinity;
-            let weekLowestTemp = Infinity;
+        const weekHighestTemp = Math.max(...temperatures);
+        const weekLowestTemp = Math.min(...temperatures);
 
-            weekInfos.map(dayInfos => dayInfos.infos).flat().forEach(info => {
-                const temp = Util.getTemp(info);
+        return {
+            weekHighestTemp,
+            weekLowestTemp,
+            days: weekInfos
+        };
+    }
 
-                weekHighestTemp = Math.max(weekHighestTemp, temp);
-                weekLowestTemp = Math.min(weekLowestTemp, temp);
-            });
+    update(info) {
+        let forecasts = info.get_forecast_list();
 
-            for (let i = 0; i < weekInfos.length; i++) {
-                let dayInfos = weekInfos[i];
-                this._addDayEntry(dayInfos, weekHighestTemp, weekLowestTemp);
+        let forecast = this._preprocess(forecasts);
 
-                if (i < weekInfos.length - 1)
-                    this._addSeparator();
-            }
+        if (forecast.days.length > 1) {
+            forecast.days.reduce((_, dayInfos) => {
+                this.append(this._buildDayEntry(dayInfos, forecast.weekHighestTemp, 
forecast.weekLowestTemp));
+                this.append(this._buildSeparator());
+            }, null);
         } else {
             let label = new Gtk.Label({
                 label: _('Forecast not available'),
@@ -110,136 +109,58 @@ var DailyForecastBox = GObject.registerClass(class DailyForecastBox extends Gtk.
         }
     }
 
-    _addDayEntry({ day, infos }, weekHighestTemp, weekLowestTemp) {
-        let maxInfo;
-        let maxTemp = -Infinity;
-
-        let minInfo;
-        let minTemp = Infinity;
-
-        day = Util.getDay(day);
-        let dayInfo;
-        let dayDiff = Infinity;
-
-        let night = Util.getNight(day);
-        let nightInfo;
-        let nightDiff = Infinity;
+    _buildDayEntry({ day, infos }, weekHighestTemp, weekLowestTemp) {
+        let datetime = Util.getDay(day);
 
-        let morning = Util.getMorning(day);
-        let morningInfo;
-        let morningDiff = Infinity;
+        const temperatures = infos.map(info => Util.getTemp(info));
+        const minTemp = Math.min(...temperatures);
+        const maxTemp = Math.max(...temperatures);
 
-        let afternoon = Util.getAfternoon(day);
-        let afternoonInfo;
-        let afternoonDiff = Infinity;
+        let periodInfos = {}, times = {
+            day: Util.getDay(datetime),
+            night: Util.getNight(datetime),
+            morning: Util.getMorning(datetime),
+            afternoon: Util.getAfternoon(datetime),
+            evening: Util.getEvening(datetime)
+        };
 
-        let evening = Util.getEvening(day);
-        let eveningInfo;
-        let eveningDiff = Infinity;
-
-        for (let i = 0; i < infos.length; i++) {
-            let info = infos[i];
-
-            let temp = Util.getTemp(info);
-            if (temp > maxTemp) {
-                maxInfo = info;
-                maxTemp = temp;
-            }
-            if (temp < minTemp) {
-                minInfo = info;
-                minTemp = temp;
-            }
-
-            let datetime = Util.getDateTime(info);
-
-            let diff = Math.abs(datetime.difference(day));
-            if (diff < dayDiff) {
-                dayInfo = info;
-                dayDiff = diff;
-            }
-
-            diff = Math.abs(datetime.difference(night));
-            if (diff < nightDiff) {
-                nightInfo = info;
-                nightDiff = diff;
-            }
+        const datetimes = infos.map(info => Util.getDateTime(info));
 
-            diff = Math.abs(datetime.difference(morning));
-            if (diff < morningDiff) {
-                morningInfo = info;
-                morningDiff = diff;
-            }
+        for (const period of ['day', 'night', 'morning', 'afternoon', 'evening']) {
+            const differences = datetimes.map(datetime => Math.abs(datetime.difference(times[period])));
 
-            diff = Math.abs(datetime.difference(afternoon));
-            if (diff < afternoonDiff) {
-                afternoonInfo = info;
-                afternoonDiff = diff;
-            }
+            const index = differences.indexOf(Math.min(...differences))
 
-            diff = Math.abs(datetime.difference(evening));
-            if (diff < eveningDiff) {
-                eveningInfo = info;
-                eveningDiff = diff;
-            }
+            periodInfos[period] = infos[index];
         }
 
-        let dayEntry = new DayEntry();
-
-        let nameFormat = '%a';
-        dayEntry.nameLabel.label = day.format(nameFormat);
-
-        /* Translators: this is the time format for day and month name according to the current locale */
-        let dateFormat = _('%b %e');
-        dayEntry.dateLabel.label = day.format(dateFormat);
-
-        dayEntry.image.iconName = `${dayInfo.get_icon_name()}-small`;
-
-
-        dayEntry.thermometer.setTemperatureRange(weekLowestTemp, weekHighestTemp, minTemp, maxTemp);
-        dayEntry.nightTemperatureLabel.label = Util.getTempString(nightInfo);
-        dayEntry.nightImage.iconName = nightInfo.get_icon_name() + '-small';
-        dayEntry.nightHumidity.label = nightInfo.get_humidity();
-        this._setWindInfo(nightInfo, dayEntry.nightWind);
-
-        dayEntry.morningTemperatureLabel.label = Util.getTempString(morningInfo);
-        dayEntry.morningImage.iconName = morningInfo.get_icon_name() + '-small';
-        dayEntry.morningHumidity.label = morningInfo.get_humidity();
-        this._setWindInfo(morningInfo, dayEntry.morningWind);
-
-        dayEntry.afternoonTemperatureLabel.label = Util.getTempString(afternoonInfo);
-        dayEntry.afternoonImage.iconName = afternoonInfo.get_icon_name() + '-small';
-        dayEntry.afternoonHumidity.label = afternoonInfo.get_humidity();
-        this._setWindInfo(afternoonInfo, dayEntry.afternoonWind);
 
-        dayEntry.eveningTemperatureLabel.label = Util.getTempString(eveningInfo);
-        dayEntry.eveningImage.iconName = eveningInfo.get_icon_name() + '-small';
-        dayEntry.eveningHumidity.label = eveningInfo.get_humidity();
-        this._setWindInfo(eveningInfo, dayEntry.eveningWind);
+        const { day: dayInfo, night, morning, afternoon, evening } = periodInfos;
 
-        this.prepend(dayEntry);
+        return new DayEntry({
+            datetime,
+            weekHighestTemp,
+            weekLowestTemp,
+            maxTemp,
+            minTemp,
+            day: dayInfo,
+            night,
+            morning,
+            afternoon,
+            evening
+        });
     }
 
-    _addSeparator() {
-        let separator = new Gtk.Separator({
+    _buildSeparator() {
+        return new Gtk.Separator({
             orientation: Gtk.Orientation.VERTICAL,
             visible: true
         });
-        this.prepend(separator);
-    }
-
-    _setWindInfo(info, label) {
-        let [ok, speed, direction] = info.get_value_wind(GWeather.SpeedUnit.DEFAULT);
-        if (ok) {
-            label.label = `${speed.toFixed(1).toString()} 
${GWeather.speed_unit_to_string(GWeather.SpeedUnit.DEFAULT)}`;
-        } else {
-            /* Fall back to get_wind() */
-            label.label = info.get_wind();
-        }
     }
 
     clear() {
-        for (const w of Array.from(this)) {
-            this.remove(w);
+        for (const entry of Array.from(this)) {
+            entry.unparent();
         }
     }
 });
@@ -259,89 +180,80 @@ var DayEntry = GObject.registerClass({
 }, class DayEntry extends Gtk.Widget {
 
     _init(params) {
-        super._init(params);
-
+        const {
+            datetime,
+            maxTemp,
+            minTemp,
+            weekHighestTemp,
+            weekLowestTemp,
+            day,
+            night,
+            morning,
+            afternoon,
+            evening
+        } = params;
+
+        super._init();
+
+
+        this.datetime = datetime;
+        this.info = {
+            day,
+            night,
+            morning,
+            afternoon,
+            evening
+        };
+        this.maxTemp = maxTemp;
+        this.minTemp = minTemp;
+        this.weekHighestTemp = weekHighestTemp;
+        this.weekLowestTemp = weekLowestTemp;
         this.layoutManager.orientation = Gtk.Orientation.VERTICAL;
     }
 
-    get nameLabel() {
-        return this._nameLabel;
-    }
-
-    get dateLabel() {
-        return this._dateLabel;
-    }
-
-    get image() {
-        return this._image;
-    }
-
-    get thermometer() {
-        return this._thermometer;
-    }
-
-    get nightTemperatureLabel() {
-        return this._nightTemperatureLabel;
-    }
-
-    get nightImage() {
-        return this._nightImage;
-    }
+    vfunc_root() {
+        super.vfunc_root();
 
-    get nightHumidity() {
-        return this._nightHumidity;
-    }
-
-    get nightWind() {
-        return this._nightWind;
-    }
-
-    get morningTemperatureLabel() {
-        return this._morningTemperatureLabel;
-    }
-
-    get morningImage() {
-        return this._morningImage;
-    }
-
-    get morningHumidity() {
-        return this._morningHumidity;
-    }
-
-    get morningWind() {
-        return this._morningWind;
-    }
+        const { datetime } = this;
+        const { day: dayInfo, evening: eveningInfo, night: nightInfo, morning: morningInfo, afternoon: 
afternoonInfo } = this.info;
 
-    get afternoonTemperatureLabel() {
-        return this._afternoonTemperatureLabel;
-    }
-
-    get afternoonImage() {
-        return this._afternoonImage;
-    }
-
-    get afternoonHumidity() {
-        return this._afternoonHumidity;
-    }
-
-    get afternoonWind() {
-        return this._afternoonWind;
-    }
-
-    get eveningTemperatureLabel() {
-        return this._eveningTemperatureLabel;
-    }
-
-    get eveningImage() {
-        return this._eveningImage;
-    }
-
-    get eveningHumidity() {
-        return this._eveningHumidity;
+        this._nameLabel.label = datetime.format('%a');
+        /* Translators: this is the time format for day and month name according to the current locale */
+        let dateFormat = _('%b %e');
+        this._dateLabel.label = datetime.format(dateFormat);
+
+        this._image.iconName = `${dayInfo.get_icon_name()}-small`;
+
+        this._thermometer.range = new Thermometer.TemperatureRange({ dailyLow: this.minTemp, dailyHigh: 
this.maxTemp, weeklyLow: this.weekLowestTemp, weeklyHigh: this.weekHighestTemp });
+        this._nightTemperatureLabel.label = Util.getTempString(nightInfo);
+        this._nightImage.iconName = nightInfo.get_icon_name() + '-small';
+        this._nightHumidity.label = nightInfo.get_humidity();
+        this._setWindInfo(nightInfo, this._nightWind);
+
+        this._morningTemperatureLabel.label = Util.getTempString(morningInfo);
+        this._morningImage.iconName = morningInfo.get_icon_name() + '-small';
+        this._morningHumidity.label = morningInfo.get_humidity();
+        this._setWindInfo(morningInfo, this._morningWind);
+
+        this._afternoonTemperatureLabel.label = Util.getTempString(afternoonInfo);
+        this._afternoonImage.iconName = afternoonInfo.get_icon_name() + '-small';
+        this._afternoonHumidity.label = afternoonInfo.get_humidity();
+        this._setWindInfo(afternoonInfo, this._afternoonWind);
+
+        this._eveningTemperatureLabel.label = Util.getTempString(eveningInfo);
+        this._eveningImage.iconName = eveningInfo.get_icon_name() + '-small';
+        this._eveningHumidity.label = eveningInfo.get_humidity();
+        this._setWindInfo(eveningInfo, this._eveningWind);
     }
 
-    get eveningWind() {
-        return this._eveningWind;
+    _setWindInfo(info, label) {
+        let [ok, speed, direction] = info.get_value_wind(GWeather.SpeedUnit.DEFAULT);
+        if (ok) {
+            label.label = `${speed.toFixed(1).toString()} 
${GWeather.speed_unit_to_string(GWeather.SpeedUnit.DEFAULT)}`;
+        } else {
+            /* Fall back to get_wind() */
+            label.label = info.get_wind();
+        }
     }
 });
 
diff --git a/src/app/entry.js b/src/app/entry.js
index 4531615..5380ab1 100644
--- a/src/app/entry.js
+++ b/src/app/entry.js
@@ -43,8 +43,6 @@ const LocationListModel = GObject.registerClass(
         Implements: [Gio.ListModel]
     },
     class LocationListModel extends Gtk.Widget {
-
-
         _init() {
             super._init();
 
@@ -299,7 +297,7 @@ var LocationSearchEntry = GObject.registerClass(
         }
 
 
-      
+
         _populateModel() {
             let filter = new Gtk.StringFilter();
             this._filter = filter;
@@ -350,10 +348,10 @@ var LocationSearchEntry = GObject.registerClass(
             return factory;
         }
 
-        _cleanup() {
-            if (this._listview instanceof Gtk.ListView) {
-                this._listview.set_model(null);
-            }
+        vfunc_unroot() {
+            this._listview?.set_model(null);
+
+            super.vfunc_unroot();
         }
     }
 );
diff --git a/src/app/hourlyForecast.js b/src/app/hourlyForecast.js
index 2284c7d..b2e32f5 100644
--- a/src/app/hourlyForecast.js
+++ b/src/app/hourlyForecast.js
@@ -218,8 +218,6 @@ var HourlyForecastBox = GObject.registerClass(class HourlyForecastBox extends Gt
 
         super.vfunc_snapshot(snapshot);
         cr.$dispose();
-
-        return Gdk.EVENT_PROPAGATE;
     }
 });
 
diff --git a/src/app/main.js b/src/app/main.js
index 83a4363..49f0c72 100644
--- a/src/app/main.js
+++ b/src/app/main.js
@@ -52,194 +52,188 @@ const ShellIntegrationInterface = ByteArray.toString(
     Gio.resources_lookup_data('/org/gnome/shell/ShellWeatherIntegration.xml', 0).get_data());
 
 function initEnvironment() {
-    window.getApp = function() {
+    window.getApp = function () {
         return Gio.Application.get_default();
     };
 }
 
 const Application = GObject.registerClass(
-    class WeatherApplication extends Gtk.Application {
+    class WeatherApplication extends Adw.Application {
 
-    _init() {
-        super._init({
-            application_id: pkg.name,
-            flags: (Gio.ApplicationFlags.CAN_OVERRIDE_APP_ID | Gio.ApplicationFlags.FLAGS_NONE)
-        });
-        let name_prefix = '';
-        if (pkg.name.endsWith('Devel')) {
-            name_prefix = '(Development) ';
+        _init() {
+            super._init({
+                application_id: pkg.name,
+                flags: Gio.ApplicationFlags.CAN_OVERRIDE_APP_ID,
+            });
+            let name_prefix = '';
+            if (pkg.name.endsWith('Devel')) {
+                name_prefix = '(Development) ';
+            }
+            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);
-    }
 
-    _onQuit() {
-        this.quit();
-    }
-
-    _onShowLocation(action, parameter) {
-        let location = this.world.deserialize(parameter.deep_unpack());
-        let win = this._createWindow();
+        _onQuit() {
+            this.quit();
+        }
 
-        let info = this.model.addNewLocation(location, false);
-        win.showInfo(info, false);
-        this._showWindowWhenReady(win);
-    }
+        _onShowLocation(action, parameter) {
+            let location = this.world.deserialize(parameter.deep_unpack());
+            let win = this._createWindow();
 
-    _onShowSearch(action, parameter) {
-        let text = parameter.deep_unpack();
-        let win = this._createWindow();
+            let info = this.model.addNewLocation(location, false);
+            win.showInfo(info, false);
+            this._showWindowWhenReady(win);
+        }
 
-        win.showSearch(text);
-        this._showWindowWhenReady(win);
-    }
+        _onShowSearch(action, parameter) {
+            let text = parameter.deep_unpack();
+            let win = this._createWindow();
 
-    vfunc_startup() {
-        super.vfunc_startup();
-        Adw.init();
+            win.showSearch(text);
+            this._showWindowWhenReady(win);
+        }
 
-        Util.loadStyleSheet('/org/gnome/Weather/application.css');
+        vfunc_startup() {
+            super.vfunc_startup();
 
-        // TODO
-        // Handy.StyleManager
-        //     .get_default()
-        //     .set_color_scheme(Handy.ColorScheme.PREFER_LIGHT);
+            Util.loadStyleSheet('resource:///org/gnome/Weather/application.css');
 
-        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', () => {
+            this.model.connect('notify::loading', () => {
+                if (this.model.loading)
+                    this.mark_busy();
+                else
+                    this.unmark_busy();
+            });
             if (this.model.loading)
                 this.mark_busy();
-            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.GWeather' });
-        // 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(action, parameter) {
-            action.change_state(parameter);
-        })
-        temperatureAction.connect('change-state', function(action, state) {
-            gwSettings.set_value('temperature-unit', state);
-        });
-        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"]);
-    }
+            let quitAction = new Gio.SimpleAction({
+                enabled: true,
+                name: 'quit'
+            });
+            quitAction.connect('activate', () => this._onQuit());
+            this.add_action(quitAction);
 
-    vfunc_dbus_register(conn, path) {
-        this._shellIntegration = new ShellIntegration();
-        this._shellIntegration.export(conn, path);
-        return true;
-    }
+            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.GWeather' });
+            // 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 (action, parameter) {
+                action.change_state(parameter);
+            })
+            temperatureAction.connect('change-state', function (action, state) {
+                gwSettings.set_value('temperature-unit', state);
+            });
+            gwSettings.connect('changed::temperature-unit', function () {
+                temperatureAction.state = 
resolveDefaultTemperatureUnit(gwSettings.get_enum('temperature-unit'));
+            });
+            this.add_action(temperatureAction);
 
-    vfunc_dbus_unregister(conn, path) {
-        this._shellIntegration.unexport(conn);
-    }
+            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"]);
+        }
 
-    _createWindow() {
-        return new Window.MainWindow({ application: this });
-    }
+        vfunc_dbus_register(conn, path) {
+            this._shellIntegration = new ShellIntegration();
+            this._shellIntegration.export(conn, path);
+            return true;
+        }
 
-    _showWindowWhenReady(win) {
-        let notifyId;
+        vfunc_dbus_unregister(conn, path) {
+            this._shellIntegration.unexport(conn);
+        }
 
-        if (this.model.loading) {
-            let timeoutId;
-            let model = this.model;
+        _createWindow() {
+            return new Window.MainWindow({ application: this });
+        }
 
-            timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1000, function() {
-                log('Timeout during model load, perhaps the network is not available?');
-                model.disconnect(notifyId);
+        _showWindowWhenReady(win) {
+            let notifyId;
+
+            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);
+                    win.show();
+                    return false;
+                });
+                notifyId = this.model.connect('notify::loading', function (model) {
+                    if (model.loading)
+                        return;
+
+                    model.disconnect(notifyId);
+                    GLib.source_remove(timeoutId);
+                    win.show();
+                });
+            } else {
                 win.show();
-                return false;
-            });
-            notifyId = this.model.connect('notify::loading', function(model) {
-                if (model.loading)
-                    return;
+            }
 
-                model.disconnect(notifyId);
-                GLib.source_remove(timeoutId);
-                win.show();
-            });
-        } else {
-            win.show();
+            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();
 
-        super.vfunc_shutdown();
-    }
-});
+            super.vfunc_shutdown();
+        }
+    });
 
 let ShellIntegration = class ShellIntegration {
     constructor() {
@@ -278,7 +272,7 @@ let ShellIntegration = class ShellIntegration {
 function main(argv) {
     initEnvironment();
 
-    const application = new Application();
+    let application = new Application();
 
     application.connect("window-removed", (_, window) => {
         if (window instanceof Window.MainWindow) {
@@ -287,4 +281,6 @@ function main(argv) {
     });
 
     application.run(argv);
+
+    application = null;
 }
diff --git a/src/app/thermometer.js b/src/app/thermometer.js
index 59a531c..ba5f4b6 100644
--- a/src/app/thermometer.js
+++ b/src/app/thermometer.js
@@ -1,6 +1,7 @@
 /* thermometer.js
  *
  * Copyright 2021 Vitaly Dyachkov <obyknovenius me com>
+ * Copyright 2022 Evan Welsh <contact evanwelsh com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -19,172 +20,176 @@
  */
 
 const GObject = imports.gi.GObject;
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
 const Gdk = imports.gi.Gdk;
 const Gtk = imports.gi.Gtk;
-const Gsk = imports.gi.Gsk;
-const Pango = imports.gi.Pango;
-const Cairo = imports.cairo;
-const Graphene = imports.gi.Graphene;
+
+const Util = imports.misc.util;
+
+var TemperatureRange = class TemperatureRange {
+  dailyLow;
+  dailyHigh;
+  weeklyLow;
+  weeklyHigh;
+
+  constructor({ dailyLow, dailyHigh, weeklyLow, weeklyHigh }) {
+    this.dailyLow = dailyLow;
+    this.dailyHigh = dailyHigh;
+    this.weeklyLow = weeklyLow;
+    this.weeklyHigh = weeklyHigh;
+  }
+}
+
+const ThermometerScaleInternal = GObject.registerClass(class extends Gtk.Widget {
+  _init() {
+    super._init({
+      vexpand: true,
+      hexpand: true,
+      cssClasses: ['inner'],
+    });
+  }
+});
 
 const ThermometerScale = GObject.registerClass({
   CssName: 'WeatherThermometerScale',
   Properties: {
-    'adjustment': GObject.ParamSpec.object(
-      'adjustment',
-      'Adjustment',
-      'The GtkAdjustment that contains the current value of this thermometer object',
+    'range': GObject.ParamSpec.jsobject(
+      'range',
+      'range',
+      'The TemperatureRange instance representing this thermometer scale',
       GObject.ParamFlags.READWRITE,
-      Gtk.Adjustment,
     ),
   },
 }, class ThermometerScale extends Gtk.Widget {
 
-  _init({ adjustment, radius = 12, margin = 12, ...params }) {
-    super._init(params);
-
-    this._adjustment = adjustment;
-    this._adjustment.connect('changed', () => {
-      this.queue_draw();
+  _init({ range = null, ...params }) {
+    super._init({
+      vexpand: true,
+      halign: Gtk.Align.CENTER,
+      ...params
     });
 
-    this.vexpand = true;
+    this.range = range;
 
-    this._radius = radius;
-    this._margin = margin;
+    this.inner = new ThermometerScaleInternal();
   }
 
-  vfunc_measure(orientation /*, for_size */) {
-    let minimum = this._radius + this._margin, minimum_baseline = -1, natural_baseline = - 1;
-
-    if (orientation === Gtk.Orientation.HORIZONTAL) {
-      return [minimum, minimum, minimum_baseline, natural_baseline];
-    } else {
-      return [minimum, minimum, minimum_baseline, natural_baseline];
-    }
+  vfunc_root() {
+    this.inner.set_parent(this);
   }
 
-  vfunc_snapshot(snapshot) {
-    if (!this._adjustment)
-      return super.vfunc_snapshot(snapshot);
+  vfunc_unroot() {
+    this.inner.unparent();
+  }
 
-    const allocation = this.get_allocation();
+  vfunc_map() {
+    super.vfunc_map();
 
-    const rect = new Graphene.Rect();
-    rect.init(0, 0, allocation.width, allocation.height);
+    this._rangeChangedId = this.connect('notify::range', () => {
+      this.queue_draw();
+    });
+  }
 
-    let cr = snapshot.append_cairo(rect);
-    const lower = this._adjustment.get_lower();
-    const upper = this._adjustment.get_upper();
-    const value = this._adjustment.get_value();
-    const pageSize = this._adjustment.get_page_size();
+  vfunc_unmap() {
+    this.disconnect(this._rangeChangedId);
 
-    const width = this.get_allocated_width();
-    const height = this.get_allocated_height();
+    super.vfunc_unmap();
+  }
 
-    const radius = this._radius;
-    const margin = this._margin;
+  vfunc_size_allocate(width, height, baseline) {
+    super.vfunc_size_allocate(width, height, baseline);
 
-    const maxScaleHeight = height - 2 * radius - 2 * margin;
+    if (!this.range) return;
 
-    const factor = maxScaleHeight / (upper - lower);
-    const scaleY = radius + margin + (upper - value - pageSize) * factor;
-    const scaleHeight = pageSize * factor;
+    const { dailyHigh, dailyLow, weeklyHigh, weeklyLow } = this.range;
 
-    if (maxScaleHeight > 0) {
-      this._renderScale(cr, width / 2 - radius, scaleY, radius, scaleHeight);
-    }
+    const { top, bottom, left, right } = this.get_style_context().get_padding();
 
-    super.vfunc_snapshot(snapshot);
+    const temperatureRange = weeklyHigh - weeklyLow;
+    const yScale = (height - top - bottom) / temperatureRange;
 
-    cr.$dispose();
-  }
+    const innerWidth = width - left - right;
+    const innerHeight = yScale * (dailyHigh - dailyLow) - bottom;
 
-  _renderScale(cr, x, y, radius, height) {
-    const gradient = this._createGradient(y - radius, y + height + radius);
-    cr.setSource(gradient);
+    const x = left;
+    const y = yScale * (weeklyHigh - dailyHigh) - top;
 
-    cr.newSubPath();
-    cr.arc(x + radius, y, radius, Math.PI, 0);
-    cr.arc(x + radius, y + height, radius, 0, Math.PI);
-    cr.closePath();
-    cr.fill();
+    this.inner.size_allocate(new Gdk.Rectangle({
+      x,
+      y,
+      width: innerWidth,
+      height: innerHeight
+    }), -1);
   }
 
-  _createGradient(start, end) {
-    const pattern = new Cairo.LinearGradient(0, start, 0, end);
-
-    const styleContext = this.get_style_context();
-
-    const [, warmColor] = styleContext.lookup_color('thermometer_warm_color');
-    pattern.addColorStopRGB(0.0, warmColor.red, warmColor.green, warmColor.blue);
+  vfunc_measure(orientation, for_size) {
+    let minimum_baseline = -1, natural_baseline = - 1;
 
-    const [, coldColor] = styleContext.lookup_color('thermometer_cold_color');
-    pattern.addColorStopRGB(1.0, coldColor.red, coldColor.green, coldColor.blue);
+    this.inner.measure(orientation, for_size);
 
-    return pattern;
+    if (orientation === Gtk.Orientation.HORIZONTAL) {
+      return [24, 24, minimum_baseline, natural_baseline];
+    } else {
+      return [36, 48, minimum_baseline, natural_baseline];
+    }
   }
-})
+});
 
-var Thermometer = GObject.registerClass({
+// This will be import.meta.url when converted to ESM.
+const url = Gio.File.parse_name(imports.app.entry.__file__).get_uri();
 
+var Thermometer = GObject.registerClass({
   CssName: 'WeatherThermometer',
+  Template: GLib.Uri.resolve_relative(url, './thermometer.ui', 0),
+  InternalChildren: ['scale', 'highLabel', 'lowLabel'],
+  Properties: {
+    'range': GObject.ParamSpec.jsobject(
+      'range',
+      'range',
+      'The TemperatureRange instance representing this thermometer scale',
+      GObject.ParamFlags.READWRITE,
+    ),
+  },
 }, class Thermometer extends Gtk.Widget {
-
   _init({ ...params }) {
     super._init(params);
 
-    this._adjustment = Gtk.Adjustment.new(
-      0,
-      0,
-      0,
-      0,
-      0,
-      0
-    );
-
     this.layoutManager.orientation = Gtk.Orientation.VERTICAL;
 
-    this._scale = new ThermometerScale({ adjustment: this._adjustment });
-    this._highLabel = new Gtk.Label();
-    this._highLabel.add_css_class('high');
-    this._lowLabel = new Gtk.Label();
-    this._lowLabel.add_css_class('low');
+    this._scale = this.get_template_child(Thermometer, 'scale');
+    this._highLabel = this.get_template_child(Thermometer, 'highLabel');
+    this._lowLabel = this.get_template_child(Thermometer, 'lowLabel');
+  }
 
-    this._highLabel.set_parent(this);
-    this._scale.set_parent(this);
-    this._lowLabel.set_parent(this);
+  vfunc_root() {
+    super.vfunc_root();
 
-    this._updateLabels();
+    this.bind_property('range', this._scale, 'range', GObject.BindingFlags.DEFAULT);
 
-    this._radius = 12;
-    this._margin = 12;
+    this.bind_property_full('range', this._lowLabel, 'label', GObject.BindingFlags.DEFAULT, range => {
+      return [!!range, Util.formatTemperature(range?.dailyLow) ?? ''];
+    }, null);
 
-  }
+    this.bind_property_full('range', this._highLabel, 'label', GObject.BindingFlags.DEFAULT, range => {
+      return [!!range, Util.formatTemperature(range?.dailyHigh) ?? ''];
+    }, null);
 
-  setTemperatureRange(weekLowestTemp, weekHighestTemp, minTemp, maxTemp) {
-    this._adjustment.configure(
-      minTemp,
-      weekLowestTemp,
-      weekHighestTemp,
-      0, 0,
-      maxTemp - minTemp
-    );
+    // Expression version
+    // const highExpression = new Gtk.ClosureExpression(String, thermometer => {
+    //   return Util.formatTemperature(thermometer.range?.dailyHigh) ?? '';
+    // }, [new Gtk.PropertyExpression(this, null, 'range')]);
 
-    this._updateLabels();
+    // const lowExpression = new Gtk.ClosureExpression(String, thermometer => {
+    //   return Util.formatTemperature(thermometer.range?.dailyLow) ?? '';
+    // }, [new Gtk.PropertyExpression(this, null, 'range')]);
 
+    // highExpression.bind(this._highLabel, 'label', this);
+    // lowExpression.bind(this._lowLabel, 'label', this);
   }
 
-  _updateLabels() {
-    if (!this._adjustment) return;
-
-    const value = this._adjustment.get_value();
-    const pageSize = this._adjustment.get_page_size();
-
-    const highLabel = Math.round(value + pageSize) + "°";
-    this._highLabel.label = highLabel;
-
-    const lowLabel = Math.round(value) + "°";
-    this._lowLabel.label = lowLabel;
+  vfunc_unroot() {
+    super.vfunc_unroot();
   }
 });
 
diff --git a/src/app/thermometer.ui b/src/app/thermometer.ui
new file mode 100644
index 0000000..69cd6e0
--- /dev/null
+++ b/src/app/thermometer.ui
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+    <requires lib="gtk" version="4.0"/>
+    <template class="Gjs_Thermometer">
+        <child>
+            <object class="GtkLabel" id="highLabel">
+                <property name="css-classes">high</property>
+            </object>
+        </child>
+        <child>
+            <object class="Gjs_ThermometerScale" id="scale">
+            </object>
+        </child>
+        <child>
+            <object class="GtkLabel" id="lowLabel">
+                <property name="css-classes">low</property>
+            </object>
+        </child>
+    </template>
+</interface>
\ No newline at end of file
diff --git a/src/app/window.js b/src/app/window.js
index 111899e..21ba1dc 100644
--- a/src/app/window.js
+++ b/src/app/window.js
@@ -36,8 +36,8 @@ const Page = {
 var MainWindow = GObject.registerClass({
     Template: 'resource:///org/gnome/Weather/window.ui',
     InternalChildren: ['header', 'refreshRevealer', 'refresh', 'forecastStackSwitcher', 'stack',
-        'titleStack','searchEntry', 'searchView', 'forecastStackSwitcherBar']
-}, class MainWindow extends Adw.ApplicationWindow {    
+        'titleStack', 'searchEntry', 'searchView', 'forecastStackSwitcherBar']
+}, class MainWindow extends Adw.ApplicationWindow {
     _init(params) {
         super._init(params);
 
@@ -53,13 +53,6 @@ var MainWindow = GObject.registerClass({
         aboutAction.connect('activate', () => this._showAbout());
         this.add_action(aboutAction);
 
-        let closeAction = new Gio.SimpleAction({
-            enabled: true,
-            name: 'close'
-        });
-        closeAction.connect('activate', () => this._close());
-        this.add_action(closeAction);
-
         let refreshAction = new Gio.SimpleAction({
             enabled: true,
             name: 'refresh'
@@ -79,11 +72,11 @@ var MainWindow = GObject.registerClass({
 
         this._cityView = new City.WeatherView(this.application, this,
             { hexpand: true, vexpand: true });
-        this._stack.add_named(this._cityView, 'city');
 
-        this._forecastStackSwitcher.set_stack(this._cityView.getInfoPage().getForecastStack());
+        this._stack.add_named(this._cityView, 'city');
 
-        this._forecastStackSwitcherBar.set_stack(this._cityView.getInfoPage().getForecastStack());
+        this._forecastStackSwitcher.stack = this._cityView.getForecastStack();
+        this._forecastStackSwitcherBar.stack = this._cityView.getForecastStack();
 
         this._stack.set_visible_child(this._searchView);
 
@@ -99,9 +92,11 @@ var MainWindow = GObject.registerClass({
         // this.show_all();
     }
 
-    on_destroy() {
-        this._cityView._cleanup();
-        this._searchEntry._cleanup();
+    vfunc_unroot() {
+        this._cityView.unparent();
+        this._cityView = null;
+
+        super.vfunc_unroot();
     }
 
     update() {
@@ -225,8 +220,4 @@ var MainWindow = GObject.registerClass({
 
         aboutDialog.show();
     }
-
-    _close() {
-        this.destroy();
-    }
 });
diff --git a/src/misc/util.js b/src/misc/util.js
index e3ffadc..c15ff20 100644
--- a/src/misc/util.js
+++ b/src/misc/util.js
@@ -46,7 +46,7 @@ function loadUI(resourcePath, objects) {
 
 function loadStyleSheet(resource) {
     let provider = new Gtk.CssProvider();
-    provider.load_from_file(Gio.File.new_for_uri('resource://' + resource));
+    provider.load_from_file(Gio.File.new_for_uri(resource));
     Gtk.StyleContext.add_provider_for_display(Gdk.Display.get_default(),
                                              provider,
                                              Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
@@ -195,11 +195,15 @@ function getTemp(info) {
     return temp;
 }
 
+function formatTemperature(value) {
+    return value ? `${Math.round(value).toFixed(0)}°` : undefined;
+};
+
 function getTempString(info) {
     let [ok, temp] = info.get_value_temp(GWeather.TemperatureUnit.DEFAULT);
     if (!ok)
         return "--";
-    return Math.round(temp) + "°";
+    return formatTemperature(temp);
 }
 
 function isDarkTheme() {
diff --git a/src/org.gnome.Weather.src.gresource.xml.in b/src/org.gnome.Weather.src.gresource.xml.in
index 220a28b..6bea654 100644
--- a/src/org.gnome.Weather.src.gresource.xml.in
+++ b/src/org.gnome.Weather.src.gresource.xml.in
@@ -5,6 +5,7 @@
     <file>app/currentLocationController.js</file>
     <file>app/hourlyForecast.js</file>
     <file>app/thermometer.js</file>
+    <file>app/thermometer.ui</file>
     <file>app/dailyForecast.js</file>
     <file>app/entry.js</file>
     <file>app/main.js</file>


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