[recipes/shopping-list: 11/15] Be safe against crash in the details page



commit 71045fec007b05e8f2f471594afb85da494aec20
Author: Matthias Clasen <mclasen redhat com>
Date:   Fri Jan 13 22:43:55 2017 -0500

    Be safe against crash in the details page

 src/Makefile.am                                    |    2 +
 src/gr-details-page.c                              |   13 +-
 src/gr-details-page.ui                             |   40 +--
 src/gr-recipe-tile.c                               |    2 +-
 src/gr-recipes-page.c                              |    8 +-
 src/gr-recipes-page.ui                             |   11 +
 src/gr-shopping-page.c                             |  452 ++++++++++++++++++++
 src/gr-shopping-page.h                             |   37 ++
 src/gr-shopping-page.ui                            |  146 +++++++
 src/gr-window.c                                    |   10 +-
 src/gr-window.ui                                   |    6 +
 .../16x16/apps/shoppingcart-symbolic.symbolic.png  |  Bin 0 -> 467 bytes
 .../24x24/apps/shoppingcart-symbolic.symbolic.png  |  Bin 0 -> 662 bytes
 src/main.c                                         |    2 +
 src/recipes-images.gresource.xml                   |    2 +
 src/recipes-ui.gresource.xml                       |    1 +
 src/recipes.css                                    |   42 ++-
 17 files changed, 731 insertions(+), 43 deletions(-)
---
diff --git a/src/Makefile.am b/src/Makefile.am
index 6647c3d..cf0411e 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -100,6 +100,8 @@ recipes_SOURCES = \
        gr-search-page.c        \
        gr-season.h             \
        gr-season.c             \
+       gr-shopping-page.h      \
+       gr-shopping-page.c      \
        gr-timer.h              \
        gr-timer.c              \
        gr-timer-widget.h       \
diff --git a/src/gr-details-page.c b/src/gr-details-page.c
index be972c9..10c4672 100644
--- a/src/gr-details-page.c
+++ b/src/gr-details-page.c
@@ -140,6 +140,7 @@ struct _GrDetailsPage
         GtkWidget *time_spin;
         GtkWidget *start_button;
         GtkWidget *favorite_button;
+        GtkWidget *shop_button;
         GtkWidget *duration_stack;
         GtkWidget *remaining_time_label;
         GtkWidget *chef_label;
@@ -517,7 +518,7 @@ shop_it (GrDetailsPage *page)
         GrRecipeStore *store;
 
         store = gr_app_get_recipe_store (GR_APP (g_application_get_default ()));
-        if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (page->favorite_button)))
+        if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (page->shop_button)))
                 gr_recipe_store_add_to_shopping (store, page->recipe);
         else
                 gr_recipe_store_remove_from_shopping (store, page->recipe);
@@ -742,6 +743,7 @@ gr_details_page_class_init (GrDetailsPageClass *klass)
         gtk_widget_class_bind_template_child (widget_class, GrDetailsPage, time_spin);
         gtk_widget_class_bind_template_child (widget_class, GrDetailsPage, start_button);
         gtk_widget_class_bind_template_child (widget_class, GrDetailsPage, favorite_button);
+        gtk_widget_class_bind_template_child (widget_class, GrDetailsPage, shop_button);
         gtk_widget_class_bind_template_child (widget_class, GrDetailsPage, duration_stack);
         gtk_widget_class_bind_template_child (widget_class, GrDetailsPage, remaining_time_label);
         gtk_widget_class_bind_template_child (widget_class, GrDetailsPage, chef_label);
@@ -900,6 +902,7 @@ gr_details_page_set_recipe (GrDetailsPage *page,
         g_autoptr(GArray) images = NULL;
         gboolean cooking;
         gboolean favorite;
+        gboolean shopping;
 
         g_set_object (&page->recipe, recipe);
 
@@ -944,6 +947,9 @@ gr_details_page_set_recipe (GrDetailsPage *page,
         favorite = gr_recipe_store_is_favorite (store, recipe);
         gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (page->favorite_button), favorite);
 
+        shopping = gr_recipe_store_is_in_shopping (store, recipe);
+        gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (page->shop_button), shopping);
+
         chef = gr_recipe_store_get_chef (store, author);
         g_set_object (&page->chef, chef);
 
@@ -994,9 +1000,12 @@ details_page_reload (GrDetailsPage *page,
         const char *name;
         const char *new_name;
 
+        if (recipe == NULL || page->recipe == NULL)
+                return;
+
         name = gr_recipe_get_id (page->recipe);
         new_name = gr_recipe_get_id (recipe);
-        if (strcmp (name, new_name) == 0)
+        if (g_strcmp0 (name, new_name) == 0)
                 gr_details_page_set_recipe (page, recipe);
 }
 
diff --git a/src/gr-details-page.ui b/src/gr-details-page.ui
index fd71bfc..fb72ae3 100644
--- a/src/gr-details-page.ui
+++ b/src/gr-details-page.ui
@@ -457,16 +457,9 @@ followed</property>
         <child>
           <object class="GtkToggleButton" id="shop_button">
             <property name="visible">1</property>
