[gnome-clocks/wip/flowbox] port to flowbox



commit ec1fdde66ce084f27080e992e718bbc5f42cda5c
Author: Paolo Borelli <pborelli gnome org>
Date:   Sun Feb 21 10:32:37 2016 +0100

    port to flowbox
    
    This is an unifinished WIP commit.
    The idea is to reimplement the ContentView widget using FlowBox
    instead of IconView.
    For now I am adding a Content2View widget so that I can leave the
    old code untouched and only work on Alarm without touching World.
    Once things are working we can rename back to ContentView and drop
    
    The code is mostly working, the most noticeable TODO items are:
     - adding and removing alarms does not update the view
     - the tiles do not change on hover
     - port more css (font size, inactive and ringing states etc)
     - the way flowbox places tiles when resizing the windows looks bad,
       the space among tiles changes and this is especially noticeable
       because the second row moves when the action bar appears
     - once alarm is fine, port world (should be just be a matter of
       creating a custom Tile template)
     - once they are both ported delete a load of custom code (the custom
       iconview, the custom cell renderers, a lot of css etc).

 data/css/gnome-clocks.css       |   48 ++++----
 data/gnome-clocks.gresource.xml |    1 +
 data/ui/alarm.ui                |    5 +-
 data/ui/alarmtile.ui            |   68 ++++++++++
 src/alarm.vala                  |   88 ++++++++-----
 src/widgets.vala                |  265 ++++++++++++++++++++++++++++++++++++++-
 src/world.vala                  |    2 +-
 7 files changed, 412 insertions(+), 65 deletions(-)
