[gnome-weather] Add a shell search provider



commit 22f635fa288e86808921e0a0ea42e44fe937f1cc
Author: Giovanni Campagna <gcampagna src gnome org>
Date:   Mon Aug 19 22:03:04 2013 +0200

    Add a shell search provider
    
    Show the current weather conditions in the shell activity
    overview for the configured locations.

 data/Makefile.am                                   |    3 +
 data/ShellSearchProvider2.xml                      |   84 +++++++++
 data/org.gnome.Weather.Application.gresource.xml   |    3 +
 ...g.gnome.Weather.Application.search-provider.ini |    5 +
 src/Makefile.am                                    |    1 +
 src/main.js                                        |   24 ++-
 src/package.js                                     |    6 +
 src/searchProvider.js                              |  190 ++++++++++++++++++++
 src/util.js                                        |   26 +++
 src/window.js                                      |    4 +
 10 files changed, 341 insertions(+), 5 deletions(-)
---
diff --git a/data/Makefile.am b/data/Makefile.am
index 741620e..5f03e00 100644
--- a/data/Makefile.am
+++ b/data/Makefile.am
@@ -40,6 +40,9 @@ $(PACKAGE_NAME).desktop.in: $(PACKAGE_NAME).desktop.in.in
 servicedir = $(datadir)/dbus-1/services
 service_DATA = $(PACKAGE_NAME).service
 
+searchproviderdir = $(datadir)/gnome-shell/search-providers
+dist_searchprovider_DATA = org.gnome.Weather.Application.search-provider.ini
+
 EXTRA_DIST = \
        CREDITS \
        $(PACKAGE_NAME).desktop.in.in \
diff --git a/data/ShellSearchProvider2.xml b/data/ShellSearchProvider2.xml
new file mode 100644
index 0000000..f7ae7ce
--- /dev/null
+++ b/data/ShellSearchProvider2.xml
@@ -0,0 +1,84 @@
+<node>
+
+  <!--
+      org.gnome.Shell.SearchProvider2:
+      @short_description: Search provider interface
+
+      The interface used for integrating into GNOME Shell's search
+      interface (version 2).
+  -->
+  <interface name="org.gnome.Shell.SearchProvider2">
+
+    <!--
+        GetInitialResultSet:
+        @terms: Array of search terms, which the provider should treat as logical AND.
+        @results: An array of result identifier strings representing items which match the given search 
terms. Identifiers must be unique within the provider's domain, but other than that may be chosen freely by 
the provider.
+
+        Called when the user first begins a search.
+    -->
+    <method name="GetInitialResultSet">
+      <arg type="as" name="terms" direction="in" />
+      <arg type="as" name="results" direction="out" />
+    </method>
+
+    <!--
+        GetSubsearchResultSet:
+        @previous_results: Array of results previously returned by GetInitialResultSet().
+        @terms: Array of updated search terms, which the provider should treat as logical AND.
+        @results: An array of result identifier strings representing items which match the given search 
terms. Identifiers must be unique within the provider's domain, but other than that may be chosen freely by 
the provider.
+
+        Called when a search is performed which is a "subsearch" of
+        the previous search, e.g. the method may return less results, but
+        not more or different results.
+
+        This allows search providers to only search through the previous
+        result set, rather than possibly performing a full re-query.
+    -->
+    <method name="GetSubsearchResultSet">
+      <arg type="as" name="previous_results" direction="in" />
+      <arg type="as" name="terms" direction="in" />
+      <arg type="as" name="results" direction="out" />
+    </method>
+
+    <!--
+        GetResultMetas:
+        @identifiers: An array of result identifiers as returned by GetInitialResultSet() or 
GetSubsearchResultSet()
+        @metas: A dictionary describing the given search result, containing a human-readable 'name' 
(string), along with the result identifier this meta is for, 'id' (string). Optionally, 'icon' (a serialized 
GIcon as obtained by g_icon_serialize) can be specified if the result can be better served with a thumbnail 
of the content (such as with images). 'gicon' (a serialized GIcon as obtained by g_icon_to_string) or 
'icon-data' (raw image data as (iiibiiay) - width, height, rowstride, has-alpha, bits per sample, channels, 
data) are deprecated values that can also be used for that purpose. A 'description' field (string) may also 
be specified if more context would help the user find the desired result.
+
+        Return an array of meta data used to display each given result
+    -->
+    <method name="GetResultMetas">
+      <arg type="as" name="identifiers" direction="in" />
+      <arg type="aa{sv}" name="metas" direction="out" />
+    </method>
+
+    <!--
+        ActivateResult:
+        @identifier: A result identifier as returned by GetInitialResultSet() or GetSubsearchResultSet()
+        @terms: Array of search terms, which the provider should treat as logical AND.
+        @timestamp: A timestamp of the user interaction that triggered this call
+
+        Called when the users chooses a given result. The result should
+        be displayed in the application associated with the corresponding
+        provider. The provided search terms can be used to allow launching a full search in
+        the application.
+    -->
+    <method name="ActivateResult">
+      <arg type="s" name="identifier" direction="in" />
+      <arg type="as" name="terms" direction="in" />
+      <arg type="u" name="timestamp" direction="in" />
+    </method>
+
+    <!--
+        LaunchSearch:
+        @terms: Array of search terms, which the provider should treat as logical AND.
+        @timestamp: A timestamp of the user interaction that triggered this call
+
+        Asks the search provider to launch a full search in the application for the provided terms.
+    -->
+    <method name="LaunchSearch">
+      <arg type="as" name="terms" direction="in" />
+      <arg type="u" name="timestamp" direction="in" />
+    </method>
+  </interface>
+</node>
diff --git a/data/org.gnome.Weather.Application.gresource.xml 
b/data/org.gnome.Weather.Application.gresource.xml
index 9c7d670..35de812 100644
--- a/data/org.gnome.Weather.Application.gresource.xml
+++ b/data/org.gnome.Weather.Application.gresource.xml
@@ -16,4 +16,7 @@
     <file>weather-snow.jpg</file>
     <file>weather-storm.jpg</file>
   </gresource>
