[recipes] Add support for recipe links in instructions
- From: Matthias Clasen <matthiasc src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [recipes] Add support for recipe links in instructions
- Date: Fri, 6 Jan 2017 11:35:13 +0000 (UTC)
commit 8b05d725232d600f1c50188900cd81c66ee39c07
Author: Matthias Clasen <mclasen redhat com>
Date: Thu Dec 29 00:28:03 2016 -0500
Add support for recipe links in instructions
Add a simple button to insert a recipe link, and format the link
nicely.
src/gr-edit-page.c | 372 ++++++++++++++++++++++++++++++++++++++++++++++++++-
src/gr-edit-page.ui | 66 +++++++++-
2 files changed, 435 insertions(+), 3 deletions(-)
---
diff --git a/src/gr-edit-page.c b/src/gr-edit-page.c
index a1725c4..6c2b721 100644
--- a/src/gr-edit-page.c
+++ b/src/gr-edit-page.c
@@ -98,6 +98,12 @@ struct _GrEditPage
GtkWidget *amount_search_button;
GtkWidget *amount_search_button_label;
GtkWidget *amount_search_revealer;
+ GtkWidget *recipe_popover;
+ GtkWidget *recipe_list;
+ GtkWidget *recipe_filter_entry;
+ GtkWidget *add_recipe_button;
+
+ GrRecipeSearch *search;
GtkSizeGroup *group;
@@ -108,6 +114,7 @@ struct _GrEditPage
char *ing_term;
char *unit_term;
+ char *recipe_term;
};
G_DEFINE_TYPE (GrEditPage, gr_edit_page, GTK_TYPE_BOX)
@@ -167,6 +174,9 @@ edit_page_finalize (GObject *object)
g_free (self->ing_term);
g_free (self->unit_term);
+ g_free (self->recipe_term);
+
+ g_clear_object (&self->search);
G_OBJECT_CLASS (gr_edit_page_parent_class)->finalize (object);
}
@@ -830,6 +840,249 @@ populate_units_list (GrEditPage *self)
G_CALLBACK (unit_row_activated), self);
}
+static gboolean
+recipe_filter_func (GtkListBoxRow *row,
+ gpointer data)
+{
+ GrEditPage *self = data;
+ const char *cf;
+
+ if (!self->recipe_term)
+ return TRUE;
+
+ cf = (const char *)g_object_get_data (G_OBJECT (row), "term");
+
+ return g_str_has_prefix (cf, self->recipe_term);
+}
+
+static void
+recipe_filter_changed (GrEditPage *self)
+{
+ const char *term;
+
+ term = gtk_entry_get_text (GTK_ENTRY (self->recipe_filter_entry));
+ g_free (self->recipe_term);
+ self->recipe_term = g_utf8_casefold (term, -1);
+ gtk_list_box_invalidate_filter (GTK_LIST_BOX (self->recipe_list));
+}
+
+static void
+recipe_filter_stop (GrEditPage *self)
+{
+}
+
+static void
+recipe_row_activated (GtkListBox *list,
+ GtkListBoxRow *row,
+ GrEditPage *self)
+{
+ GrRecipe *recipe;
+ const char *id;
+ const char *name;
+ GtkTextBuffer *buffer;
+ GtkTextIter iter;
+ GtkTextTag *tag;
+ GdkRGBA color;
+
+ gdk_rgba_parse (&color, "blue");
+
+ recipe = GR_RECIPE (g_object_get_data (G_OBJECT (row), "recipe"));
+ id = gr_recipe_get_id (recipe);
+ name = gr_recipe_get_name (recipe);
+
+ buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->instructions_field));
+ tag = gtk_text_buffer_create_tag (buffer, NULL,
+ "foreground-rgba", &color,
+ "underline", PANGO_UNDERLINE_SINGLE,
+ NULL);
+ g_object_set_data_full (G_OBJECT (tag), "recipe-id", g_strdup (id), g_free);
+
+ gtk_text_buffer_get_iter_at_mark (buffer, &iter,
+ gtk_text_buffer_get_insert (buffer));
+
+ gtk_text_buffer_insert_with_tags (buffer, &iter, name, -1, tag, NULL);
+
+ gtk_popover_popdown (GTK_POPOVER (self->recipe_popover));
+ gtk_entry_set_text (GTK_ENTRY (self->recipe_filter_entry), "");
+
+ gtk_widget_grab_focus (self->instructions_field);
+}
+
+static void
+recipe_filter_activated (GrEditPage *self)
+{
+ GtkListBoxRow *row;
+
+ row = gtk_list_box_get_row_at_index (GTK_LIST_BOX (self->recipe_list), 0);
+ if (row) {
+ recipe_row_activated (GTK_LIST_BOX (self->recipe_list), row, self);
+ }
+}
+
+static void
+search_started (GrRecipeSearch *search,
+ GrEditPage *page)
+{
+}
+
+static void
+search_hits_added (GrRecipeSearch *search,
+ GList *hits,
+ GrEditPage *page)
+{
+ GList *l;
+ GrRecipeStore *store;
+
+ store = gr_app_get_recipe_store (GR_APP (g_application_get_default ()));
+
+ for (l = hits; l; l = l->next) {
+ GrRecipe *recipe = l->data;
+ GtkWidget *box;
+ GtkWidget *label;
+ GtkWidget *row;
+ const char *author;
+ char *tmp;
+ g_autoptr(GrChef) chef = NULL;
+
+ box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 20);
+ gtk_widget_show (box);
+ g_object_set (box,
+ "margin-start", 20,
+ "margin-end", 20,
+ "margin-top", 10,
+ "margin-bottom", 10,
+ NULL);
+
+ label = gtk_label_new (gr_recipe_get_name (recipe));
+ gtk_label_set_xalign (GTK_LABEL (label), 0.0);
+ gtk_widget_show (label);
+ gtk_box_pack_start (GTK_BOX (box), label, TRUE, TRUE, 0);
+
+ author = gr_recipe_get_author (recipe);
+ chef = gr_recipe_store_get_chef (store, author ? author : "");
+ if (chef) {
+ tmp = g_strdup_printf (_("by %s"), gr_chef_get_name (chef));
+ label = gtk_label_new (tmp);
+ g_free (tmp);
+ gtk_label_set_xalign (GTK_LABEL (label), 1.0);
+ gtk_widget_show (label);
+ gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label");
+ gtk_box_pack_start (GTK_BOX (box), label, FALSE, TRUE, 0);
+ }
+
+ gtk_container_add (GTK_CONTAINER (page->recipe_list), box);
+ row = gtk_widget_get_parent (box);
+ g_object_set_data_full (G_OBJECT (row), "recipe", g_object_ref (recipe), g_object_unref);
+ g_object_set_data_full (G_OBJECT (row), "term", g_utf8_casefold (gr_recipe_get_name
(recipe), -1), g_free);
+ }
+}
+
+static void
+search_hits_removed (GrRecipeSearch *search,
+ GList *hits,
+ GrEditPage *page)
+{
+ GList *children, *l;
+
+ children = gtk_container_get_children (GTK_CONTAINER (page->recipe_list));
+ for (l = children; l; l = l->next) {
+ GtkWidget *row = l->data;
+ GrRecipe *recipe;
+
+ recipe = GR_RECIPE (g_object_get_data (G_OBJECT (row), "recipe"));
+ if (g_list_find (hits, recipe)) {
+ gtk_container_remove (GTK_CONTAINER (page->recipe_list), row);
+ }
+ }
+}
+
+static void
+search_finished (GrRecipeSearch *search,
+ GrEditPage *page)
+{
+}
+
+static void
+cursor_moved (GObject *object,
+ GParamSpec *pspec,
+ GrEditPage *page)
+{
+ GtkTextBuffer *buffer = GTK_TEXT_BUFFER (object);
+ GtkTextIter iter;
+ GSList *tags, *s;
+ gboolean in_recipe = FALSE;
+
+ gtk_text_buffer_get_iter_at_mark (buffer, &iter,
+ gtk_text_buffer_get_insert (buffer));
+
+ tags = gtk_text_iter_get_tags (&iter);
+ for (s = tags; s; s = s->next) {
+ GtkTextTag *tag = s->data;
+
+ if (g_object_get_data (G_OBJECT (tag), "recipe-id") != NULL) {
+ in_recipe = TRUE;
+ break;
+ }
+ }
+ g_slist_free (tags);
+
+ gtk_widget_set_sensitive (page->add_recipe_button, !in_recipe);
+}
+
+static void
+populate_recipe_list (GrEditPage *self)
+{
+ GtkTextBuffer *buffer;
+
+ gtk_list_box_set_header_func (GTK_LIST_BOX (self->recipe_list),
+ all_headers, self, NULL);
+
+ gtk_list_box_set_filter_func (GTK_LIST_BOX (self->recipe_list),
+ recipe_filter_func, self, NULL);
+
+ g_signal_connect (self->recipe_list, "row-activated",
+ G_CALLBACK (recipe_row_activated), self);
+
+ self->search = gr_recipe_search_new ();
+ g_signal_connect (self->search, "started", G_CALLBACK (search_started), self);
+ g_signal_connect (self->search, "hits-added", G_CALLBACK (search_hits_added), self);
+ g_signal_connect (self->search, "hits-removed", G_CALLBACK (search_hits_removed), self);
+ g_signal_connect (self->search, "finished", G_CALLBACK (search_finished), self);
+
+ buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->instructions_field));
+ g_signal_connect (buffer, "notify::cursor-position", G_CALLBACK (cursor_moved), self);
+}
+
+static void
+add_recipe_link (GtkButton *button, GrEditPage *page)
+{
+ if (gtk_list_box_get_row_at_index (GTK_LIST_BOX (page->recipe_list), 0) == NULL) {
+ gr_recipe_search_stop (page->search);
+ gr_recipe_search_set_query (page->search, "na:");
+ }
+
+ gtk_entry_set_text (GTK_ENTRY (page->recipe_filter_entry), "");
+ gtk_popover_popup (GTK_POPOVER (page->recipe_popover));
+}
+
+static void
+recipe_reload (GrEditPage *page)
+{
+ container_remove_all (GTK_CONTAINER (page->recipe_list));
+}
+
+static void
+connect_store_signals (GrEditPage *page)
+{
+ GrRecipeStore *store;
+
+ store = gr_app_get_recipe_store (GR_APP (g_application_get_default ()));
+
+ g_signal_connect_swapped (store, "recipe-added", G_CALLBACK (recipe_reload), page);
+ g_signal_connect_swapped (store, "recipe-removed", G_CALLBACK (recipe_reload), page);
+ g_signal_connect_swapped (store, "recipe-changed", G_CALLBACK (recipe_reload), page);
+}
+
static void
gr_edit_page_init (GrEditPage *page)
{
@@ -843,6 +1096,8 @@ gr_edit_page_init (GrEditPage *page)
populate_season_combo (page);
populate_ingredients_list (page);
populate_units_list (page);
+ populate_recipe_list (page);
+ connect_store_signals (page);
#ifdef ENABLE_GSPELL
{
@@ -942,6 +1197,10 @@ gr_edit_page_class_init (GrEditPageClass *klass)
gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GrEditPage, amount_search_button);
gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GrEditPage,
amount_search_button_label);
gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GrEditPage, amount_search_revealer);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GrEditPage, recipe_popover);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GrEditPage, recipe_list);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GrEditPage, recipe_filter_entry);
+ gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (klass), GrEditPage, add_recipe_button);
gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (klass), dismiss_error);
gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (klass), add_image);
@@ -963,6 +1222,11 @@ gr_edit_page_class_init (GrEditPageClass *klass)
gtk_widget_class_bind_template_callback (widget_class, unit_filter_activated);
gtk_widget_class_bind_template_callback (widget_class, amount_search_button_clicked);
gtk_widget_class_bind_template_callback (widget_class, popover_keypress_handler);
+ gtk_widget_class_bind_template_callback (widget_class, add_recipe_link);
+
+ gtk_widget_class_bind_template_callback (widget_class, recipe_filter_changed);
+ gtk_widget_class_bind_template_callback (widget_class, recipe_filter_stop);
+ gtk_widget_class_bind_template_callback (widget_class, recipe_filter_activated);
}
GtkWidget *
@@ -1291,6 +1555,60 @@ gr_edit_page_clear (GrEditPage *page)
g_clear_object (&page->recipe);
}
+static void
+set_instructions (GtkTextView *text_view,
+ const char *text)
+{
+ GtkTextBuffer *buffer;
+ GtkTextIter iter;
+ GtkTextTag *tag;
+ const char *p;
+ const char *p1, *p2, *q1, *q2, *r1, *r2;
+ GdkRGBA color;
+
+ gdk_rgba_parse (&color, "blue");
+ buffer = gtk_text_view_get_buffer (text_view);
+ gtk_text_buffer_set_text (buffer, "", -1);
+
+ p = text;
+ while (*p) {
+ q1 = NULL;
+ r1 = NULL;
+ p1 = strstr (p, "<a href=\"recipe:");
+ if (!p1)
+ break;
+ p2 = p1 + strlen ("<a href=\"recipe:");
+ q1 = strstr (p2, "\">");
+
+ if (!q1)
+ break;
+
+ q2 = q1 + strlen ("\">");
+ r1 = strstr (q2, "</a>");
+
+ if (!r1)
+ break;
+
+ r2 = r1 + strlen ("</a>");
+
+ gtk_text_buffer_get_end_iter (buffer, &iter);
+ gtk_text_buffer_insert (buffer, &iter, p, p1 - p);
+
+ tag = gtk_text_buffer_create_tag (buffer, NULL,
+ "foreground-rgba", &color,
+ "underline", PANGO_UNDERLINE_SINGLE,
+ NULL);
+ g_object_set_data_full (G_OBJECT (tag), "recipe-id", g_strndup (p2, q1 - p2), g_free);
+ gtk_text_buffer_get_end_iter (buffer, &iter);
+ gtk_text_buffer_insert_with_tags (buffer, &iter, q2, r1 - q2, tag, NULL);
+
+ p = r2;
+ }
+
+ gtk_text_buffer_get_end_iter (buffer, &iter);
+ gtk_text_buffer_insert (buffer, &iter, p, -1);
+}
+
void
gr_edit_page_edit (GrEditPage *page,
GrRecipe *recipe)
@@ -1343,7 +1661,7 @@ gr_edit_page_edit (GrEditPage *page,
set_spiciness (page, spiciness);
gtk_spin_button_set_value (GTK_SPIN_BUTTON (page->serves_spin), serves);
set_text_view_text (GTK_TEXT_VIEW (page->description_field), description);
- set_text_view_text (GTK_TEXT_VIEW (page->instructions_field), instructions);
+ set_instructions (GTK_TEXT_VIEW (page->instructions_field), instructions);
populate_ingredients (page, ingredients);
@@ -1574,6 +1892,56 @@ ensure_user_chef (GrRecipeStore *store,
gtk_window_export_handle (GTK_WINDOW (window), window_handle_exported, page);
}
+static char *
+get_instructions (GtkTextView *text_view)
+{
+ GtkTextBuffer *buffer;
+ GtkTextIter start, end;
+ GString *s;
+ g_autofree char *last_text = NULL;
+
+ s = g_string_new ("");
+
+ buffer = gtk_text_view_get_buffer (text_view);
+ gtk_text_buffer_get_start_iter (buffer, &start);
+ end = start;
+ while (gtk_text_iter_forward_to_tag_toggle (&end, NULL)) {
+ g_autofree char *text = NULL;
+ GSList *tags, *l;
+ GtkTextTag *tag;
+
+ text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
+ g_string_append (s, text);
+ tags = gtk_text_iter_get_tags (&end);
+ tag = NULL;
+ for (l = tags; l; l = l->next) {
+ if (g_object_get_data (G_OBJECT (l->data), "recipe-id")) {
+ tag = l->data;
+ break;
+ }
+ }
+ g_slist_free (tags);
+
+ if (tag) {
+ g_autofree char *name = NULL;
+ start = end;
+ gtk_text_iter_forward_to_tag_toggle (&end, tag);
+ name = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
+ g_string_append_printf (s, "<a href=\"recipe:%s\">", (const char*)g_object_get_data
(G_OBJECT (tag), "recipe-id"));
+ g_string_append (s, name);
+ g_string_append (s, "</a>");
+
+ start = end;
+ }
+ }
+
+ gtk_text_buffer_get_end_iter (buffer, &end);
+ last_text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
+ g_string_append (s, last_text);
+
+ return g_string_free (s, FALSE);
+}
+
gboolean
gr_edit_page_save (GrEditPage *page)
{
@@ -1606,7 +1974,7 @@ gr_edit_page_save (GrEditPage *page)
serves = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (page->serves_spin));
ingredients = collect_ingredients (page);
description = get_text_view_text (GTK_TEXT_VIEW (page->description_field));
- instructions = get_text_view_text (GTK_TEXT_VIEW (page->instructions_field));
+ instructions = get_instructions (GTK_TEXT_VIEW (page->instructions_field));
diets = (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (page->gluten_free_check)) ?
GR_DIET_GLUTEN_FREE : 0) |
(gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (page->nut_free_check)) ? GR_DIET_NUT_FREE
: 0) |
(gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (page->vegan_check)) ? GR_DIET_VEGAN : 0) |
diff --git a/src/gr-edit-page.ui b/src/gr-edit-page.ui
index 433ddb4..865ed67 100644
--- a/src/gr-edit-page.ui
+++ b/src/gr-edit-page.ui
@@ -600,6 +600,31 @@
</packing>
</child>
<child>
+ <object class="GtkButton" id="add_recipe_button">
+ <property name="visible">1</property>
+ <property name="halign">start</property>
+ <property name="tooltip-text" translatable="yes">Add Recipe link</property>
+ <property name="margin-top">10</property>
+ <signal name="clicked" handler="add_recipe_link"/>
+ <style>
+ <class name="dim-label"/>
+ <class name="image-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">1</property>
+ <property name="icon-name">insert-link-symbolic</property>
+ <property name="icon-size">1</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left-attach">0</property>
+ <property name="top-attach">13</property>
+ <property name="width">2</property>
+ </packing>
+ </child>
+ <child>
<object class="GtkLabel" id="author_label">
<property name="visible">1</property>
<property name="xalign">0</property>
@@ -607,7 +632,7 @@
</object>
<packing>
<property name="left-attach">0</property>
- <property name="top-attach">13</property>
+ <property name="top-attach">14</property>
<property name="width">2</property>
</packing>
</child>
@@ -809,4 +834,43 @@
</object>
</child>
</object>
+ <object class="GtkPopover" id="recipe_popover">
+ <property name="position">top</property>
+ <property name="relative-to">add_recipe_button</property>
+ <property name="modal">1</property>
+ <property name="constrain-to">none</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">1</property>
+ <property name="margin">12</property>
+ <property name="spacing">12</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkSearchEntry" id="recipe_filter_entry">
+ <property name="visible">1</property>
+ <property name="placeholder-text" translatable="yes">Search…</property>
+ <signal name="search-changed" handler="recipe_filter_changed" swapped="yes"/>
+ <signal name="stop-search" handler="recipe_filter_stop" swapped="yes"/>
+ <signal name="activate" handler="recipe_filter_activated" swapped="yes"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="visible">1</property>
+ <property name="shadow-type">in</property>
+ <property name="hscrollbar-policy">never</property>
+ <property name="propagate-natural-height">1</property>
+ <property name="max-content-height">220</property>
+ <child>
+ <object class="GtkListBox" id="recipe_list">
+ <property name="visible">1</property>
+ <property name="selection-mode">none</property>
+ <property name="height-request">220</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
</interface>
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]