---
diff --git a/data/css/gnome-clocks.css b/data/css/gnome-clocks.css
index daec8e6..b7654cb 100644
--- a/data/css/gnome-clocks.css
+++ b/data/css/gnome-clocks.css
@@ -42,71 +42,69 @@ window > stack:backdrop {
 
 /* alarms */
 
-.clocks-digital-renderer.active {
+.alarm-tile {
+    font-size: 32pt;
+    color: shade(@insensitive_fg_color,0.9);
     background-color: transparent;
     background-image: -gtk-gradient(radial,
                                     center center, 0,
                                     center bottom, 1.0,
-                                    from(shade(@theme_selected_bg_color,1.2)), to(@theme_selected_bg_color));
-    color: white;
+                                    from(shade(@insensitive_bg_color,0.9)), 
to(shade(@insensitive_bg_color,0.85)));
+}
+
+.alarm-tile .sub-label {
+    font-size: 14pt;
 }
 
-.clocks-digital-renderer.active:hover {
+.alarm-tile:hover {
+    color: @insensitive_fg_color;
     background-color: transparent;
     background-image: -gtk-gradient(radial,
                                     center center, 0,
                                     center bottom, 1.0,
-                                    from(shade(@theme_selected_bg_color, 1.4)), 
to(@theme_selected_bg_color));
-    text-shadow: 0 2px 2px rgba(0,0,0,0.5);
+                                    from(shade(@insensitive_bg_color,0.99)), 
to(shade(@insensitive_bg_color,0.9)));
 }
 
-.clocks-digital-renderer.snoozing {
+.active .alarm-tile {
+    color: white;
     background-color: transparent;
     background-image: -gtk-gradient(radial,
                                     center center, 0,
                                     center bottom, 1.0,
-                                    from(@warning_color), to(shade(@warning_color,0.9)));
-    color: white;
+                                    from(shade(@theme_selected_bg_color,1.2)), to(@theme_selected_bg_color));
 }
 
-.clocks-digital-renderer.snoozing:hover {
+.active .alarm-tile:hover {
+    text-shadow: 0 2px 2px rgba(0,0,0,0.5);
     background-color: transparent;
     background-image: -gtk-gradient(radial,
                                     center center, 0,
                                     center bottom, 1.0,
-                                    from(shade(@warning_color,1.1)), to(shade(@warning_color,0.99)));
-    text-shadow: 0 2px 2px rgba(0,0,0,0.5);
+                                    from(shade(@theme_selected_bg_color, 1.4)), 
to(@theme_selected_bg_color));
 }
 
-.clocks-digital-renderer.inactive {
+.snoozing .alarm-tile {
+    color: white;
     background-color: transparent;
     background-image: -gtk-gradient(radial,
                                     center center, 0,
                                     center bottom, 1.0,
-                                    from(shade(@insensitive_bg_color,0.9)), 
to(shade(@insensitive_bg_color,0.85)));
-    color: shade(@insensitive_fg_color,0.9);
+                                    from(@warning_color), to(shade(@warning_color,0.9)));
 }
 
-.clocks-digital-renderer.inactive:hover {
+.snoozing .alarm-tile:hover {
+    text-shadow: 0 2px 2px rgba(0,0,0,0.5);
     background-color: transparent;
     background-image: -gtk-gradient(radial,
                                     center center, 0,
                                     center bottom, 1.0,
-                                    from(shade(@insensitive_bg_color,0.99)), 
to(shade(@insensitive_bg_color,0.9)));
-    color: @insensitive_fg_color;
+                                    from(shade(@warning_color,1.1)), to(shade(@warning_color,0.99)));
 }
 
 .clocks-ampm-toggle-button {
     font-size: 18pt;
 }
 
-.clocks-digital-renderer.active.stripe,
-.clocks-digital-renderer.snoozing.stripe,
-.clocks-digital-renderer.inactive.stripe {
-    background-color: transparent;
-    background-image: none;
-}
-
 .clocks-standalone-label,
 .clocks-ringing-label {
     font-size: 64pt;
diff --git a/data/gnome-clocks.gresource.xml b/data/gnome-clocks.gresource.xml
index c2390e8..eb0cf40 100644
--- a/data/gnome-clocks.gresource.xml
+++ b/data/gnome-clocks.gresource.xml
@@ -9,6 +9,7 @@
     <file preprocess="xml-stripblanks">ui/worldlocationdialog.ui</file>
     <file preprocess="xml-stripblanks">ui/world.ui</file>
     <file preprocess="xml-stripblanks">ui/alarmringing.ui</file>
+    <file preprocess="xml-stripblanks">ui/alarmtile.ui</file>
     <file preprocess="xml-stripblanks">ui/alarmsetupdialog.ui</file>
     <file preprocess="xml-stripblanks">ui/alarm.ui</file>
     <file preprocess="xml-stripblanks">ui/stopwatch.ui</file>
diff --git a/data/ui/alarm.ui b/data/ui/alarm.ui
index 99c7bf4..d06168b 100644
--- a/data/ui/alarm.ui
+++ b/data/ui/alarm.ui
@@ -1,8 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <!-- interface-requires gtk+ 3.6 -->
+  <requires lib="gtk+" version="3.18"/>
   <template class="ClocksAlarmFace" parent="GtkStack">
     <property name="visible">True</property>
+    <property name="can_focus">False</property>
     <signal name="notify::visible-child" handler="visible_child_changed" swapped="no"/>
     <child>
       <object class="GtkGrid" id="empty_view">
@@ -42,7 +43,7 @@
       </object>
     </child>
     <child>
-      <object class="ClocksContentView" id="content_view">
+      <object class="ClocksContent2View" id="content_view">
         <property name="visible">True</property>
         <signal name="item-activated" handler="item_activated" swapped="no"/>
       </object>
diff --git a/data/ui/alarmtile.ui b/data/ui/alarmtile.ui
new file mode 100644
index 0000000..31213ef
--- /dev/null
+++ b/data/ui/alarmtile.ui
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="ClocksAlarmTile" parent="GtkGrid">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="halign">start</property>
+    <property name="valign">start</property>
+    <child>
+      <object class="GtkGrid" id="tile_grid">
+        <property name="width_request">256</property>
+        <property name="height_request">256</property>
+        <property name="can_focus">False</property>
+        <child>
+          <object class="GtkLabel" id="time_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="halign">center</property>
+            <property name="valign">end</property>
+            <property name="hexpand">True</property>
+            <property name="vexpand">True</property>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="sub_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="halign">center</property>
+            <property name="valign">start</property>
+            <property name="hexpand">True</property>
+            <property name="vexpand">True</property>
+            <style>
+              <class name="sub-label"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">1</property>
+          </packing>
+        </child>
+        <style>
+          <class name="alarm-tile"/>
+        </style>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="name_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">center</property>
+        <style>
+          <class name="alarm-name"/>
+        </style>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">1</property>
+      </packing>
+    </child>
+  </template>
+</interface>
diff --git a/src/alarm.vala b/src/alarm.vala
index f57291a..262c54c 100644
--- a/src/alarm.vala
+++ b/src/alarm.vala
@@ -54,24 +54,23 @@ private class Item : Object, ContentItem {
     public int minute { get; set; }
     public Utils.Weekdays days { get; construct set; }
 
-    public string repeat_label {
-        owned get {
-            return days.get_label ();
-        }
-    }
-
     public State state { get; private set; }
 
     public string time_label {
          owned get {
-            return Utils.WallClock.get_default ().format_time (alarm_time);
+            var t = state == State.SNOOZING ? snooze_time : alarm_time;
+            return Utils.WallClock.get_default ().format_time (t);
          }
     }
 
-    public string snooze_time_label {
-         owned get {
-            return Utils.WallClock.get_default ().format_time (snooze_time);
-         }
+    public string sub_label {
+        owned get {
+            if (state == State.SNOOZING) {
+                return Utils.WallClock.get_default ().format_time (alarm_time);
+            }
+
+            return days.get_label ();
+        }
     }
 
     public bool active {
@@ -226,22 +225,6 @@ private class Item : Object, ContentItem {
         return state != last_state;
     }
 
-    public void get_thumb_properties (out string text,
-                                      out string subtext,
-                                      out Gdk.Pixbuf? pixbuf,
-                                      out string css_class) {
-        if (state == State.SNOOZING) {
-            text = snooze_time_label;
-            subtext = "(%s)".printf(time_label);
-            css_class = "snoozing";
-        } else {
-            text = time_label;
-            subtext = repeat_label;
-            css_class = active ? "active" : "inactive";
-        }
-        pixbuf = null;
-    }
-
     public void serialize (GLib.VariantBuilder builder) {
         builder.open (new GLib.VariantType ("a{sv}"));
         builder.add ("{sv}", "name", new GLib.Variant.string (name));
@@ -285,6 +268,44 @@ private class Item : Object, ContentItem {
     }
 }
 
+[GtkTemplate (ui = "/org/gnome/clocks/ui/alarmtile.ui")]
+private class Tile : Gtk.Grid {
+    public Item alarm { get; construct set; }
+
+    [GtkChild]
+    private Gtk.Widget name_label;
+    [GtkChild]
+    private Gtk.Widget time_label;
+    [GtkChild]
+    private Gtk.Widget sub_label;
+
+    public Tile (Item alarm) {
+        Object (alarm: alarm);
+
+        alarm.bind_property ("name", name_label, "label", BindingFlags.DEFAULT | BindingFlags.SYNC_CREATE);
+        alarm.bind_property ("time-label", time_label, "label", BindingFlags.DEFAULT | 
BindingFlags.SYNC_CREATE);
+        alarm.bind_property ("sub-label", sub_label, "label", BindingFlags.DEFAULT | 
BindingFlags.SYNC_CREATE);
+
+        update_css_classes ();
+        alarm.notify["active"].connect (update_css_classes);
+        alarm.notify["state"].connect (update_css_classes);
+    }
+
+    private void update_css_classes () {
+        if (alarm.active) {
+            get_style_context ().add_class ("active");
+        } else {
+            get_style_context ().remove_class ("active");
+        }
+
+        if (alarm.state == Item.State.SNOOZING) {
+            get_style_context ().add_class ("snoozing");
+        } else {
+            get_style_context ().remove_class ("snoozing");
+        }
+    }
+}
+
 [GtkTemplate (ui = "/org/gnome/clocks/ui/alarmsetupdialog.ui")]
 private class SetupDialog : Gtk.Dialog {
     private Utils.WallClock.Format format;
@@ -518,11 +539,7 @@ private class RingingPanel : Gtk.Grid {
 
     public void update () {
         if (alarm != null) {
-            if (alarm.state == Item.State.SNOOZING) {
-                time_label.set_text (alarm.snooze_time_label);
-            } else {
-                time_label.set_text (alarm.time_label);
-            }
+            time_label.set_text (alarm.time_label);
         }
     }
 }
@@ -539,7 +556,7 @@ public class Face : Gtk.Stack, Clocks.Clock {
     [GtkChild]
     private Gtk.Widget empty_view;
     [GtkChild]
-    private ContentView content_view;
+    private Content2View content_view;
     [GtkChild]
     private RingingPanel ringing_panel;
 
@@ -579,7 +596,10 @@ public class Face : Gtk.Stack, Clocks.Clock {
         new_button.action_name = "win.new";
         header_bar.pack_start (new_button);
 
-        content_view.bind_model (alarms);
+        content_view.bind_model (alarms, (item) => {
+            return new Tile ((Item)item);
+        });
+
         content_view.set_header_bar (header_bar);
 
         load ();
diff --git a/src/widgets.vala b/src/widgets.vala
index b0065b0..ebbca1b 100644
--- a/src/widgets.vala
+++ b/src/widgets.vala
@@ -260,12 +260,14 @@ public interface ContentItem : GLib.Object {
     public abstract bool selectable { get; set; default = true; }
     public abstract bool selected { get; set; default = false; }
 
+    public abstract void serialize (GLib.VariantBuilder builder);
+}
+
+public interface ContentThumb : GLib.Object {
     public abstract void get_thumb_properties (out string text,
                                                out string subtext,
                                                out Gdk.Pixbuf? pixbuf,
                                                out string css_class);
-
-    public abstract void serialize (GLib.VariantBuilder builder);
 }
 
 public class ContentStore : GLib.Object, GLib.ListModel {
@@ -456,7 +458,7 @@ private class IconView : Gtk.IconView {
                 string subtext;
                 Gdk.Pixbuf? pixbuf;
                 string css_class;
-                item.get_thumb_properties (out text, out subtext, out pixbuf, out css_class);
+                ((ContentThumb)item).get_thumb_properties (out text, out subtext, out pixbuf, out css_class);
                 renderer.selectable = item.selectable;
                 renderer.toggle_visible = (mode == Mode.SELECTION);
                 renderer.checked = item.selected;
@@ -741,6 +743,263 @@ public class ContentView : Gtk.Bin {
     }
 }
 
+public class Content2View : Gtk.Bin {
+    public enum Mode {
+        NORMAL,
+        SELECTION
+    }
+
+    public Mode mode {
+        get {
+            return _mode;
+        }
+
+        private set {
+            if (_mode != value) {
+                _mode = value;
+
+                switch (_mode) {
+                case Mode.SELECTION:
+                    header_bar.mode = HeaderBar.Mode.SELECTION;
+                    action_bar.show ();
+                    break;
+                case Mode.NORMAL:
+                    header_bar.mode = HeaderBar.Mode.NORMAL;
+                    action_bar.hide ();
+                    // clear current selection
+                    model.unselect_all ();
+                    break;
+                default:
+                    assert_not_reached ();
+                }
+
+                update_header_bar ();
+            }
+        }
+    }
+
+    private bool can_select {
+        get {
+            return _can_select;
+        }
+
+        private set {
+            if (_can_select != value) {
+                _can_select = value;
+
+                // show the select button only if we are mapped,
+                // since we do not want to show it when the geolocation
+                // query ends, but we are on another page
+                select_button.visible = _can_select && get_mapped ();
+            }
+        }
+    }
+
+    private Mode _mode;
+    private bool _can_select;
+    private ContentStore model;
+    private Gtk.FlowBox flow_box;
+    private Gtk.Button select_button;
+    private Gtk.Button cancel_button;
+    private SelectionMenuButton selection_menubutton;
+    private Gtk.Grid grid;
+    private Gtk.ActionBar action_bar;
+    private Gtk.Button delete_button;
+    private HeaderBar? header_bar;
+
+    construct {
+        get_style_context ().add_class ("content-view");
+
+        flow_box = new Gtk.FlowBox ();
+        flow_box.selection_mode = Gtk.SelectionMode.NONE;
+        flow_box.min_children_per_line = 3;
+        flow_box.row_spacing = 12;
+        flow_box.column_spacing = 12;
+        flow_box.margin_top = 12;
+        flow_box.margin_bottom = 12;
+        flow_box.margin_left = 12;
+        flow_box.margin_right = 12;
+
+        flow_box.child_activated.connect ((child) => {
+            var item = model.get_item (child.get_index ()) as ContentItem;
+            if (item != null) {
+                item_activated (item);
+            }
+        });
+
+        var scrolled_window = new Gtk.ScrolledWindow (null, null);
+        scrolled_window.add (flow_box);
+        scrolled_window.hexpand = true;
+        scrolled_window.vexpand = true;
+        scrolled_window.halign = Gtk.Align.FILL;
+        scrolled_window.valign = Gtk.Align.FILL;
+
+        grid = new Gtk.Grid ();
+        grid.attach (scrolled_window, 0, 0, 1, 1);
+
+        action_bar = new Gtk.ActionBar ();
+        action_bar.no_show_all = true;
+        grid.attach (action_bar, 0, 1, 1, 1);
+
+        delete_button = new Gtk.Button ();
+        delete_button.label = _("Delete");
+        delete_button.visible = true;
+        delete_button.sensitive = false;
+        delete_button.halign = Gtk.Align.END;
+        delete_button.hexpand = true;
+        delete_button.clicked.connect (() => {
+            model.delete_selected ();
+            mode = Mode.NORMAL;
+        });
+
+        action_bar.pack_end (delete_button);
+
+        add (grid);
+        grid.show_all ();
+    }
+
+    public signal void item_activated (ContentItem item);
+
+    public delegate Gtk.Widget Content2ViewCreateWidgetFunc (ContentItem item);
+
+    public void bind_model (ContentStore store, owned Content2ViewCreateWidgetFunc create_func) {
+        model = store;
+        model.items_changed.connect ((position, removed, added) => {
+            var first_selectable = model.find ((i) => {
+                return i.selectable;
+            });
+
+            can_select = first_selectable != null;
+        });
+
+        model.selection_changed.connect (() => {
+            var n_items = model.get_n_selected ();
+            selection_menubutton.n_items = n_items;
+
+            if (n_items != 0) {
+                delete_button.sensitive = true;
+            } else {
+                delete_button.sensitive = false;
+            }
+        });
+
+        flow_box.bind_model (model, (object) => {
+            var item = (ContentItem) object;
+            var inner = create_func (item);
+
+            // wrap the widget in an event box to handle righ-click
+            var event_box = new Gtk.EventBox ();
+            event_box.add (inner);
+            event_box.button_press_event.connect ((event) => {
+                // On right click, swicth to selection mode automatically
+                if (item.selectable && event.button == Gdk.BUTTON_SECONDARY) {
+                    mode = Mode.SELECTION;
+                }
+
+                if (item.selectable && mode == Mode.SELECTION) {
+                    item.selected = !item.selected;
+                    return true;
+                } else if (event.button == Gdk.BUTTON_PRIMARY) {
+                    item_activated (item);
+                    return true;
+                }
+
+                return false;
+            });
+
+            // wrap the widget in overlay for the selection check box
+            var overlay = new Gtk.Overlay ();
+            overlay.halign = Gtk.Align.START;
+            overlay.valign = Gtk.Align.START;
+            overlay.add (event_box);
+
+            var check = new Gtk.CheckButton ();
+            check.no_show_all = true;
+            check.halign = Gtk.Align.END;
+            check.valign = Gtk.Align.END;
+            check.margin_bottom = 8;
+            check.margin_end = 8;
+
+            item.bind_property ("selected", check, "active", BindingFlags.DEFAULT | BindingFlags.SYNC_CREATE 
| BindingFlags.BIDIRECTIONAL);
+            item.bind_property ("selectable", check, "visible", BindingFlags.DEFAULT | 
BindingFlags.SYNC_CREATE,
+                                 (binding, selectable, ref visible) => {
+                visible = this.mode == Mode.SELECTION && (item).selectable;
+                return true;
+            });
+
+            bind_property ("mode", check, "visible", BindingFlags.DEFAULT | BindingFlags.SYNC_CREATE,
+                           (binding, mode, ref visible) => {
+                visible = mode == Mode.SELECTION && (item).selectable;
+                return true;
+            });
+
+            overlay.add_overlay (check);
+
+            // manually wrap in flowboxchild ourselves since we want to set alignment
+            var flow_box_child = new Gtk.FlowBoxChild ();
+            flow_box_child.halign = Gtk.Align.START;
+            flow_box_child.valign = Gtk.Align.START;
+            flow_box_child.add (overlay);
+            flow_box_child.get_style_context ().add_class ("tile");
+
+            return flow_box_child;
+        });
+    }
+
+    public void select_all () {
+        mode = Mode.SELECTION;
+        model.select_all ();
+    }
+
+    public void unselect_all () {
+        model.unselect_all ();
+    }
+
+    public bool escape_pressed () {
+        if (mode == Mode.SELECTION) {
+            mode = Mode.NORMAL;
+            return true;
+        }
+        return false;
+    }
+
+    public void set_header_bar (HeaderBar bar) {
+        header_bar = bar;
+
+        select_button = new Gtk.Button ();
+        var select_button_image = new Gtk.Image.from_icon_name ("object-select-symbolic", Gtk.IconSize.MENU);
+        select_button.set_image (select_button_image);
+        select_button.valign = Gtk.Align.CENTER;
+        select_button.no_show_all = true;
+        select_button.clicked.connect (() => {
+            mode = Mode.SELECTION;
+        });
+        header_bar.pack_end (select_button);
+
+        cancel_button = new Gtk.Button.with_label (_("Cancel"));
+        cancel_button.no_show_all = true;
+        cancel_button.valign = Gtk.Align.CENTER;
+        cancel_button.clicked.connect (() => {
+            mode = Mode.NORMAL;
+        });
+        header_bar.pack_end (cancel_button);
+
+        selection_menubutton = new SelectionMenuButton ();
+    }
+
+    public void update_header_bar () {
+        switch (header_bar.mode) {
+        case HeaderBar.Mode.SELECTION:
+            header_bar.custom_title = selection_menubutton;
+            cancel_button.show ();
+            break;
+        case HeaderBar.Mode.NORMAL:
+            select_button.visible = can_select;
+            break;
+        }
+    }
+}
+
 public class AmPmToggleButton : Gtk.Button {
     public enum AmPm {
         AM,
diff --git a/src/world.vala b/src/world.vala
index 791f0f1..bb84ede 100644
--- a/src/world.vala
+++ b/src/world.vala
@@ -19,7 +19,7 @@
 namespace Clocks {
 namespace World {
 
-public class Item : Object, ContentItem {
+public class Item : Object, ContentItem, ContentThumb {
     private static Gdk.Pixbuf? day_pixbuf = Utils.load_image ("day.png");
     private static Gdk.Pixbuf? night_pixbuf = Utils.load_image ("night.png");
 


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