[gnome-weather] Add a world view



commit fcebb9795d34046c4ba3ba7dcf0ef6dfff6ec706
Author: Giovanni Campagna <gcampagna src gnome org>
Date:   Sat Feb 23 17:16:51 2013 +0100

    Add a world view
    
    Following the GNOME 3 style of big icon views, add a world view that
    shows the current conditions of multiple cities at once.
    Clicking on each city allows to see detailed conditions and forecasts.

 configure.ac                                   |    2 +-
 data/Makefile.am                               |   12 ++-
 data/application.css                           |    4 +
 data/org.gnome.Weather.Application.gschema.xml |   12 ++
 src/Makefile.am                                |    3 +-
 src/{view.js => city.js}                       |   37 ++++--
 src/forecast.js                                |    9 +--
 src/main.js                                    |    7 +-
 src/util.js                                    |   41 ++++++
 src/window.js                                  |  154 ++++++++++++++++-------
 src/world.js                                   |  161 ++++++++++++++++++++++++
 11 files changed, 374 insertions(+), 68 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 5d871e4..d9a7798 100644
--- a/configure.ac
+++ b/configure.ac
@@ -18,7 +18,7 @@ AC_PROG_CC
 AM_PROG_CC_C_O
 LT_INIT([disable-static])
 
-LIBGD_INIT([header-bar stack revealer gir])
+LIBGD_INIT([header-bar main-toolbar main-view stack revealer gir])
 
 PKG_PROG_PKG_CONFIG([0.22])
 
diff --git a/data/Makefile.am b/data/Makefile.am
index 326b97c..56f6681 100644
--- a/data/Makefile.am
+++ b/data/Makefile.am
@@ -6,7 +6,15 @@ apps_DATA = gnome-weather.desktop
 
 @INTLTOOL_DESKTOP_RULE@
 
-EXTRA_DIST = gnome-weather.desktop.in
-CLEANFILES = $(apps_DATA)
+gsettings_SCHEMAS = org.gnome.Weather.Application.gschema.xml
+
+ GSETTINGS_RULES@
+
+EXTRA_DIST = gnome-weather.desktop.in $(gsettings_SCHEMAS)
+CLEANFILES = $(apps_DATA) *.valid gschemas.compiled
+
+# For uninstalled use
+all-local:
+       $(GLIB_COMPILE_SCHEMAS) $(builddir)
 
 include $(top_srcdir)/git.mk
diff --git a/data/application.css b/data/application.css
index bf3cc03..d6a4144 100644
--- a/data/application.css
+++ b/data/application.css
@@ -21,3 +21,7 @@
 #conditions-image {
     padding-right: 12px;
 }
+
+.content-view.cell {
+    font-weight: bold;
+}
\ No newline at end of file
diff --git a/data/org.gnome.Weather.Application.gschema.xml b/data/org.gnome.Weather.Application.gschema.xml
new file mode 100644
index 0000000..dc36225
--- /dev/null
+++ b/data/org.gnome.Weather.Application.gschema.xml
@@ -0,0 +1,12 @@
+<schemalist gettext-domain="gnome-weather">
+  <schema id="org.gnome.Weather.Application" path="/org/gnome/Weather/Application/">
+    <key name="locations" type="av">
+      <default>[]</default>
+      <summary>Configured cities to show weather for</summary>
+      <description>
+        The locations shown in the world view of gnome-weather. Each value is a
+        GVariant returned by gweather_location_serialize().
+      </description>
+    </key>
+  </schema>
+</schemalist>
diff --git a/src/Makefile.am b/src/Makefile.am
index f9b0641..329cb5a 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -4,12 +4,13 @@ nodist_bin_SCRIPTS = gnome-weather
 
 jsdir = $(pkgdatadir)
 dist_js_DATA = \
+       city.js         \
        forecast.js     \
        main.js         \
        strings.js      \
        util.js         \
-       view.js         \
        window.js       \
+       world.js        \
        $(NULL)
 
 gnome-weather: $(top_builddir)/config.status gnome-weather.in
