[gnome-builder] todo: add support for word wrapping GtkCellRenderer



commit c9b37a201f2fcd2287af8cf673603bcac8313e30
Author: Christian Hergert <chergert redhat com>
Date:   Fri Jun 30 20:03:48 2017 -0700

    todo: add support for word wrapping GtkCellRenderer
    
    This is a total hack. But it friggin' works. So we'll try to
    make it more generic (it will still be a hack though) so that
    we can use it for a few plugins.

 libide/ide.h                          |    1 +
 libide/meson.build                    |    2 +
 libide/util/ide-cell-renderer-fancy.c |  393 +++++++++++++++++++++++++++++++++
 libide/util/ide-cell-renderer-fancy.h |   37 +++
 plugins/todo/gbp-todo-panel.c         |   93 +++++++-
 5 files changed, 514 insertions(+), 12 deletions(-)
---
diff --git a/libide/ide.h b/libide/ide.h
index 35f38fe..d23ede7 100644
--- a/libide/ide.h
+++ b/libide/ide.h
@@ -148,6 +148,7 @@ G_BEGIN_DECLS
 #include "transfers/ide-transfer.h"
 #include "transfers/ide-transfer-button.h"
 #include "transfers/ide-transfer-manager.h"
+#include "util/ide-cell-renderer-fancy.h"
 #include "util/ide-flatpak.h"
 #include "util/ide-gtk.h"
 #include "util/ide-line-reader.h"