-            <property name="tooltip-text" translatable="yes">Shopping list</property>
-            <style> <class name="image-button"/> </style>
+            <property name="focus-on-click">0</property>
+            <property name="label" translatable="yes">Cook it later</property>
             <signal name="toggled" handler="shop_it" swapped="yes"/>
-            <child>
-              <object class="GtkImage">
-                <property name="visible">1</property>
-                <property name="icon-name">shoppingcart</property>
-                <property name="icon-size">1</property>
-              </object>
-            </child>
           </object>
           <packing>
             <property name="pack-type">end</property>
@@ -475,8 +468,11 @@ followed</property>
         <child>
           <object class="GtkToggleButton" id="favorite_button">
             <property name="visible">1</property>
-            <property name="tooltip-text" translatable="yes">Favorites</property>
-            <style> <class name="image-button"/> </style>
+            <property name="focus-on-click">0</property>
+            <property name="tooltip-text" translatable="yes">Add to favorites</property>
+            <style>
+              <class name="image-button"/>
+            </style>
             <signal name="toggled" handler="cook_it_later" swapped="yes"/>
             <child>
               <object class="GtkImage">
@@ -492,16 +488,9 @@ followed</property>
         </child>
         <child>
           <object class="GtkButton" id="export_button">
-            <property name="tooltip-text" translatable="yes">Export</property>
+            <property name="focus-on-click">0</property>
+            <property name="label" translatable="yes">Export</property>
             <signal name="clicked" handler="export_recipe" swapped="yes"/>
-            <style> <class name="image-button"/> </style>
-            <child>
-              <object class="GtkImage">
-                <property name="visible">1</property>
-                <property name="icon-name">emblem-shared-symbolic</property>
-                <property name="icon-size">1</property>
-              </object>
-            </child>
           </object>
           <packing>
             <property name="pack-type">end</property>
@@ -510,16 +499,9 @@ followed</property>
         <child>
           <object class="GtkButton">
             <property name="visible">1</property>
-            <property name="tooltip-text" translatable="yes">Print</property>
+            <property name="focus-on-click">0</property>
+            <property name="label" translatable="yes">Print</property>
             <signal name="clicked" handler="print_recipe" swapped="yes"/>
-            <style> <class name="image-button"/> </style>
-            <child>
-              <object class="GtkImage">
-                <property name="visible">1</property>
-                <property name="icon-name">printer-symbolic</property>
-                <property name="icon-size">1</property>
-              </object>
-            </child>
           </object>
           <packing>
             <property name="pack-type">end</property>