diff --git a/src/view.js b/src/city.js
similarity index 90%
rename from src/view.js
rename to src/city.js
index 086e568..cf239f1 100644
--- a/src/view.js
+++ b/src/city.js
@@ -95,10 +95,7 @@ const WeatherWidget = new Lang.Class({
     },
 
     update: function(info) {
-        let conditions = info.get_conditions();
-        if (conditions == '-') // Not significant
-            conditions = info.get_sky();
-        this._conditions.label = conditions;
+        this._conditions.label = Util.getWeatherConditions(info);
         this._temperature.label = info.get_temp_summary();
 
         let attr = info.get_attribution();
@@ -132,7 +129,6 @@ const WeatherView = new Lang.Class({
     Extends: Gd.Stack,
 
     _init: function(params) {
-        let filtered = Params.filter(params, { info: null });
         this.parent(params);
 
         let loadingPage = new Gtk.Grid({ orientation: Gtk.Orientation.VERTICAL,
@@ -149,9 +145,30 @@ const WeatherView = new Lang.Class({
         this._infoPage = new WeatherWidget();
         this.add_named(this._infoPage, 'info');
 
-        this._info = filtered.info;
-        this._updateId = this._info.connect('updated',
-                                            Lang.bind(this, this._onUpdate));
+        this._info = null;
+        this._updateId = 0;
+    },
+
+    get info() {
+        return this._info;
+    },
+
+    set info(info) {
+        if (this._updateId) {
+            this._info.disconnect(this._updateId);
+            this._updateId = 0;
+
+            this._infoPage.clear();
+        }
+
+        this._info = info;
+
+        if (info) {
+            this._updateId = this._info.connect('updated',
+                                                Lang.bind(this, this._onUpdate));
+            if (info.is_valid())
+                this._onUpdate(info);
+        }
     },
 
     vfunc_destroy: function() {
@@ -163,10 +180,12 @@ const WeatherView = new Lang.Class({
         this.parent();
     },
 
-    beginUpdate: function() {
+    update: function() {
         this.visible_child_name = 'loading';
         this._spinner.start();
         this._infoPage.clear();
+
+        this._info.update();
     },
 
     _onUpdate: function(info) {
diff --git a/src/forecast.js b/src/forecast.js
index d0df482..e984e97 100644
--- a/src/forecast.js
+++ b/src/forecast.js
@@ -252,20 +252,13 @@ const TodaySidebar = new Lang.Class({
         this._grid.attach(image, 1, row, 1, 1);
         this._infoWidgets.push(image);
 
-        let conditions = new Gtk.Label({ label: this._getConditions(info),
+        let conditions = new Gtk.Label({ label: Util.getWeatherConditions(info),
                                          visible: true,
                                          xalign: 0.0 });
         this._grid.attach(conditions, 2, row, 1, 1);
         this._infoWidgets.push(conditions);
     },
 
-    _getConditions: function(info) {
-        let conditions = info.get_conditions();
-        if (conditions == '-') // Not significant
-            conditions = info.get_sky();
-        return conditions;
-    },
-
     _showMore: function() {
         if (!this._hasMore) {
             log('_showMore called when _hasMore is false, this should not happen');
diff --git a/src/main.js b/src/main.js
index 224cb05..4662923 100644
--- a/src/main.js
+++ b/src/main.js
@@ -21,13 +21,16 @@ pkg.initGettext();
 pkg.initFormat();
 pkg.require({ 'Gd': '1.0',
               'Gdk': '3.0',
+              'GdkPixbuf': '2.0',
+              'Gio': '2.0',
               'GLib': '2.0',
               'GObject': '2.0',
               'Gtk': '3.0',
               'GWeather': '3.0',
               'Lang': '1.0',
               'Mainloop': '1.0',
-              'Params': '1.0' });
+              'Params': '1.0',
+              'System': '1.0' });
 
 const Util = imports.util;
 const Window = imports.window;
@@ -47,7 +50,7 @@ const Application = new Lang.Class({
         Util.loadStyleSheet();
 
         let settings = Gtk.Settings.get_for_screen(Gdk.Screen.get_default());
-        settings.gtk_application_prefer_dark_theme = true;
+        settings.gtk_application_prefer_dark_theme = false;
 
         this.world = GWeather.Location.new_world(false);
     },
diff --git a/src/util.js b/src/util.js
index 7c9ca4d..6bc3c26 100644
--- a/src/util.js
+++ b/src/util.js
@@ -54,3 +54,44 @@ function arrayEqual(one, two) {
 
     return true;
 }
+
+function getSettings(schemaId, path) {
+    const GioSSS = Gio.SettingsSchemaSource;
+    let schemaSource;
+
+    if (pkg.moduledir != pkg.pkgdatadir) {
+        // Running from the source tree
+        schemaSource = GioSSS.new_from_directory(pkg.pkgdatadir,
+                                                 GioSSS.get_default(),
+                                                 false);
+    } else {
+        schemaSource = GioSSS.get_default();
+    }
+
+    let schemaObj = schemaSource.lookup(schemaId, true);
+    if (!schemaObj) {
+        log('Missing GSettings schema ' + schemaId);
+        System.exit(1);
+    }
+
+    if (path === undefined)
+        return new Gio.Settings({ settings_schema: schemaObj });
+    else
+        return new Gio.Settings({ settings_schema: schemaObj,
+                                  path: path });
+}
+
+function loadIcon(iconName, size) {
+    let theme = Gtk.IconTheme.get_default();
+
+    return theme.load_icon(iconName,
+                           size,
+                           Gtk.IconLookupFlags.GENERIC_FALLBACK);
+}
+
+function getWeatherConditions(info) {
+    let conditions = info.get_conditions();
+    if (conditions == '-') // Not significant
+        conditions = info.get_sky();
+    return conditions;
+}
diff --git a/src/window.js b/src/window.js
index 62de72e..aeb7a9c 100644
--- a/src/window.js
+++ b/src/window.js
@@ -16,7 +16,8 @@
 // with Gnome Weather; if not, write to the Free Software Foundation,
 // Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 
-const View = imports.view;
+const City = imports.city;
+const World = imports.world;
 
 function makeTitle(location) {
     let city = location;
@@ -34,70 +35,74 @@ function makeTitle(location) {
         return city.get_name();
 }
 
+const Page = {
+    WORLD: 0,
+    CITY: 1
+};
+
 const MainWindow = new Lang.Class({
     Name: 'MainWindow',
     Extends: Gtk.ApplicationWindow,
-    Properties: {
-        'location': GObject.ParamSpec.boxed('location', 'Location', '',
-                                            GObject.ParamFlags.READABLE |
-                                            GObject.ParamFlags.WRITABLE,
-                                            GWeather.Location),
-    },
 
     _init: function(params) {
-        params = Params.fill(params, { default_width: 700,
-                                       default_height: 500 });
+        params = Params.fill(params, { width_request: 700,
+                                       height_request: 520 });
         this.parent(params);
 
         this._world = this.application.world;
-        this._info = new GWeather.Info({ world: this._world,
-                                         forecast_type: GWeather.ForecastType.LIST,
-                                         enabled_providers: GWeather.Provider.METAR |
-                                                            GWeather.Provider.YR_NO });
-        this._location = this._info.get_location();
+        this._model = new World.WorldModel(this._world);
+        this._currentInfo = null;
+        this._currentPage = Page.WORLD;
+        this._pageWidgets = [[],[]];
 
         let grid = new Gtk.Grid({ orientation: Gtk.Orientation.VERTICAL });
 
-        this._header = new Gd.HeaderBar({ title: makeTitle(this._location),
-                                          hexpand: true });
+        this._header = new Gd.HeaderBar({ hexpand: true });
         grid.add(this._header);
 
-        this._search = new Gd.HeaderToggleButton({ symbolic_icon_name: 'edit-find-symbolic' });
-        this._header.pack_end(this._search);
+        let newButton = new Gd.HeaderSimpleButton({ label: _("New") });
+        newButton.connect('clicked', Lang.bind(this, this._newLocation));
+        this._header.pack_start(newButton);
+        this._pageWidgets[Page.WORLD].push(newButton);
+
+        let goWorldButton = new Gd.HeaderSimpleButton({ label: _("World Weather") });
+        goWorldButton.connect('clicked', Lang.bind(this, this._goWorld));
+        this._header.pack_start(goWorldButton);
+        this._pageWidgets[Page.CITY].push(goWorldButton);
 
         let refresh = new Gd.HeaderSimpleButton({ symbolic_icon_name: 'view-refresh-symbolic' });
         refresh.connect('clicked', Lang.bind(this, this.update));
         this._header.pack_end(refresh);
+        this._pageWidgets[Page.CITY].push(refresh);
+
+        let select = new Gd.HeaderToggleButton({ symbolic_icon_name: 'object-select-symbolic' });
+        this._header.pack_end(select);
+        this._pageWidgets[Page.WORLD].push(select);
+
+        this._stack = new Gd.Stack();
+
+        this._cityView = new City.WeatherView({ hexpand: true,
+                                                vexpand: true });
+        this._stack.add(this._cityView);
+
+        this._worldView = new Gd.MainView({ view_type: Gd.MainViewType.ICON });
+        this._worldView.model = this._model;
+        this._worldView.connect('item-activated', Lang.bind(this, this._itemActivated));
+        this._worldView.connect('selection-mode-request', function() {
+            select.active = true;
+        });
+        select.bind_property('active', this._worldView, 'selection-mode',
+                             GObject.BindingFlags.DEFAULT);
+        this._stack.add(this._worldView);
 
-        this._locationEntry = new GWeather.LocationEntry({ top: this._world,
-                                                           location: this._location,
-                                                           width_request: 500,
-                                                           halign: Gtk.Align.CENTER });
-        this._locationEntry.bind_property('location', this, 'location',
-                                          GObject.BindingFlags.DEFAULT);
-
-        let toolbar = new Gtk.Toolbar({ hexpand: true });
-        toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_PRIMARY_TOOLBAR);
-        let item = new Gtk.ToolItem();
-        item.set_expand(true);
-        item.add(this._locationEntry);
-        toolbar.insert(item, 0);
-
-        let revealer = new Gd.Revealer({ reveal_child: false,
-                                         child: toolbar });
-        this._search.bind_property('active', revealer, 'reveal-child',
-                                   GObject.BindingFlags.DEFAULT);
-        grid.add(revealer);
-
-        this._view = new View.WeatherView({ info: this._info,
-                                            hexpand: true,
-                                            vexpand: true });
-        grid.add(this._view);
+        this._stack.set_visible_child(this._worldView);
+        grid.add(this._stack);
 
         this.add(grid);
         grid.show_all();
 
-        this._view.beginUpdate();
+        for (let i = 0; i < this._pageWidgets[Page.CITY].length; i++)
+            this._pageWidgets[Page.CITY][i].hide();
     },
 
     get location() {
@@ -114,7 +119,66 @@ const MainWindow = new Lang.Class({
     },
 
     update: function() {
-        this._view.beginUpdate();
-        this._info.update();
+        this._cityView.update();
+    },
+
+    _getTitle: function() {
+        if (this._currentPage == Page.WORLD)
+            return '';
+
+        return makeTitle(this._cityView.info.location);
+    },
+
+    _goToPage: function(page) {
+        if (page == this._currentPage)
+            return;
+
+        for (let i = 0; i < this._pageWidgets[this._currentPage].length; i++)
+            this._pageWidgets[this._currentPage][i].hide();
+
+        for (let i = 0; i < this._pageWidgets[page].length; i++)
+            this._pageWidgets[page][i].show();
+
+        this._currentPage = page;
+        this._header.title = this._getTitle();
+    },
+
+    _itemActivated: function(view, id, path) {
+        let [ok, iter] = this._model.get_iter(path);
+        let info = this._model.get_value(iter, World.Columns.INFO);
+
+        this._cityView.info = info;
+        this._stack.set_visible_child(this._cityView);
+        this._goToPage(Page.CITY);
+    },
+
+    _goWorld: function() {
+        this._cityView.info = null;
+        this._stack.set_visible_child(this._worldView);
+        this._goToPage(Page.WORLD);
     },
+
+    _newLocation: function() {
+        let dialog = new Gtk.Dialog({ title: _("New Location"),
+                                      transient_for: this.get_toplevel(),
+                                      modal: true });
+        let entry = new GWeather.LocationEntry({ top: this._world });
+
+        dialog.get_content_area().add(entry);
+        dialog.add_button('gtk-add', Gtk.ResponseType.OK);
+
+        dialog.connect('response', Lang.bind(this, function(dialog, response) {
+            dialog.destroy();
+
+            if (response != Gtk.ResponseType.OK)
+                return;
+
+            let location = entry.location;
+            if (!location)
+                return;
+
+            this._model.addLocation(entry.location);
+        }));
+        dialog.show_all();
+    }
 });
diff --git a/src/world.js b/src/world.js
new file mode 100644
index 0000000..d02ac24
--- /dev/null
+++ b/src/world.js
@@ -0,0 +1,161 @@
+// -*- Mode: js; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*-
+//
+// Copyright (c) 2012 Giovanni Campagna <scampa giovanni gmail com>
+//
+// Gnome Weather is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by the
+// Free Software Foundation; either version 2 of the License, or (at your
+// option) any later version.
+//
+// Gnome Weather is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+// for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with Gnome Weather; if not, write to the Free Software Foundation,
+// Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+
+const Util = imports.util;
+
+const Columns = {
+    ID: Gd.MainColumns.ID,
+    URI: Gd.MainColumns.URI,
+    PRIMARY_TEXT: Gd.MainColumns.PRIMARY_TEXT,
+    SECONDARY_TEXT: Gd.MainColumns.SECONDARY_TEXT,
+    ICON: Gd.MainColumns.ICON,
+    MTIME: Gd.MainColumns.MTIME,
+    SELECTED: Gd.MainColumns.SELECTED,
+    LOCATION: 7,
+    INFO: 8
+};
+
+const ICON_SIZE = 128;
+
+const WorldModel = new Lang.Class({
+    Name: 'WorldModel',
+    Extends: Gtk.ListStore,
+    Signals: {
+        'updated': { param_types: [ GWeather.Info ] }
+    },
+
+    _init: function(world) {
+        this.parent();
+        this.set_column_types([GObject.TYPE_STRING,
+                               GObject.TYPE_STRING,
+                               GObject.TYPE_STRING,
+                               GObject.TYPE_STRING,
+                               GdkPixbuf.Pixbuf,
+                               GObject.TYPE_INT,
+                               GObject.TYPE_BOOLEAN,
+                               GWeather.Location,
+                               GWeather.Info]);
+
+        this._world = world;
+
+        this._settings = Util.getSettings('org.gnome.Weather.Application');
+
+        let locations = this._settings.get_value('locations').deep_unpack();
+        for (let i = 0; i < locations.length; i++) {
+            let variant = locations[i];
+            let location = this._world.deserialize(variant);
+            this._addLocationInternal(location);
+        }
+
+        this._settings.connect('changed::locations', Lang.bind(this, this._onChanged));
+    },
+
+    _addLocationInternal: function(location) {
+        let info = new GWeather.Info({ world: this._world,
+                                       location: location,
+                                       forecast_type: GWeather.ForecastType.LIST,
+                                       enabled_providers: (GWeather.Provider.METAR |
+                                                           GWeather.Provider.YR_NO) });
+        let iter;
+        info.connect('updated', Lang.bind(this, function(info) {
+            let icon = Util.loadIcon(info.get_symbolic_icon_name(), ICON_SIZE);
+            let secondary_text = Util.getWeatherConditions(info);
+            this.set(iter,
+                     [Columns.ICON, Columns.SECONDARY_TEXT],
+                     [icon, secondary_text]);
+
+            this.emit('updated', info);
+        }));
+        info.update();
+
+        let primary_text = location.get_city_name();
+        let icon = Util.loadIcon('view-refresh-symbolic', ICON_SIZE);
+
+        iter = this.insert_with_valuesv(-1,
+                                        [Columns.PRIMARY_TEXT,
+                                         Columns.ICON,
+                                         Columns.LOCATION,
+                                         Columns.INFO],
+                                        [primary_text,
+                                         icon,
+                                         location,
+                                         info]);
+    },
+
+    _onChanged: function() {
+        let newLocations = this._settings.get_value('locations').deep_unpack();
+        let toErase = [];
+
+        let [ok, iter] = this.get_iter_first();
+        while (ok) {
+            let location = this.get_value(iter, Columns.LOCATION);
+
+            let found = true;
+            for (let j = 0; j < newLocations.length; j++) {
+                let variant = newLocations[j];
+                if (variant == null)
+                    continue;
+
+                let newLocation = this._world.deserialize(variant);
+
+                if (location.equal(newLocation)) {
+                    newLocations[j] = null;
+                    found = true;
+                    break;
+                }
+            }
+
+            if (!found)
+                toErase.push(iter.copy());
+
+            ok = this.iter_next(iter);
+        }
+
+        for (let i = 0; i < toErase.length; i++)
+            this.erase(toErase[i]);
+
+        for (let i = 0; i < newLocations.length; i++) {
+            let variant = newLocations[i];
+            if (variant == null)
+                continue;
+
+            let newLocation = this._world.deserialize(variant);
+            this._addLocationInternal(newLocation);
+        }
+    },
+
+    addLocation: function(location) {
+        let newLocations = this._settings.get_value('locations').deep_unpack();
+        newLocations.push(location.serialize());
+        this._settings.set_value('locations', new GLib.Variant('av', newLocations));
+    },
+
+    removeLocation: function(iter) {
+        let location = this.get_value(iter, Columns.LOCATION);
+        let variant = location.serialize();
+
+        let newLocations = this._settings.get_value('locations').deep_unpack();
+        for (let i = 0; i < newLocations.length; i++) {
+            if (newLocations[i].equal(variant)) {
+                newLocations.splice(i, 1);
+                break;
+            }
+        }
+        this._settings.set_value('locations', new GLib.Variant('av', newLocations));
+    },
+});


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