diff --git a/libide/meson.build b/libide/meson.build
index 8ec2994..f8d3625 100644
--- a/libide/meson.build
+++ b/libide/meson.build
@@ -193,6 +193,7 @@ libide_public_headers = [
   'transfers/ide-transfer-button.h',
   'transfers/ide-transfers-button.h',
   'transfers/ide-transfers-progress-icon.h',
+  'util/ide-cell-renderer-fancy.h',
   'util/ide-flatpak.h',
   'util/ide-glib.h',
   'util/ide-gtk.h',
@@ -393,6 +394,7 @@ libide_public_sources = [
   'transfers/ide-transfer-button.c',
   'transfers/ide-transfers-button.c',
   'transfers/ide-transfers-progress-icon.c',
+  'util/ide-cell-renderer-fancy.c',
   'util/ide-flatpak.c',
   'util/ide-glib.c',
   'util/ide-gtk.c',
diff --git a/libide/util/ide-cell-renderer-fancy.c b/libide/util/ide-cell-renderer-fancy.c
new file mode 100644
index 0000000..260b375
--- /dev/null
+++ b/libide/util/ide-cell-renderer-fancy.c
@@ -0,0 +1,393 @@
+/* ide-cell-renderer-fancy.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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/>.
+ */
+
+#define G_LOG_DOMAIN "ide-cell-renderer-fancy"
+
+#include "ide-cell-renderer-fancy.h"
+
+#define TITLE_SPACING 3
+
+struct _IdeCellRendererFancy
+{
+  GtkCellRenderer parent_instance;
+
+  gchar *title;
+  gchar *body;
+};
+
+enum {
+  PROP_0,
+  PROP_BODY,
+  PROP_TITLE,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeCellRendererFancy, ide_cell_renderer_fancy, GTK_TYPE_CELL_RENDERER)
+
+static GParamSpec *properties [N_PROPS];
+
+static PangoLayout *
+get_layout (IdeCellRendererFancy *self,
+            GtkWidget            *widget,
+            const gchar          *text,
+            gboolean              is_title,
+            GtkCellRendererState  flags)
+{
+  PangoLayout *l;
+  PangoAttrList *attrs;
+
+  l = gtk_widget_create_pango_layout (widget, text);
+
+  if (text == NULL || *text == 0)
+    return l;
+
+  attrs = pango_attr_list_new ();
+
+  if ((flags & GTK_CELL_RENDERER_SELECTED) != 0)
+    {
+      GtkStyleContext *style = gtk_widget_get_style_context (widget);
+      GtkStateFlags state = gtk_style_context_get_state (style);
+      GdkRGBA rgba;
+
+      gtk_style_context_get_color (style, state, &rgba);
+      pango_attr_list_insert (attrs,
+                              pango_attr_foreground_new (rgba.red * 65535,
+                                                         rgba.green * 65535,
+                                                         rgba.blue * 65535));
+    }
+
+  if (is_title)
+    {
+      pango_attr_list_insert (attrs, pango_attr_scale_new (0.8333));
+      pango_attr_list_insert (attrs, pango_attr_foreground_alpha_new (65536 * 0.5));
+    }
+
+  pango_layout_set_attributes (l, attrs);
+  pango_attr_list_unref (attrs);
+
+  return l;
+}
+
+static GtkSizeRequestMode
+ide_cell_renderer_fancy_get_request_mode (GtkCellRenderer *cell)
+{
+  return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static void
+ide_cell_renderer_fancy_get_preferred_width (GtkCellRenderer *cell,
+                                             GtkWidget       *widget,
+                                             gint            *min_width,
+                                             gint            *nat_width)
+{
+  IdeCellRendererFancy *self = (IdeCellRendererFancy *)cell;
+  PangoLayout *body;
+  PangoLayout *title;
+  gint body_width = 0;
+  gint title_width = 0;
+  gint dummy;
+  gint xpad;
+  gint ypad;
+
+  if (min_width == NULL)
+    min_width = &dummy;
+
+  if (nat_width == NULL)
+    nat_width = &dummy;
+
+  g_assert (IDE_IS_CELL_RENDERER_FANCY (self));
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (min_width != NULL);
+  g_assert (nat_width != NULL);
+
+  gtk_cell_renderer_get_padding (cell, &xpad, &ypad);
+
+  body = get_layout (self, widget, self->body, FALSE, 0);
+  title = get_layout (self, widget, self->title, TRUE, 0);
+
+  pango_layout_set_width (body, -1);
+  pango_layout_set_width (title, -1);
+
+  pango_layout_get_pixel_size (body, &body_width, NULL);
+  pango_layout_get_pixel_size (title, &title_width, NULL);
+
+  *min_width = xpad * 2;
+  *nat_width = (xpad * 2) + MAX (title_width, body_width);
+
+  g_object_unref (body);
+  g_object_unref (title);
+}
+
+static void
+ide_cell_renderer_fancy_get_preferred_height_for_width (GtkCellRenderer *cell,
+                                                        GtkWidget       *widget,
+                                                        gint             width,
+                                                        gint            *min_height,
+                                                        gint            *nat_height)
+{
+  IdeCellRendererFancy *self = (IdeCellRendererFancy *)cell;
+  PangoLayout *body;
+  PangoLayout *title;
+  GtkAllocation alloc;
+  gint body_height = 0;
+  gint title_height = 0;
+  gint xpad;
+  gint ypad;
+  gint dummy;
+
+  if (min_height == NULL)
+    min_height = &dummy;
+
+  if (nat_height == NULL)
+    nat_height = &dummy;
+
+  g_assert (IDE_IS_CELL_RENDERER_FANCY (self));
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (min_height != NULL);
+  g_assert (nat_height != NULL);
+
+  gtk_cell_renderer_get_padding (cell, &xpad, &ypad);
+
+  /*
+   * HACK: @width is the min_width returned in our get_preferred_width()
+   *       function. That results in pretty bad values here, so we will
+   *       do this by assuming we are the onl widget in the tree view.
+   *
+   *       This makes this cell very much not usable for generic situations,
+   *       but it does make it so we can do text wrapping without resorting
+   *       to GtkListBox *for our exact usecase only*.
+   *
+   *       The problem here is that we require the widget to already be
+   *       realized and allocated and that we are the only renderer
+   *       within the only column (and also, in a treeview) without
+   *       exotic styling.
+   *
+   *       If we get something absurdly small (like 50) that is because we
+   *       are hitting our minimum size of (xpad * 2). So this works around
+   *       the issue and tries to get something reasonable with wrapping
+   *       at the 200px mark (our ~default width for panels).
+   *
+   *       Furthermore, we need to queue a resize when the column size
+   *       changes (as it will from resizing the widget). So the tree
+   *       view must also call gtk_tree_view_column_queue_resize().
+   */
+  gtk_widget_get_allocation (widget, &alloc);
+  if (alloc.width > width)
+    width = alloc.width - (xpad * 2);
+  else if (alloc.width < 50)
+    width = 200;
+
+  body = get_layout (self, widget, self->body, FALSE, 0);
+  title = get_layout (self, widget, self->title, TRUE, 0);
+
+  pango_layout_set_width (body, width * PANGO_SCALE);
+  pango_layout_set_width (title, width * PANGO_SCALE);
+  pango_layout_get_pixel_size (title, NULL, &title_height);
+  pango_layout_get_pixel_size (body, NULL, &body_height);
+  *min_height = *nat_height = (ypad * 2) + title_height + TITLE_SPACING + body_height;
+
+  g_object_unref (body);
+  g_object_unref (title);
+}
+
+static void
+ide_cell_renderer_fancy_render (GtkCellRenderer      *renderer,
+                                cairo_t              *cr,
+                                GtkWidget            *widget,
+                                const GdkRectangle   *bg_area,
+                                const GdkRectangle   *cell_area,
+                                GtkCellRendererState  flags)
+{
+  IdeCellRendererFancy *self = (IdeCellRendererFancy *)renderer;
+  PangoLayout *body;
+  PangoLayout *title;
+  gint xpad;
+  gint ypad;
+  gint height;
+
+  g_assert (IDE_IS_CELL_RENDERER_FANCY (self));
+  g_assert (cr != NULL);
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (bg_area != NULL);
+  g_assert (cell_area != NULL);
+
+  gtk_cell_renderer_get_padding (renderer, &xpad, &ypad);
+
+  body = get_layout (self, widget, self->body, FALSE, flags);
+  title = get_layout (self, widget, self->title, TRUE, flags);
+
+  pango_layout_set_width (title, (cell_area->width - (xpad * 2)) * PANGO_SCALE);
+  pango_layout_set_width (body, (cell_area->width - (xpad * 2)) * PANGO_SCALE);
+
+  cairo_move_to (cr, cell_area->x + xpad, cell_area->y + ypad);
+  pango_cairo_show_layout (cr, title);
+
+  pango_layout_get_pixel_size (title, NULL, &height);
+  cairo_move_to (cr, cell_area->x + xpad, cell_area->y +ypad + + height + TITLE_SPACING);
+  pango_cairo_show_layout (cr, body);
+
+  g_object_unref (body);
+  g_object_unref (title);
+}
+
+static void
+ide_cell_renderer_fancy_finalize (GObject *object)
+{
+  IdeCellRendererFancy *self = (IdeCellRendererFancy *)object;
+
+  g_clear_pointer (&self->body, g_free);
+  g_clear_pointer (&self->title, g_free);
+
+  G_OBJECT_CLASS (ide_cell_renderer_fancy_parent_class)->finalize (object);
+}
+
+static void
+ide_cell_renderer_fancy_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  IdeCellRendererFancy *self = IDE_CELL_RENDERER_FANCY (object);
+
+  switch (prop_id)
+    {
+    case PROP_BODY:
+      g_value_set_string (value, self->body);
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, self->title);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_cell_renderer_fancy_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  IdeCellRendererFancy *self = IDE_CELL_RENDERER_FANCY (object);
+
+  switch (prop_id)
+    {
+    case PROP_BODY:
+      ide_cell_renderer_fancy_set_body (self, g_value_get_string (value));
+      break;
+
+    case PROP_TITLE:
+      ide_cell_renderer_fancy_set_title (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_cell_renderer_fancy_class_init (IdeCellRendererFancyClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkCellRendererClass *cell_class = GTK_CELL_RENDERER_CLASS (klass);
+
+  object_class->finalize = ide_cell_renderer_fancy_finalize;
+  object_class->get_property = ide_cell_renderer_fancy_get_property;
+  object_class->set_property = ide_cell_renderer_fancy_set_property;
+
+  cell_class->get_request_mode = ide_cell_renderer_fancy_get_request_mode;
+  cell_class->get_preferred_width = ide_cell_renderer_fancy_get_preferred_width;
+  cell_class->get_preferred_height_for_width = ide_cell_renderer_fancy_get_preferred_height_for_width;
+  cell_class->render = ide_cell_renderer_fancy_render;
+
+  /* Note that we do not emit notify for these properties */
+
+  properties [PROP_BODY] =
+    g_param_spec_string ("body",
+                         "Body",
+                         "The body of the renderer",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title of the renderer",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_cell_renderer_fancy_init (IdeCellRendererFancy *self)
+{
+}
+
+const gchar *
+ide_cell_renderer_fancy_get_title (IdeCellRendererFancy *self)
+{
+  return self->title;
+}
+
+/**
+ * ide_cell_renderer_fancy_take_title:
+ * @self: a #IdeCellRendererFancy
+ * @title: (transfer full) (nullable): the new title
+ *
+ * Like ide_cell_renderer_fancy_set_title() but takes ownership
+ * of @title, saving a string copy.
+ *
+ * Since: 3.26
+ */
+void
+ide_cell_renderer_fancy_take_title (IdeCellRendererFancy *self,
+                                    gchar                *title)
+{
+  if (self->title != title)
+    {
+      g_free (self->title);
+      self->title = title;
+    }
+}
+
+void
+ide_cell_renderer_fancy_set_title (IdeCellRendererFancy *self,
+                                   const gchar          *title)
+{
+  ide_cell_renderer_fancy_take_title (self, g_strdup (title));
+}
+
+const gchar *
+ide_cell_renderer_fancy_get_body (IdeCellRendererFancy *self)
+{
+  return self->body;
+}
+
+void
+ide_cell_renderer_fancy_set_body (IdeCellRendererFancy *self,
+                                  const gchar          *body)
+{
+  if (self->body != body)
+    {
+      g_free (self->body);
+      self->body = g_strdup (body);
+    }
+}
diff --git a/libide/util/ide-cell-renderer-fancy.h b/libide/util/ide-cell-renderer-fancy.h
new file mode 100644
index 0000000..45a3b68
--- /dev/null
+++ b/libide/util/ide-cell-renderer-fancy.h
@@ -0,0 +1,37 @@
+/* ide-cell-renderer-fancy.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * 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>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_CELL_RENDERER_FANCY (ide_cell_renderer_fancy_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeCellRendererFancy, ide_cell_renderer_fancy, IDE, CELL_RENDERER_FANCY, 
GtkCellRenderer)
+
+GtkCellRenderer *ide_cell_renderer_fancy_new        (void);
+void             ide_cell_renderer_fancy_take_title (IdeCellRendererFancy *self,
+                                                     gchar                *title);
+void             ide_cell_renderer_fancy_set_title  (IdeCellRendererFancy *self,
+                                                     const gchar          *title);
+void             ide_cell_renderer_fancy_set_body   (IdeCellRendererFancy *self,
+                                                     const gchar          *body);
+
+G_END_DECLS
diff --git a/plugins/todo/gbp-todo-panel.c b/plugins/todo/gbp-todo-panel.c
index 11536ca..037eb8e 100644
--- a/plugins/todo/gbp-todo-panel.c
+++ b/plugins/todo/gbp-todo-panel.c
@@ -29,6 +29,9 @@ struct _GbpTodoPanel
 
   GtkTreeView  *tree_view;
   GbpTodoModel *model;
+
+  guint         last_width;
+  guint         relayout_source;
 };
 
 G_DEFINE_TYPE (GbpTodoPanel, gbp_todo_panel, GTK_TYPE_BIN)
@@ -58,8 +61,8 @@ gbp_todo_panel_cell_data_func (GtkCellLayout   *cell_layout,
 
   if (message != NULL)
     {
-      g_autofree gchar *escape1 = NULL;
-      g_autofree gchar *escape2 = NULL;
+      g_autofree gchar *title = NULL;
+      const gchar *path;
       guint lineno;
 
       /*
@@ -70,15 +73,18 @@ gbp_todo_panel_cell_data_func (GtkCellLayout   *cell_layout,
       while (g_ascii_isspace (*message))
         message++;
 
-      escape1 = g_markup_escape_text (gbp_todo_item_get_path (item), -1);
-      escape2 = g_markup_escape_text (message, -1);
+      path = gbp_todo_item_get_path (item);
       lineno = gbp_todo_item_get_lineno (item);
-
-      markup = g_strdup_printf ("<span size='smaller' fgalpha='32768'>%s:%u</span>\n%s",
-                                escape1, lineno, escape2);
+      title = g_strdup_printf ("%s:%u", path, lineno);
+      ide_cell_renderer_fancy_take_title (IDE_CELL_RENDERER_FANCY (cell),
+                                          g_steal_pointer (&title));
+      ide_cell_renderer_fancy_set_body (IDE_CELL_RENDERER_FANCY (cell), message);
+    }
+  else
+    {
+      ide_cell_renderer_fancy_set_body (IDE_CELL_RENDERER_FANCY (cell), NULL);
+      ide_cell_renderer_fancy_set_title (IDE_CELL_RENDERER_FANCY (cell), NULL);
     }
-
-  g_object_set (cell, "markup", markup, NULL);
 }
 
 static void
@@ -196,6 +202,68 @@ gbp_todo_panel_query_tooltip (GbpTodoPanel *self,
   return FALSE;
 }
 
+static gboolean
+queue_relayout_in_idle (gpointer user_data)
+{
+  GbpTodoPanel *self = user_data;
+  GtkAllocation alloc;
+  guint n_columns;
+
+  g_assert (GBP_IS_TODO_PANEL (self));
+
+  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+  if (alloc.width == self->last_width)
+    goto cleanup;
+
+  self->last_width = alloc.width;
+
+  n_columns = gtk_tree_view_get_n_columns (self->tree_view);
+
+  for (guint i = 0; i < n_columns; i++)
+    {
+      GtkTreeViewColumn *column;
+
+      column = gtk_tree_view_get_column (self->tree_view, i);
+      gtk_tree_view_column_queue_resize (column);
+    }
+
+cleanup:
+  self->relayout_source = 0;
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+gbp_todo_panel_size_allocate (GtkWidget     *widget,
+                              GtkAllocation *alloc)
+{
+  GbpTodoPanel *self = (GbpTodoPanel *)widget;
+
+  g_assert (GBP_IS_TODO_PANEL (self));
+  g_assert (alloc != NULL);
+
+  GTK_WIDGET_CLASS (gbp_todo_panel_parent_class)->size_allocate (widget, alloc);
+
+  if (self->last_width != alloc->width)
+    {
+      /*
+       * We must perform our queued relayout from an idle callback
+       * so that we don't affect this draw cycle. If we do that, we
+       * will get empty content flashes for the current frame. This
+       * allows us to draw the current frame slightly incorrect but
+       * fixup on the next frame (which looks much nicer from a user
+       * point of view).
+       */
+      if (self->relayout_source == 0)
+        self->relayout_source =
+          gdk_threads_add_idle_full (G_PRIORITY_LOW + 100,
+                                     queue_relayout_in_idle,
+                                     g_object_ref (self),
+                                     g_object_unref);
+    }
+}
+
 static void
 gbp_todo_panel_destroy (GtkWidget *widget)
 {
@@ -206,6 +274,7 @@ gbp_todo_panel_destroy (GtkWidget *widget)
   if (self->tree_view != NULL)
     gtk_tree_view_set_model (self->tree_view, NULL);
 
+  ide_clear_source (&self->relayout_source);
   g_clear_object (&self->model);
 
   GTK_WIDGET_CLASS (gbp_todo_panel_parent_class)->destroy (widget);
@@ -259,6 +328,7 @@ gbp_todo_panel_class_init (GbpTodoPanelClass *klass)
   object_class->set_property = gbp_todo_panel_set_property;
 
   widget_class->destroy = gbp_todo_panel_destroy;
+  widget_class->size_allocate = gbp_todo_panel_size_allocate;
 
   properties [PROP_MODEL] =
     g_param_spec_object ("model",
@@ -309,11 +379,10 @@ gbp_todo_panel_init (GbpTodoPanel *self)
                          NULL);
   gtk_tree_view_append_column (self->tree_view, column);
 
-  cell = g_object_new (GTK_TYPE_CELL_RENDERER_TEXT,
-                       "ellipsize", PANGO_ELLIPSIZE_END,
+  cell = g_object_new (IDE_TYPE_CELL_RENDERER_FANCY,
                        "visible", TRUE,
                        "xalign", 0.0f,
-                       "xpad", 3,
+                       "xpad", 4,
                        "ypad", 6,
                        NULL);
   gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), cell, TRUE);


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