diff --git a/src/gr-recipe-tile.c b/src/gr-recipe-tile.c
index 9ce3f96..d8e5933 100644
--- a/src/gr-recipe-tile.c
+++ b/src/gr-recipe-tile.c
@@ -74,7 +74,7 @@ add_recipe_css (GrRecipe *recipe,
         if (images->len > 0) {
                 GrRotatedImage *ri = &g_array_index (images, GrRotatedImage, 0);
 
-                g_string_append_printf (css, "box.recipe.%s {\n", id);
+                g_string_append_printf (css, "image.recipe.small.%s,\nbox.recipe.%s {\n", id, id);
                 g_string_append_printf (css, "  background: url('%s');\n"
                                              "  background-size: cover;\n"
                                              "  background-position: center;\n", ri->path);
diff --git a/src/gr-recipes-page.c b/src/gr-recipes-page.c
index 5d09757..5a35d39 100644
--- a/src/gr-recipes-page.c
+++ b/src/gr-recipes-page.c
@@ -337,12 +337,12 @@ populate_recipes_from_store (GrRecipesPage *self)
         }
 
         if (shopping == 1)
-                tmp = g_strdup_printf (_("Shopping list: <b>%s</b>"), shop1);
+                tmp = g_strdup_printf (_("Cook later: <b>%s</b>"), shop1);
         else if (shopping == 2)
-                tmp = g_strdup_printf (_("Shopping list: <b>%s and %s</b>"), shop1, shop2);
+                tmp = g_strdup_printf (_("Cook later: <b>%s and %s</b>"), shop1, shop2);
         else
-                tmp = g_strdup_printf (ngettext ("Shopping list: <b>%s, %s and %d other</b>",
-                                                 "Shopping list: <b>%s, %s and %d others</b>", shopping - 
2), shop1, shop2, shopping - 2);
+                tmp = g_strdup_printf (ngettext ("Cook later: <b>%s, %s and %d other</b>",
+                                                 "Cook later: <b>%s, %s and %d others</b>", shopping - 2), 
shop1, shop2, shopping - 2);
         gtk_label_set_label (GTK_LABEL (self->shopping_list), tmp);
         g_free (tmp);
 
diff --git a/src/gr-recipes-page.ui b/src/gr-recipes-page.ui
index 8421546..0eeef4a 100644
--- a/src/gr-recipes-page.ui
+++ b/src/gr-recipes-page.ui
@@ -63,6 +63,17 @@
                     <property name="visible">1</property>
                     <property name="orientation">horizontal</property>
                     <child>
+                      <object class="GtkImage">
+                        <property name="visible">1</property>
+                        <property name="margin-start">10</property>
+                        <property name="margin-top">10</property>
+                        <property name="margin-bottom">10</property>
+                        <property name="margin-end">0</property>
+                        <property name="icon-name">shoppingcart-symbolic</property>
+                        <property name="icon-size">1</property>
+                      </object>
+                    </child>
+                    <child>
                       <object class="GtkLabel" id="shopping_list">
                         <property name="visible">1</property>
                         <property name="margin">10</property>
diff --git a/src/gr-shopping-page.c b/src/gr-shopping-page.c
new file mode 100644
index 0000000..4a46edf
--- /dev/null
+++ b/src/gr-shopping-page.c
@@ -0,0 +1,452 @@
+/* gr-shopping-page.c:
+ *
+ * Copyright (C) 2017 Matthias Clasen <mclasen redhat com>
+ *
+ * Licensed under the GNU General Public License Version 3
+ *
+ * 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
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include "gr-shopping-page.h"
+#include "gr-recipe-store.h"
+#include "gr-recipe.h"
+#include "gr-recipe-small-tile.h"
+#include "gr-app.h"
+#include "gr-utils.h"
+#include "gr-ingredients-list.h"
+#include "gr-window.h"
+
+struct _GrShoppingPage
+{
+        GtkBox parent_instance;
+
+        GtkWidget *recipe_count_label;
+        GtkWidget *recipe_list;
+        GtkWidget *ingredients_count_label;
+        GtkWidget *ingredients_list;
+
+        int ingredient_count;
+        int recipe_count;
+        GrRecipeSearch *search;
+
+        GtkSizeGroup *group;
+        GHashTable *ingredients;
+};
+
+G_DEFINE_TYPE (GrShoppingPage, gr_shopping_page, GTK_TYPE_BOX)
+
+static void connect_store_signals (GrShoppingPage *page);
+
+static void
+shopping_page_finalize (GObject *object)
+{
+        GrShoppingPage *self = GR_SHOPPING_PAGE (object);
+
+        g_clear_object (&self->search);
+        g_clear_object (&self->group);
+        g_clear_pointer (&self->ingredients, g_hash_table_unref);
+
+        G_OBJECT_CLASS (gr_shopping_page_parent_class)->finalize (object);
+}
+
+static void
+recount_ingredients (GrShoppingPage *page)
+{
+        GList *children, *l;
+        int count;
+        g_autofree char *tmp = NULL;
+
+        children = gtk_container_get_children (GTK_CONTAINER (page->ingredients_list));
+
+        count = 0;
+        for (l = children; l; l = l->next) {
+                GtkWidget *row = l->data;
+                GtkWidget *check = GTK_WIDGET (g_object_get_data (G_OBJECT (row), "check"));
+
+                if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (check)))
+                        count++;
+        }
+        g_list_free (children);
+
+        tmp = g_strdup_printf (ngettext ("%d ingredient marked for purchase",
+                                         "%d ingredients marked for purchase", count),
+                                         count);
+        gtk_label_set_label (GTK_LABEL (page->ingredients_count_label), tmp);
+}
+
+static void
+recount_recipes (GrShoppingPage *page)
+{
+        GList *children, *l;
+        int count;
+        g_autofree char *tmp = NULL;
+        g_autofree char *tmp2 = NULL;
+        GtkWidget *window;
+
+        children = gtk_container_get_children (GTK_CONTAINER (page->recipe_list));
+
+        count = 0;
+        for (l = children; l; l = l->next) {
+                GtkWidget *tile = gtk_bin_get_child (GTK_BIN (l->data));
+                gboolean active;
+
+                g_object_get (tile, "active", &active, NULL);
+                if (active)
+                        count++;
+        }
+        g_list_free (children);
+
+        window = gtk_widget_get_ancestor (GTK_WIDGET (page), GTK_TYPE_APPLICATION_WINDOW);
+
+        tmp = g_strdup_printf (ngettext ("Cook it later (%d recipe)",
+                                         "Cook it later (%d recipes)", page->recipe_count),
+                               page->recipe_count);
+        gtk_window_set_title (GTK_WINDOW (window), tmp);
+
+        tmp2 = g_strdup_printf (ngettext ("%d Recipe marked for preparation",
+                                          "%d Recipes marked for preparation", count),
+                                count);
+        gtk_label_set_label (GTK_LABEL (page->recipe_count_label), tmp2);
+}
+
+static void
+ing_row_activated (GtkListBox *list,
+                   GtkListBoxRow *row,
+                   GrShoppingPage *page)
+{
+        GtkToggleButton *check = GTK_TOGGLE_BUTTON (g_object_get_data (G_OBJECT (row), "check"));
+        gtk_toggle_button_set_active (check, !gtk_toggle_button_get_active (check));
+}
+
+typedef struct {
+        char *ingredient;
+        char **units;
+} Ingredient;
+
+static void
+ingredient_free (gpointer data)
+{
+        Ingredient *ing = data;
+
+        g_free (ing->ingredient);
+        g_strfreev (ing->units);
+        g_free (ing);
+}
+
+static void
+add_ingredient_row (GrShoppingPage *page,
+                    const char *unit,
+                    const char *ing)
+{
+        GtkWidget *box;
+        GtkWidget *button;
+        GtkWidget *unit_label;
+        GtkWidget *ing_label;
+        GtkWidget *row;
+
+        box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
+        gtk_widget_show (box);
+
+        button = gtk_check_button_new ();
+        gtk_widget_show (button);
+        g_object_set (button, "margin", 10, NULL);
+        gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE);
+        gtk_container_add (GTK_CONTAINER (box), button);
+        g_signal_connect_swapped (button, "toggled", G_CALLBACK (recount_ingredients), page);
+
+        unit_label = gtk_label_new (unit);
+        gtk_widget_show (unit_label);
+        gtk_label_set_xalign (GTK_LABEL (unit_label), 0.0);
+        g_object_set (unit_label, "margin", 10, NULL);
+        gtk_style_context_add_class (gtk_widget_get_style_context (unit_label), "dim-label");
+        gtk_container_add (GTK_CONTAINER (box), unit_label);
+        gtk_size_group_add_widget (page->group, unit_label);
+
+        ing_label = gtk_label_new (ing);
+        gtk_widget_show (ing_label);
+        gtk_label_set_xalign (GTK_LABEL (ing_label), 0.0);
+        g_object_set (ing_label, "margin", 10, NULL);
+        gtk_container_add (GTK_CONTAINER (box), ing_label);
+
+        gtk_container_add (GTK_CONTAINER (page->ingredients_list), box);
+        row = gtk_widget_get_parent (box);
+        g_object_set_data (G_OBJECT (row), "check", button);
+}
+
+static void
+add_ingredient (GrShoppingPage *page,
+                const char *unit,
+                const char *ingredient)
+{
+        Ingredient *ing;
+
+        ing = g_hash_table_lookup (page->ingredients, ingredient);
+        if (ing == NULL) {
+                ing = g_new0 (Ingredient, 1);
+                ing->ingredient = g_strdup (ingredient);
+                ing->units = g_new0 (char *, 2);
+                ing->units[0] = g_strdup (unit);
+                g_hash_table_insert (page->ingredients, g_strdup (ingredient), ing);
+        }
+        else {
+                int len;
+                len = g_strv_length (ing->units);
+                ing->units = g_realloc (ing->units, len + 2);
+                ing->units[len] = g_strdup (unit);
+                ing->units[len + 1] = NULL;
+        }
+}
+
+static void
+collect_ingredients_from_recipe (GrShoppingPage *page,
+                                 GrRecipe       *recipe)
+{
+        g_autoptr(GrIngredientsList) il = NULL;
+        g_autofree char **seg = NULL;
+        int i, j;
+
+        il = gr_ingredients_list_new (gr_recipe_get_ingredients (recipe));
+        seg = gr_ingredients_list_get_segments (il);
+        for (i = 0; seg[i]; i++) {
+                g_auto(GStrv) ing = NULL;
+                ing = gr_ingredients_list_get_ingredients (il, seg[i]);
+                for (j = 0; ing[j]; j++) {
+                        g_autofree char *unit = NULL;
+                        unit = gr_ingredients_list_scale_unit (il, seg[i], ing[j], 1, 1);
+                        add_ingredient (page, unit, ing[j]);
+                }
+        }
+}
+
+static void
+collect_ingredients (GrShoppingPage *page)
+{
+        GList *children, *l;
+        GHashTableIter iter;
+        Ingredient *ing;
+
+        g_hash_table_remove_all (page->ingredients);
+
+        children = gtk_container_get_children (GTK_CONTAINER (page->recipe_list));
+
+        for (l = children; l; l = l->next) {
+                GtkWidget *tile = gtk_bin_get_child (GTK_BIN (l->data));
+                gboolean active;
+
+                g_object_get (tile, "active", &active, NULL);
+                if (active) {
+                        GrRecipe *recipe;
+
+                        recipe = gr_recipe_small_tile_get_recipe (GR_RECIPE_SMALL_TILE (tile));
+                        collect_ingredients_from_recipe (page, recipe);
+                }
+        }
+        g_list_free (children);
+
+        container_remove_all (GTK_CONTAINER (page->ingredients_list));
+        g_hash_table_iter_init (&iter, page->ingredients);
+        while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&ing)) {
+                g_autofree char *unit = NULL;
+                unit = g_strjoinv (", ", ing->units);
+                add_ingredient_row (page, unit, ing->ingredient);
+        }
+}
+
+static void
+recipes_changed (GrShoppingPage *page)
+{
+        collect_ingredients (page);
+        recount_ingredients (page);
+        recount_recipes (page);
+}
+
+static void
+search_started (GrRecipeSearch *search,
+                GrShoppingPage     *page)
+{
+        container_remove_all (GTK_CONTAINER (page->recipe_list));
+        page->recipe_count = 0;
+}
+
+static void
+search_hits_added (GrRecipeSearch *search,
+                   GList          *hits,
+                   GrShoppingPage *page)
+{
+        GList *l;
+
+        for (l = hits; l; l = l->next) {
+                GrRecipe *recipe = l->data;
+                GtkWidget *tile;
+                tile = gr_recipe_small_tile_new (recipe);
+                g_object_set (tile, "active", TRUE, NULL);
+                g_signal_connect_swapped (tile, "notify::active", G_CALLBACK (recipes_changed), page);
+                gtk_container_add (GTK_CONTAINER (page->recipe_list), tile);
+                page->recipe_count++;
+        }
+}
+
+static void
+search_hits_removed (GrRecipeSearch *search,
+                     GList          *hits,
+                     GrShoppingPage *page)
+{
+        GList *children, *l;
+
+        children = gtk_container_get_children (GTK_CONTAINER (page->recipe_list));
+        for (l = children; l; l = l->next) {
+                GtkWidget *item = l->data;
+                GtkWidget *tile;
+                GrRecipe *recipe;
+
+                tile = gtk_bin_get_child (GTK_BIN (item));
+                recipe = gr_recipe_small_tile_get_recipe (GR_RECIPE_SMALL_TILE (tile));
+                if (g_list_find (hits, recipe)) {
+                        gtk_container_remove (GTK_CONTAINER (page->recipe_list), item);
+                        page->recipe_count--;
+                }
+        }
+}
+
+static void
+search_finished (GrRecipeSearch *search,
+                 GrShoppingPage *page)
+{
+        collect_ingredients (page);
+        recount_ingredients (page);
+        recount_recipes (page);
+}
+
+static void
+clear_list (GrShoppingPage *page)
+{
+        GList *children, *l;
+        GrRecipeStore *store;
+        GtkWidget *window;
+        GList *recipes;
+
+        store = gr_app_get_recipe_store (GR_APP (g_application_get_default ()));
+
+        children = gtk_container_get_children (GTK_CONTAINER (page->recipe_list));
+        recipes = NULL;
+        for (l = children; l; l = l->next) {
+                GtkWidget *tile = gtk_bin_get_child (GTK_BIN (l->data));
+                GrRecipe *recipe = gr_recipe_small_tile_get_recipe (GR_RECIPE_SMALL_TILE (tile));
+                recipes = g_list_prepend (recipes, g_object_ref (recipe));
+        }
+        g_list_free (children);
+
+        container_remove_all (GTK_CONTAINER (page->ingredients_list));
+        container_remove_all (GTK_CONTAINER (page->recipe_list));
+
+        for (l = recipes; l; l = l->next) {
+                GrRecipe *recipe = l->data;
+                gr_recipe_store_remove_from_shopping (store, recipe);
+        }
+        g_list_free_full (recipes, g_object_unref);
+
+        window = gtk_widget_get_ancestor (GTK_WIDGET (page), GTK_TYPE_APPLICATION_WINDOW);
+        gr_window_go_back (GR_WINDOW (window));
+}
+
+static void
+all_headers (GtkListBoxRow *row,
+             GtkListBoxRow *before,
+             gpointer       user_data)
+{
+        GtkWidget *header;
+
+        header = gtk_list_box_row_get_header (row);
+        if (header)
+                return;
+
+        header = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
+        gtk_list_box_row_set_header (row, header);
+}
+
+static void
+gr_shopping_page_init (GrShoppingPage *page)
+{
+        gtk_widget_set_has_window (GTK_WIDGET (page), FALSE);
+        gtk_widget_init_template (GTK_WIDGET (page));
+        connect_store_signals (page);
+
+        page->search = gr_recipe_search_new ();
+        g_signal_connect (page->search, "started", G_CALLBACK (search_started), page);
+        g_signal_connect (page->search, "hits-added", G_CALLBACK (search_hits_added), page);
+        g_signal_connect (page->search, "hits-removed", G_CALLBACK (search_hits_removed), page);
+        g_signal_connect (page->search, "finished", G_CALLBACK (search_finished), page);
+
+        page->group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+        page->ingredients = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, ingredient_free);
+
+        gtk_list_box_set_header_func (GTK_LIST_BOX (page->ingredients_list),
+                                      all_headers, page, NULL);
+
+        g_signal_connect (page->ingredients_list, "row-activated", G_CALLBACK (ing_row_activated), page);
+}
+
+static void
+gr_shopping_page_class_init (GrShoppingPageClass *klass)
+{
+        GObjectClass *object_class = G_OBJECT_CLASS (klass);
+        GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+        object_class->finalize = shopping_page_finalize;
+
+        gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Recipes/gr-shopping-page.ui");
+
+        gtk_widget_class_bind_template_child (widget_class, GrShoppingPage, recipe_count_label);
+        gtk_widget_class_bind_template_child (widget_class, GrShoppingPage, recipe_list);
+        gtk_widget_class_bind_template_child (widget_class, GrShoppingPage, ingredients_count_label);
+        gtk_widget_class_bind_template_child (widget_class, GrShoppingPage, ingredients_list);
+
+        gtk_widget_class_bind_template_callback (widget_class, clear_list);
+}
+
+GtkWidget *
+gr_shopping_page_new (void)
+{
+        GrShoppingPage *page;
+
+        page = g_object_new (GR_TYPE_SHOPPING_PAGE, NULL);
+
+        return GTK_WIDGET (page);
+}
+
+void
+gr_shopping_page_populate (GrShoppingPage *self)
+{
+        container_remove_all (GTK_CONTAINER (self->recipe_list));
+        gr_recipe_search_stop (self->search);
+        gr_recipe_search_set_query (self->search, "is:shopping");
+}
+
+static void
+connect_store_signals (GrShoppingPage *page)
+{
+        GrRecipeStore *store;
+
+        store = gr_app_get_recipe_store (GR_APP (g_application_get_default ()));
+
+        /* FIXME: inefficient */
+        g_signal_connect_swapped (store, "recipe-added", G_CALLBACK (gr_shopping_page_populate), page);
+        g_signal_connect_swapped (store, "recipe-removed", G_CALLBACK (gr_shopping_page_populate), page);
+        g_signal_connect_swapped (store, "recipe-changed", G_CALLBACK (gr_shopping_page_populate), page);
+}
diff --git a/src/gr-shopping-page.h b/src/gr-shopping-page.h
new file mode 100644
index 0000000..6992d63
--- /dev/null
+++ b/src/gr-shopping-page.h
@@ -0,0 +1,37 @@
+/* gr-shopping-page.h:
+ *
+ * Copyright (C) 2017 Matthias Clasen <mclasen redhat com>
+ *
+ * Licensed under the GNU General Public License Version 3
+ *
+ * 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
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include "gr-recipe.h"
+#include "gr-chef.h"
+
+G_BEGIN_DECLS
+
+#define GR_TYPE_SHOPPING_PAGE (gr_shopping_page_get_type ())
+
+G_DECLARE_FINAL_TYPE (GrShoppingPage, gr_shopping_page, GR, SHOPPING_PAGE, GtkBox)
+
+GtkWidget      *gr_shopping_page_new      (void);
+
+void            gr_shopping_page_populate (GrShoppingPage *self);
+
+G_END_DECLS
diff --git a/src/gr-shopping-page.ui b/src/gr-shopping-page.ui
new file mode 100644
index 0000000..dfa140b
--- /dev/null
+++ b/src/gr-shopping-page.ui
@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface domain="recipes">
+  <!-- interface-requires gtk+ 3.8 -->
+  <template class="GrShoppingPage" parent="GtkBox">
+    <property name="visible">True</property>
+    <property name="orientation">vertical</property>
+    <child>
+      <object class="GtkScrolledWindow">
+        <property name="visible">1</property>
+        <property name="expand">1</property>
+        <property name="hscrollbar-policy">never</property>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">1</property>
+            <property name="orientation">vertical</property>
+            <property name="halign">center</property>
+            <property name="margin-start">80</property>
+            <property name="margin-end">80</property>
+            <child>
+              <object class="GtkBox">
+                <property name="visible">1</property>
+                <property name="orientation">horizontal</property>
+                <property name="spacing">20</property>
+                <property name="margin-top">20</property>
+                <property name="margin-bottom">20</property>
+                <child>
+                  <object class="GtkImage">
+                    <property name="visible">1</property>
+                    <property name="valign">end</property>
+                    <property name="icon-name">shoppingcart-symbolic</property>
+                    <property name="pixel-size">48</property>
+                    <style>
+                      <class name="heading"/>
+                    </style>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="visible">1</property>
+                    <property name="xalign">0</property>
+                    <property name="valign">end</property>
+                    <property name="label" translatable="yes">Cook it Later</property>
+                    <style>
+                      <class name="heading"/>
+                    </style>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="recipe_count_label">
+                <property name="visible">1</property>
+                <property name="xalign">0</property>
+                <property name="margin-bottom">10</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkFlowBox" id="recipe_list">
+                <property name="visible">1</property>
+                <property name="halign">center</property>
+                <property name="valign">start</property>
+                <property name="row-spacing">20</property>
+                <property name="column-spacing">20</property>
+                <property name="margin-bottom">20</property>
+                <property name="homogeneous">1</property>
+                <property name="min-children-per-line">3</property>
+                <property name="max-children-per-line">3</property>
+                <property name="selection-mode">none</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="ingredients_count_label">
+                <property name="visible">1</property>
+                <property name="xalign">0</property>
+                <property name="margin-bottom">10</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkListBox" id="ingredients_list">
+                <property name="visible">1</property>
+                <property name="selection-mode">none</property>
+                <property name="margin-bottom">20</property>
+                <style>
+                  <class name="frame"/>
+                </style>
+                <child type="placeholder">
+                  <object class="GtkLabel">
+                    <property name="visible">1</property>
+                    <property name="margin-start">20</property>
+                    <property name="margin-end">20</property>
+                    <property name="margin-top">10</property>
+                    <property name="margin-bottom">10</property>
+                    <property name="label" translatable="yes">No shopping necessary!</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+    <child>
+      <object class="GtkActionBar">
+        <property name="visible">1</property>
+        <child>
+          <object class="GtkButton">
+            <property name="visible">1</property>
+            <property name="use-underline">1</property>
+            <property name="label" translatable="yes">_Edit</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkButton">
+            <property name="visible">1</property>
+            <property name="use-underline">1</property>
+            <property name="label" translatable="yes">_Clear List</property>
+            <signal name="clicked" handler="clear_list" swapped="yes"/>
+            <style>
+              <class name="destructive-action"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkButton">
+            <property name="visible">1</property>
+            <property name="use-underline">1</property>
+            <property name="label" translatable="yes">E_xport</property>
+          </object>
+          <packing>
+            <property name="pack-type">end</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton">
+            <property name="visible">1</property>
+            <property name="use-underline">1</property>
+            <property name="label" translatable="yes">_Print</property>
+          </object>
+          <packing>
+            <property name="pack-type">end</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/gr-window.c b/src/gr-window.c
index 35b5f50..93e7b9e 100644
--- a/src/gr-window.c
+++ b/src/gr-window.c
@@ -30,6 +30,7 @@
 #include "gr-list-page.h"
 #include "gr-cuisine-page.h"
 #include "gr-search-page.h"
+#include "gr-shopping-page.h"
 #include "gr-recipes-page.h"
 #include "gr-cuisines-page.h"
 #include "gr-ingredients-page.h"
@@ -53,6 +54,7 @@ struct _GrWindow
         GtkWidget *details_page;
         GtkWidget *edit_page;
         GtkWidget *list_page;
+        GtkWidget *shopping_page;
         GtkWidget *search_page;
         GtkWidget *cuisines_page;
         GtkWidget *cuisine_page;
@@ -430,6 +432,7 @@ gr_window_class_init (GrWindowClass *klass)
         gtk_widget_class_bind_template_child (widget_class, GrWindow, details_page);
         gtk_widget_class_bind_template_child (widget_class, GrWindow, edit_page);
         gtk_widget_class_bind_template_child (widget_class, GrWindow, list_page);
+        gtk_widget_class_bind_template_child (widget_class, GrWindow, shopping_page);
         gtk_widget_class_bind_template_child (widget_class, GrWindow, search_page);
         gtk_widget_class_bind_template_child (widget_class, GrWindow, cuisines_page);
         gtk_widget_class_bind_template_child (widget_class, GrWindow, cuisine_page);
@@ -644,15 +647,14 @@ gr_window_show_shopping (GrWindow *window)
 {
         save_back_entry (window);
 
-        gr_list_page_populate_from_shopping (GR_LIST_PAGE (window->list_page));
-
-        gtk_header_bar_set_title (GTK_HEADER_BAR (window->header), _("Shopping list"));
+        gtk_header_bar_set_title (GTK_HEADER_BAR (window->header), _("Cook it later"));
+        gr_shopping_page_populate (GR_SHOPPING_PAGE (window->shopping_page));
 
         gtk_stack_set_visible_child_name (GTK_STACK (window->header_start_stack), "back");
         gtk_stack_set_visible_child_name (GTK_STACK (window->header_title_stack), "title");
         gtk_stack_set_visible_child_name (GTK_STACK (window->header_end_stack), "list");
 
-        gtk_stack_set_visible_child_name (GTK_STACK (window->main_stack), "list");
+        gtk_stack_set_visible_child_name (GTK_STACK (window->main_stack), "shopping");
 }
 
 void
diff --git a/src/gr-window.ui b/src/gr-window.ui
index 7d2ae69..f9c9fe5 100644
--- a/src/gr-window.ui
+++ b/src/gr-window.ui
@@ -237,6 +237,12 @@
                   </packing>
                 </child>
                 <child>
+                  <object class="GrShoppingPage" id="shopping_page"/>
+                  <packing>
+                    <property name="name">shopping</property>
+                  </packing>
+                </child>
+                <child>
                   <object class="GrSearchPage" id="search_page"/>
                   <packing>
                     <property name="name">search</property>
diff --git a/src/icons/16x16/apps/shoppingcart-symbolic.symbolic.png 
b/src/icons/16x16/apps/shoppingcart-symbolic.symbolic.png
new file mode 100644
index 0000000..bd03ff3
Binary files /dev/null and b/src/icons/16x16/apps/shoppingcart-symbolic.symbolic.png differ
diff --git a/src/icons/24x24/apps/shoppingcart-symbolic.symbolic.png 
b/src/icons/24x24/apps/shoppingcart-symbolic.symbolic.png
new file mode 100644
index 0000000..639e4dd
Binary files /dev/null and b/src/icons/24x24/apps/shoppingcart-symbolic.symbolic.png differ
diff --git a/src/main.c b/src/main.c
index 3062805..6b9bc84 100644
--- a/src/main.c
+++ b/src/main.c
@@ -34,6 +34,7 @@
 #include "gr-query-editor.h"
 #include "gr-recipes-page.h"
 #include "gr-search-page.h"
+#include "gr-shopping-page.h"
 #include "gr-timer-widget.h"
 #include "gr-toggle-button.h"
 #include "gr-image-viewer.h"
@@ -55,6 +56,7 @@ main (int argc, char *argv[])
         g_type_ensure (GR_TYPE_QUERY_EDITOR);
         g_type_ensure (GR_TYPE_RECIPES_PAGE);
         g_type_ensure (GR_TYPE_SEARCH_PAGE);
+        g_type_ensure (GR_TYPE_SHOPPING_PAGE);
         g_type_ensure (GR_TYPE_TIMER_WIDGET);
         g_type_ensure (GR_TYPE_TOGGLE_BUTTON);
 
diff --git a/src/recipes-images.gresource.xml b/src/recipes-images.gresource.xml
index 11e06f2..f160b6e 100644
--- a/src/recipes-images.gresource.xml
+++ b/src/recipes-images.gresource.xml
@@ -40,7 +40,9 @@
     <file>icons/48x48/apps/wheat-content-symbolic.symbolic.png</file>
     <file>icons/symbolic/apps/wheat-content-symbolic.svg</file>
     <file>icons/16x16/apps/shoppingcart.png</file>
+    <file>icons/16x16/apps/shoppingcart-symbolic.symbolic.png</file>
     <file>icons/24x24/apps/shoppingcart.png</file>
+    <file>icons/24x24/apps/shoppingcart-symbolic.symbolic.png</file>
     <file>built-with-builder.png</file>
   </gresource>
 </gresources>
diff --git a/src/recipes-ui.gresource.xml b/src/recipes-ui.gresource.xml
index 78a5eff..b767462 100644
--- a/src/recipes-ui.gresource.xml
+++ b/src/recipes-ui.gresource.xml
@@ -21,6 +21,7 @@
     <file preprocess="xml-stripblanks">gr-recipe-small-tile.ui</file>
     <file preprocess="xml-stripblanks">gr-recipe-tile.ui</file>
     <file preprocess="xml-stripblanks">gr-search-page.ui</file>
+    <file preprocess="xml-stripblanks">gr-shopping-page.ui</file>
     <file preprocess="xml-stripblanks">gr-query-editor.ui</file>
     <file preprocess="xml-stripblanks">gr-window.ui</file>
     <file preprocess="xml-stripblanks">chef-conflict-dialog.ui</file>
diff --git a/src/recipes.css b/src/recipes.css
index a4b0a52..5978d77 100644
--- a/src/recipes.css
+++ b/src/recipes.css
@@ -1,5 +1,5 @@
 /* headings on the recipes page */
