[gnome-weather/wip/egg-flow-box: 4/4] [WIP] Convert from GdMainView to EggFlowBox



commit 30309b44462c05542032b3514d7c16db71de61c2
Author: Giovanni Campagna <gcampagna src gnome org>
Date:   Wed Mar 20 19:42:35 2013 +0100

    [WIP] Convert from GdMainView to EggFlowBox
    
    EggFlowBox frees us from GtkTreeModel, so we can implement our data structures
    directly in JS. It also frees us from GtkCellRenderer, allowing custom
    complex widgets.
    
    Currently broken:
    - The GtkToggleButtons don't reflect the selection status from
      EggFlowBox
    - The size of the toggle buttons is wrong
    - Rubberbanding doesn't work
    - child-activated happens in selection-mode too
    - The view starts off with one item selected due to focus navigation
    - Items get a blue background when selected
    
    https://bugzilla.gnome.org/show_bug.cgi?id=695735

 egg-list-box  |    2 +-
 src/main.js   |    4 +-
 src/window.js |   56 +++++-----
 src/world.js  |  351 +++++++++++++++++++++++++++++++++++++++++----------------
 4 files changed, 284 insertions(+), 129 deletions(-)
---
diff --git a/egg-list-box b/egg-list-box
index 575e4e6..5d6d8bf 160000
--- a/egg-list-box
+++ b/egg-list-box
@@ -1 +1 @@
-Subproject commit 575e4e6735f804cf3e11b31b23f3d075e2268b16
+Subproject commit 5d6d8bff3416bb531f6bdd3a5c85be4cc2dc2fb4
diff --git a/src/main.js b/src/main.js
index fdc89f6..9ad303f 100644
--- a/src/main.js
+++ b/src/main.js
@@ -20,7 +20,8 @@ pkg.initSubmodule('egg-list-box');
 pkg.initSubmodule('libgd');
 pkg.initGettext();
 pkg.initFormat();