+  <gresource prefix="/org/gnome/shell">
+    <file>ShellSearchProvider2.xml</file>
+  </gresource>
 </gresources>
diff --git a/data/org.gnome.Weather.Application.search-provider.ini 
b/data/org.gnome.Weather.Application.search-provider.ini
new file mode 100644
index 0000000..0d4c42e
--- /dev/null
+++ b/data/org.gnome.Weather.Application.search-provider.ini
@@ -0,0 +1,5 @@
+[Shell Search Provider]
+DesktopId=org.gnome.Weather.Application.desktop
+BusName=org.gnome.Weather.Application
+ObjectPath=/org/gnome/Weather/Application
+Version=2
diff --git a/src/Makefile.am b/src/Makefile.am
index d6af53f..bf12cc1 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -12,6 +12,7 @@ dist_js_DATA = \
        main.js         \
        package.js      \
        params.js       \
+       searchProvider.js \
        strings.js      \
        util.js         \
        window.js       \
diff --git a/src/main.js b/src/main.js
index 8d563d0..1c53f56 100644
--- a/src/main.js
+++ b/src/main.js
@@ -19,6 +19,7 @@
 pkg.initSubmodule('libgd');
 pkg.initGettext();
 pkg.initFormat();
+pkg.initResources();
 pkg.require({ 'Gd': '1.0',
               'Gdk': '3.0',
               'GdkPixbuf': '2.0',
@@ -35,6 +36,7 @@ pkg.require({ 'Gd': '1.0',
 const Util = imports.util;
 const Window = imports.window;
 const World = imports.world;
+const SearchProvider = imports.searchProvider;
 
 const Application = new Lang.Class({
     Name: 'WeatherApplication',
@@ -42,8 +44,11 @@ const Application = new Lang.Class({
 
     _init: function() {
         this.parent({ application_id: pkg.name,
-                      flags: Gio.ApplicationFlags.IS_SERVICE });
+                      flags: Gio.ApplicationFlags.IS_SERVICE,
+                      inactivity_timeout: 60000 });
         GLib.set_application_name(_("Weather"));
+
+        this._searchProvider = new SearchProvider.SearchProvider(this);
     },
 
     _onQuit: function() {
@@ -58,14 +63,23 @@ const Application = new Lang.Class({
         this.set_app_menu(menu);
     },
 
+    vfunc_dbus_register: function(connection, path) {
+        this.parent(connection, path);
+
+        this._searchProvider.export(connection, path);
+        return true;
+    },
+
+    vfunc_dbus_unregister: function(connection) {
+        this._searchProvider.unexport(connection);
+
+        this.parent(connection);
+    },
+
     vfunc_startup: function() {
         this.parent();
         Gd.ensure_types();
 
-        let resource = Gio.Resource.load(GLib.build_filenamev([pkg.pkgdatadir,
-                                                               pkg.name + '.gresource']));
-        resource._register();
-
         Util.loadStyleSheet('/org/gnome/weather/application.css');
 
         let settings = Gtk.Settings.get_for_screen(Gdk.Screen.get_default());
diff --git a/src/package.js b/src/package.js
index 9795551..f8fab2d 100644
--- a/src/package.js
+++ b/src/package.js
@@ -294,6 +294,12 @@ function initSubmodule(name) {
     }
 }
 
+function initResources() {
+    let resource = Gio.Resource.load(GLib.build_filenamev([pkg.pkgdatadir,
+                                                           pkg.name + '.gresource']));
+    resource._register();
+}
+
 function launch(params) {
     params.flags = params.flags || 0;
     let app = new Gio.Application({ application_id: params.name,
diff --git a/src/searchProvider.js b/src/searchProvider.js
new file mode 100644
index 0000000..0834439
--- /dev/null
+++ b/src/searchProvider.js
@@ -0,0 +1,190 @@
+// -*- Mode: js; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*-
+//
+// Copyright (c) 2013 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 Window = imports.window;
+const World = imports.world;
+
+const SearchProviderInterface = Gio.resources_lookup_data('/org/gnome/shell/ShellSearchProvider2.xml', 
0).toArray().toString();
+
+function getCountryName(location) {
+    while (location &&
+           location.get_level() > GWeather.LocationLevel.COUNTRY)
+        location = location.get_parent();
+
+    return location.get_name();
+}
+
+const SearchProvider = new Lang.Class({
+    Name: 'WeatherSearchProvider',
+
+    _init: function(application) {
+        this._app = application;
+
+        this._impl = Gio.DBusExportedObject.wrapJSObject(SearchProviderInterface, this);
+    },
+
+    export: function(connection, path) {
+        return this._impl.export(connection, path);
+    },
+
+    unexport: function(connection) {
+        return this._impl.unexport_from_connection(connection);
+    },
+
+    GetInitialResultSet: function(terms) {
+        this._app.hold();
+
+        let model = this._app.model;
+        let nameRet = [];
+        let cityRet = [];
+        let countryRet = [];
+
+        let [ok, iter] = model.get_iter_first();
+        while (ok) {
+            let location = model.get_value(iter, World.Columns.LOCATION);
+
+            let name = Util.normalizeCasefoldAndUnaccent(location.get_name());
+            let city = Util.normalizeCasefoldAndUnaccent(location.get_city_name());
+            let country = Util.normalizeCasefoldAndUnaccent(getCountryName(location));
+
+            let nameMatch = false;
+            let cityMatch = false;
+            let countryMatch = false;
+            let good = true;
+            for (let i = 0; i < terms.length && good; i++) {
+                terms[i] = Util.normalizeCasefoldAndUnaccent(terms[i]);
+
+                if (name.indexOf(terms[i]) >= 0) {
+                    nameMatch = true;
+                } else if (city.indexOf(terms[i]) >= 0) {
+                    cityMatch = true;
+                } else if (country.indexOf(terms[i]) >= 0) {
+                    countryMatch = true;
+                } else {
+                    good = false;
+                }
+
+                //log ('Comparing %s against (%s, %s, %s): %s'.format(terms[i],
+                //                                                    name, city, country, good));
+            }
+
+            if (good) {
+                let path = model.get_path(iter).to_string();
+
+                if (nameMatch)
+                    nameRet.push(path);
+                else if (cityMatch)
+                    cityRet.push(path);
+                else
+                    countryRet.push(path);
+            }
+
+            ok = model.iter_next(iter);
+        }
+
+        this._app.release();
+
+        return nameRet.concat(cityRet).concat(countryRet);
+    },
+
+    GetSubsearchResultSet: function(previous, terms) {
+        this._app.hold();
+
+        let model = this._app.model;
+        let ret = [];
+
+        for (let i = 0; i < previous.length; i++) {
+            let [ok, iter] = model.get_iter_from_string(previous[i]);
+
+            if (!ok)
+                continue;
+
+            let location = model.get_value(iter, World.Columns.LOCATION);
+
+            let name = Util.normalizeCasefoldAndUnaccent(location.get_name());
+            let city = Util.normalizeCasefoldAndUnaccent(location.get_city_name());
+            let country = Util.normalizeCasefoldAndUnaccent(getCountryName(location));
+            let good = true;
+            for (let j = 0; j < terms.length && good; j++) {
+                terms[i] = Util.normalizeCasefoldAndUnaccent(terms[j]);
+
+                good = (name.indexOf(terms[i]) >= 0) ||
+                    (city.indexOf(terms[i]) >= 0) ||
+                    (country.indexOf(terms[i]) >= 0);
+
+                //log ('Comparing %s against (%s, %s, %s): %s'.format(terms[i],
+                //                                                    name, city, country, good));
+            }
+
+            if (good)
+                ret.push(previous[i]);
+        }
+
+        this._app.release();
+
+        return ret;
+    },
+
+    GetResultMetas: function(identifiers) {
+        this._app.hold();
+
+        let model = this._app.model;
+        let ret = [];
+
+        for (let i = 0; i < identifiers.length; i++) {
+            let [ok, iter] = model.get_iter_from_string(identifiers[i]);
+
+            if (!ok)
+                continue;
+
+            let location = model.get_value(iter, World.Columns.LOCATION);
+            let info = model.get_value(iter, World.Columns.INFO);
+            let name = model.get_value(iter, World.Columns.PRIMARY_TEXT);
+            let conditions = model.get_value(iter, World.Columns.SECONDARY_TEXT);
+
+            let summary = _("%s, %s").format(conditions, info.get_temp());
+            ret.push({ name: new GLib.Variant('s', name),
+                       id: new GLib.Variant('s', identifiers[i]),
+                       description: new GLib.Variant('s', summary),
+                       icon: (new Gio.ThemedIcon({ name: info.get_icon_name() })).serialize()
+                     });
+        }
+
+        this._app.release();
+
+        return ret;
+    },
+
+    ActivateResult: function(id, terms, timestamp) {
+        this._app.hold();
+
+        let model = this._app.model;
+        let [ok, iter] = model.get_iter_from_string(id);
+        if (!ok)
+            return;
+
+        let info = model.get_value(iter, World.Columns.INFO);
+        let win = new Window.MainWindow({ application: this._app });
+
+        win.showInfo(info);
+        win.present_with_time(timestamp);
+
+        this._app.release();
+    }
+});
diff --git a/src/util.js b/src/util.js
index d7642a1..eae90dd 100644
--- a/src/util.js
+++ b/src/util.js
@@ -112,3 +112,29 @@ function getWeatherConditions(info) {
         conditions = info.get_sky();
     return conditions;
 }
+
+function isCdm(c) {
+    return ((c >= 0x0300 && c <= 0x036F) ||
+        (c >= 0x1DC0 && c <= 0x1DFF)  ||
+        (c >= 0x20D0 && c <= 0x20FF)  ||
+        (c >= 0xFE20 && c <= 0xFE2F));
+}
+
+function normalizeCasefoldAndUnaccent(str) {
+    // The one and only!
+    // Travelled all over gnome, from tracker to gnome-shell to gnome-control-center,
+    // to seahorse, epiphany...
+    //
+    // Originally written by Aleksander Morgado <aleksander gnu org>
+
+    str = GLib.utf8_normalize(str, -1, GLib.NormalizeMode.NFKD);
+    str = GLib.utf8_casefold(str, -1);
+
+    /* Combining diacritical mark?
+     *  Basic range: [0x0300,0x036F]
+     *  Supplement:  [0x1DC0,0x1DFF]
+     *  For Symbols: [0x20D0,0x20FF]
+     *  Half marks:  [0xFE20,0xFE2F]
+     */
+    return str.replace(/[\u0300-\u036f]|[\u1dc0-\u1dff]|[\u20d0-\u20ff]|[\ufe20-\ufe2f]/, '');
+}
diff --git a/src/window.js b/src/window.js
index ec20df2..f35ed2b 100644
--- a/src/window.js
+++ b/src/window.js
@@ -247,6 +247,10 @@ const MainWindow = new Lang.Class({
         let [ok, iter] = view.model.get_iter(path);
         let info = view.model.get_value(iter, World.Columns.INFO);
 
+        this.showInfo(info);
+    },
+
+    showInfo: function(info) {
         this._cityView.info = info;
         this._stack.set_visible_child(this._cityView);
         this._goToPage(Page.CITY);


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