-label.heading {
+.heading {
        font: 16px Cantarell;
        font-weight: bold;
   padding: 20px 0 10px 0;
@@ -75,11 +75,13 @@ label.chef.name {
   border: 1px solid @selected_borders;
 }
 
-.favorites.view.tile label {
+.favorites.view.tile label,
+.favorites.view.tile image {
   color: @theme_selected_fg_color;
 }
 
-.favorites.view.tile label:backdrop {
+.favorites.view.tile label:backdrop,
+.favorites.view.tile image:backdrop {
   color: @theme_unfocused_selected_fg_color;
 }
 
@@ -92,6 +94,11 @@ label.chef.name {
   min-height: 200px;
 }
 
+.recipe.view.small.tile {
+  min-width: 258px;
+  min-height: 64px;
+}
+
 label.recipe.name {
   background: alpha(@theme_base_color, .75);
   color: @theme_fg_color;
@@ -107,6 +114,11 @@ label.recipe.author {
   padding: 8px 20px 10px 20px;
 }
 
+label.recipe.author.small,
+label.recipe.name.small {
+  background: @theme_base_color;
+}
+
 @define-color diet_tag_bg #cc1212;
 @define-color diet_tag_fg #ffffff;
 
@@ -219,3 +231,27 @@ image.very-spicy {
 .preview-list {
        background: black;
 }
+
+.favorite-button:hover,
+.favorite-button:checked {
+  color: red;
+}
+
+.shop-button:hover,
+.shop-button:checked {
+  color: shade(green, 1.1);
+}
+
+.favorite-button:not(:hover),
+.shop-button:not(:hover) {
+  background-image: none;
+  background-color: transparent;
+  border-color: transparent;
+  box-shadow: none;
+}
+
+.favorite-button:backdrop,
+.shop-button:backdrop {
+  background: transparent;
+}
+



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