-pkg.require({ 'Gd': '1.0',
+pkg.require({ 'Egg': '1.0',
+              'Gd': '1.0',
               'Gdk': '3.0',
               'GdkPixbuf': '2.0',
               'Gio': '2.0',
@@ -31,6 +32,7 @@ pkg.require({ 'Gd': '1.0',
               'Lang': '',
               'Mainloop': '',
               'Params': '1.0',
+              'Signals': '',
               'System': '' });
 
 const Util = imports.util;
diff --git a/src/window.js b/src/window.js
index 26a128d..0c848b4 100644
--- a/src/window.js
+++ b/src/window.js
@@ -126,12 +126,12 @@ const MainWindow = new Lang.Class({
         goWorldButton.connect('clicked', Lang.bind(this, this._goWorld));
         this._pageWidgets[Page.CITY].push(goWorldButton);
 
-        let select = builder.get_object('select-button');
-        this._pageWidgets[Page.WORLD].push(select);
-
         let refresh = builder.get_object('refresh-button');
         this._pageWidgets[Page.CITY].push(refresh);
 
+        let select = builder.get_object('select-button');
+        this._pageWidgets[Page.WORLD].push(select);
+
         let selectDone = builder.get_object('done-button');
         this._pageWidgets[Page.WORLD].push(selectDone);
 
@@ -149,10 +149,16 @@ const MainWindow = new Lang.Class({
         let iconView = this._worldView.iconView;
         this._stack.add(this._worldView);
 
-        iconView.connect('item-activated', Lang.bind(this, this._itemActivated));
+        iconView.connect('child-activated', Lang.bind(this, this._childActivated));
 
-        iconView.connect('notify::selection-mode', Lang.bind(this, function() {
-            if (iconView.selection_mode) {
+        this._worldView.bind_property('empty', this.lookup_action('selection-mode'), 'enabled',
+                                      GObject.BindingFlags.SYNC_CREATE |
+                                      GObject.BindingFlags.INVERT_BOOLEAN);
+
+        this._stack.set_visible_child(this._worldView);
+
+        iconView.connect('notify::is-selecting', Lang.bind(this, function() {
+            if (iconView.is_selecting) {
                 this._header.get_style_context().add_class('selection-mode');
                 this._header.set_custom_title(this._selectionMenuButton);
             } else {
@@ -160,26 +166,21 @@ const MainWindow = new Lang.Class({
                 this._header.set_custom_title(null);
             }
 
-            let selectionState = new GLib.Variant('b', iconView.selection_mode);
+            let selectionState = new GLib.Variant('b', iconView.is_selecting);
             this.lookup_action('selection-mode').set_state(selectionState);
         }));
 
-        iconView.bind_property('selection-mode', newButton, 'visible',
+        iconView.bind_property('is-selecting', newButton, 'visible',
                                GObject.BindingFlags.INVERT_BOOLEAN);
-        iconView.bind_property('selection-mode', select, 'visible',
+        iconView.bind_property('is-selecting', select, 'visible',
                                GObject.BindingFlags.INVERT_BOOLEAN);
-        iconView.bind_property('selection-mode', selectDone, 'visible',
+        iconView.bind_property('is-selecting', selectDone, 'visible',
                                GObject.BindingFlags.SYNC_CREATE);
-        iconView.bind_property('selection-mode', selectionBarRevealer, 'reveal-child',
+        iconView.bind_property('is-selecting', selectionBarRevealer, 'reveal-child',
                                GObject.BindingFlags.SYNC_CREATE);
-        this._worldView.bind_property('empty', this.lookup_action('selection-mode'), 'enabled',
-                                      GObject.BindingFlags.SYNC_CREATE |
-                                      GObject.BindingFlags.INVERT_BOOLEAN);
-
-        this._stack.set_visible_child(this._worldView);
 
-        iconView.connect('view-selection-changed', Lang.bind(this, function() {
-            let items = iconView.get_selection();
+        iconView.connect('selected-children-changed', Lang.bind(this, function() {
+            let items = iconView.get_selected_children();
             let label;
 
             if (items.length > 0) {
@@ -244,11 +245,10 @@ const MainWindow = new Lang.Class({
         this._header.subtitle = subtitle;
     },
 
-    _itemActivated: function(view, id, path) {
-        let [ok, iter] = view.model.get_iter(path);
-        let info = view.model.get_value(iter, World.Columns.INFO);
+    _childActivated: function(view, child) {
+        this._worldView.iconView.is_selecting = false;
+        this._cityView.info = child.info;
 
-        this._cityView.info = info;
         this._stack.set_visible_child(this._cityView);
         this._goToPage(Page.CITY);
     },
@@ -266,11 +266,10 @@ const MainWindow = new Lang.Class({
     },
 
     _setSelectionMode: function(action, param) {
-        this._worldView.iconView.selection_mode = param.get_boolean();
+        this._worldView.iconView.is_selecting = param.get_boolean();
     },
 
     _selectAll: function() {
-        this._worldView.iconView.selection_mode = true;
         this._worldView.iconView.select_all();
     },
 
@@ -310,16 +309,15 @@ const MainWindow = new Lang.Class({
     },
 
     _deleteSelected: function() {
-        let items = this._worldView.iconView.get_selection();
+        let items = this._worldView.iconView.get_selected_children();
         let model = this._worldView.iconView.model;
 
         for (let i = items.length - 1; i >= 0; i--) {
-            let [res, iter] = model.get_iter(items[i]);
-            if (res)
-                model.removeLocation(iter);
+            let location = items[i].info.get_location();
+            model.removeLocation(location);
         }
 
-        this._worldView.iconView.selection_mode = false;
+        this._worldView.iconView.is_selecting = false;
     },
 
     _close: function() {
diff --git a/src/world.js b/src/world.js
index 7359e40..f941785 100644
--- a/src/world.js
+++ b/src/world.js
@@ -34,27 +34,13 @@ 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');
 
+        this._locations = [];
         let locations = this._settings.get_value('locations').deep_unpack();
         for (let i = 0; i < locations.length; i++) {
             let variant = locations[i];
@@ -65,44 +51,35 @@ const WorldModel = new Lang.Class({
         this._settings.connect('changed::locations', Lang.bind(this, this._onChanged));
     },
 
-    _addLocationInternal: function(location) {
+    get locations() {
+        return this._locations;
+    },
+
+    _addLocationInternal: function(location, index) {
         let info = new GWeather.Info({ 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, true);
-            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]);
+
+        let obj = { weather_info: info,
+                    location: location };
+
+        if (index == undefined)
+            this._locations.push(obj);
+        else
+            this._locations[index] = obj;
+
+        this.emit('location-added', obj, index);
     },
 
     _onChanged: function() {
         let newLocations = this._settings.get_value('locations').deep_unpack();
+        let newObjects = [];
         let toErase = [];
 
-        let [ok, iter] = this.get_iter_first();
-        while (ok) {
-            let location = this.get_value(iter, Columns.LOCATION);
+        for (let i = 0; i < this._locations.length; i++) {
+            let obj = this._locations[i];
+            let location = obj.location;
 
             let found = false;
             for (let j = 0; j < newLocations.length; j++) {
@@ -114,19 +91,17 @@ const WorldModel = new Lang.Class({
 
                 if (location.equal(newLocation)) {
                     newLocations[j] = null;
+                    newObjects[j] = obj;
                     found = true;
                     break;
                 }
             }
 
             if (!found)
-                toErase.push(iter.copy());
-
-            ok = this.iter_next(iter);
+                this.emit('location-removed', obj);
         }
 
-        for (let i = 0; i < toErase.length; i++)
-            this.remove(toErase[i]);
+        this._locations = newObjects;
 
         for (let i = 0; i < newLocations.length; i++) {
             let variant = newLocations[i];
@@ -134,7 +109,7 @@ const WorldModel = new Lang.Class({
                 continue;
 
             let newLocation = this._world.deserialize(variant);
-            this._addLocationInternal(newLocation);
+            this._addLocationInternal(newLocation, i);
         }
     },
 
@@ -144,8 +119,7 @@ const WorldModel = new Lang.Class({
         this._settings.set_value('locations', new GLib.Variant('av', newLocations));
     },
 
-    removeLocation: function(iter) {
-        let location = this.get_value(iter, Columns.LOCATION);
+    removeLocation: function(location) {
         let variant = location.serialize();
 
         let newLocations = this._settings.get_value('locations').deep_unpack();
@@ -158,24 +132,237 @@ const WorldModel = new Lang.Class({
         this._settings.set_value('locations', new GLib.Variant('av', newLocations));
     },
 });
+Signals.addSignalMethods(WorldModel.prototype);
+
+const WorldItem = new Lang.Class({
+    Name: 'WorldItem',
+    Extends: Gtk.EventBox,
+
+    Properties: { 'is-selecting': GObject.ParamSpec.boolean('is-selecting', '', '',
+                                                            GObject.ParamFlags.READABLE |
+                                                            GObject.ParamFlags.WRITABLE, false) },
+    Signals: { 'selection-request': { } },
+
+    _init: function(params) {
+        let filtered = Params.filter(params, { weather_info: null });
+        this.parent(params);
+
+        this.add_events(Gdk.EventMask.BUTTON_PRESS_MASK);
+
+        let overlay = new Gtk.Overlay();
+        let grid = new Gtk.Grid({ orientation: Gtk.Orientation.VERTICAL });
+
+        this.info = filtered.weather_info;
+        this._updatedId = this.info.connect('updated', Lang.bind(this, this._updated));
+
+        this._stack = new Gtk.Stack();
+        this._spinner = new Gtk.Spinner();
+        this._stack.add(this._spinner);
+        this._image = new Gtk.Image({ icon_name: 'view-refresh-symbolic',
+                                      use_fallback: true,
+                                      pixel_size: ICON_SIZE });
+        this._stack.add(this._image);
+        grid.add(this._stack);
+
+        this._label = new Gtk.Label({ label: this.info.get_location().get_city_name() });
+        grid.add(this._label);
+
+        this._sublabel = new Gtk.Label();
+        this._sublabel.get_style_context().add_class('dim-label');
+        grid.add(this._sublabel);
+
+        this._toggle = new Gtk.CheckButton({ halign: Gtk.Align.END,
+                                             valign: Gtk.Align.END });
+        overlay.add_overlay(this._toggle);
+
+        overlay.add(grid);
+        this.add(overlay);
+        this.show_all();
+
+        // Can't use vfunc_button_release_event because the signal has GdkEvent
+        // but the vfunc has GdkEventKey
+        this.connect('button-release-event', function(widget, event) {
+            let [ok, button] = event.get_button();
+
+            if (ok && button == 3) {
+                widget.emit('selection-request');
+                return true;
+            }
+
+            return false;
+        });
+
+        if (this.info.is_valid()) {
+            this._stack.visible_child = this._image;
+        } else {
+            this._stack.visible_child = this._spinner;
+            this._spinner.start();
+        }
+    },
+
+    get is_selecting() {
+        if (!this._toggle)
+            return false;
+
+        return this._toggle.visible;
+    },
+
+    set is_selecting(v) {
+        if (!this._toggle)
+            return;
+
+        this._toggle.visible = v;
+        this.notify('is-selecting');
+    },
+
+    vfunc_destroy: function() {
+        if (this._updatedId) {
+            this.info.disconnect(this._updatedId);
+            this._updatedId = 0;
+        }
+
+        this.parent();
+    },
+
+    _updated: function() {
+        this._spinner.stop();
+        this._stack.visible_child = this._image;
+
+        this._image.icon_name = this.info.get_symbolic_icon_name();
+        this._sublabel.label = Util.getWeatherConditions(this.info, true);
+    },
+});
 
 const WorldIconView = new Lang.Class({
     Name: 'WorldView',
-    Extends: Gd.MainView,
+    Extends: Egg.FlowBox,
+    Properties: { 'empty': GObject.ParamSpec.boolean('empty', '', '', GObject.ParamFlags.READABLE, false),
+                  // Can't use "selection-mode", already used for something entirely different
+                  'is-selecting': GObject.ParamSpec.boolean('is-selecting', '', '',
+                                                            GObject.ParamFlags.READABLE |
+                                                            GObject.ParamFlags.WRITABLE, false) },
 
     _init: function(params) {
-        params = Params.fill(params, { view_type: Gd.MainViewType.ICON });
+        let filtered = Params.filter(params, { model: null });
+        params = Params.fill(params, { activate_on_single_click: true,
+                                       selection_mode: Gtk.SelectionMode.MULTIPLE,
+                                       margin_top: 6,
+                                       margin_bottom: 6,
+                                       margin_left: 6,
+                                       margin_right: 6,
+                                       homogeneous: true,
+                                       vertical_spacing: 12,
+                                       horizontal_spacing: 12 });
         this.parent(params);
 
-        this.connect('selection-mode-request', Lang.bind(this, function() {
-            this.selection_mode = true;
+        let model = filtered.model;
+        this.model = filtered.model;
+
+        this._locationAddedId = model.connect('location-added', Lang.bind(this, this._locationAdded));
+        this._locationRemovedId = model.connect('location-removed', Lang.bind(this, this._locationRemoved));
+
+        this._nChildren = 0;
+
+        let locations = this.model.locations;
+        for (let i = 0; i < locations.length; i++)
+            this._locationAdded(this.model, locations[i], i);
+
+        this._selectionMode = false;
+        this.unselect_all();
+    },
+
+    vfunc_destroy: function() {
+        if (this._locationAddedId) {
+            this.model.disconnect(this._locationAddedId);
+            this._locationAddedId = 0;
+        }
+        if (this._locationRemovedId) {
+            this.model.disconnect(this._locationRemovedId);
+            this._locationRemovedId = 0;
+        }
+
+        this.parent();
+    },
+
+    vfunc_selected_children_changed: function() {
+        let selected = this.get_selected_children();
+
+        if (selected.length > 0)
+            this.is_selecting = true;
+
+        this.parent();
+    },
+
+    get is_selecting() {
+        return this._selectionMode;
+    },
+
+    set is_selecting(v) {
+        if (v == this._selectionMode)
+            return;
+
+        this._selectionMode = v;
+        if (!v)
+            this.unselect_all();
+
+        let children = this.get_children();
+        for (let i = 0; i < children.length; i++)
+            children.is_selecting = v;
+
+        this.notify('is-selecting');
+    },
+
+    get empty() {
+        return this._nChildren == 0;
+    },
+
+    _locationAdded: function(model, obj, index) {
+        let item = new WorldItem({ weather_info: obj.weather_info });
+        obj._item = item;
+
+        item.connect('selection-request', Lang.bind(this, function(item) {
+            if (this.is_child_selected(item))
+                this.unselect_child(item);
+            else
+                this.select_child(item);
         }));
+
+        // FIXME: child-set the index
+        this.add(item);
+        this._nChildren++;
+
+        if (this._nChildren == 1)
+            this.notify('empty');
+    },
+
+    _locationRemoved: function(model, obj) {
+        if (obj._item) {
+            obj._item.destroy();
+            this._nChildren--;
+
+            if (this._nChildren == 0)
+                this.notify('empty');
+        }
+
+        obj._item = null;
+    },
+
+    _updateEmpty: function() {
+        let [ok, iter] = this.model.get_iter_first();
+
+        if (!ok != this._empty) {
+            if (ok) {
+            }
+
+            this._empty = !ok;
+            this.notify('empty');
+        }
     }
 });
 
 const WorldContentView = new Lang.Class({
     Name: 'WorldContentView',
-    Extends: Gtk.Bin,
+    Extends: Gtk.Frame,
     Properties: { 'empty': GObject.ParamSpec.boolean('empty', '', '', GObject.ParamFlags.READABLE, false) },
 
     _init: function(model, params) {
@@ -183,6 +370,9 @@ const WorldContentView = new Lang.Class({
                                        halign: Gtk.Align.FILL, valign: Gtk.Align.FILL });
         this.parent(params);
 
+        this.get_style_context().add_class('view');
+        this.get_style_context().add_class('content-view');
+
         this.iconView = new WorldIconView({ model: model, visible: true });
 
         this._placeHolder = new Gtk.Grid({ halign: Gtk.Align.CENTER,
@@ -220,49 +410,14 @@ const WorldContentView = new Lang.Class({
                                  1, 1, 1, 1);
         this._placeHolder.show_all();
 
-        this.model = model;
-        this._rowInsertedId = model.connect('row-inserted', Lang.bind(this, this._updateEmpty));
-        this._rowDeletedId = model.connect('row-deleted', Lang.bind(this, this._updateEmpty));
-
-        let [ok, ] = model.get_iter_first();
-        if (ok)
-            this.add(this.iconView);
-        else
-            this.add(this._placeHolder);
-        this._empty = !ok;
-    },
-
-    get empty() {
-        return this._empty;
-    },
-
-    vfunc_destroy: function() {
-        if (this._rowInsertedId) {
-            this.model.disconnect(this._rowInsertedId);
-            this._rowInsertedId = 0;
-        }
-        if (this._rowDeletedId) {
-            this.model.disconnect(this._rowDeletedId);
-            this._rowDeletedId = 0;
-        }
-
-        this.parent();
+        this.iconView.connect('notify::empty', Lang.bind(this, this._updateEmpty));
+        this._updateEmpty();
     },
 
     _updateEmpty: function() {
-        let [ok, iter] = this.model.get_iter_first();
-
-        if (!ok != this._empty) {
-            if (ok) {
-                this.remove(this._placeHolder);
-                this.add(this.iconView);
-            } else {
-                this.remove(this.iconView);
-                this.add(this._placeHolder);
-            }
-
-            this._empty = !ok;
-            this.notify('empty');
-        }
-    }
+        if (this.iconView.empty)
+            this.add(this._placeHolder);
+        else
+            this.add(this.iconView);
+    },
 });


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