[gnome-software/11-collapse-long-descriptions] gs-details-page: Collapse long descriptions




commit 75c63a9c8552e8498a0a2d97d81bd376c3788ccc
Author: Milan Crha <mcrha redhat com>
Date:   Wed Jan 6 09:37:13 2021 +0100

    gs-details-page: Collapse long descriptions
    
    Add a button to Read More/Read Less of the application description, starting
    with the collapsed state.
    
    Closes https://gitlab.gnome.org/GNOME/gnome-software/-/issues/11

 src/gs-description-box.c | 269 +++++++++++++++++++++++++++++++++++++++++++++++
 src/gs-description-box.h |  32 ++++++
 src/gs-details-page.c    |  49 +--------
 src/gs-details-page.ui   |  27 +++--
 src/gtk-style-hc.css     |   5 +
 src/gtk-style.css        |   5 +
 src/meson.build          |   1 +
 7 files changed, 337 insertions(+), 51 deletions(-)
---
diff --git a/src/gs-description-box.c b/src/gs-description-box.c
new file mode 100644
index 00000000..f6569c1d
--- /dev/null
+++ b/src/gs-description-box.c
@@ -0,0 +1,269 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2020 Red Hat <www.redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "gs-description-box.h"
+
+#define MAX_COLLAPSED_LINES 4
+#define THREE_DOTS_TEXT _("…")
+
+struct _GsDescriptionBox {
+       GtkBox parent;
+       GtkLabel *label;
+       GtkButton *button;
+       gchar *text;
+       gboolean is_collapsed;
+       gboolean needs_recalc;
+       gint last_width;
+       gint last_height;
+};
+
+G_DEFINE_TYPE (GsDescriptionBox, gs_description_box, GTK_TYPE_BOX)
+
+static void
+gs_description_box_update_content (GsDescriptionBox *box)
+{
+       GtkAllocation allocation;
+       PangoLayout *layout;
+       gint n_lines;
+
+       if (!box->text || !*(box->text)) {
+               gtk_widget_hide (GTK_WIDGET (box));
+               box->needs_recalc = TRUE;
+               return;
+       }
+
+       gtk_widget_get_allocation (GTK_WIDGET (box), &allocation);
+
+       if (!box->needs_recalc && box->last_width == allocation.width && box->last_height == 
allocation.height)
+               return;
+
+       box->needs_recalc = allocation.width <= 1 || allocation.height <= 1;
+       box->last_width = allocation.width;
+       box->last_height = allocation.height;
+
+       gtk_button_set_label (box->button, box->is_collapsed ? _("_Read More") : _("_Read Less"));
+
+       gtk_label_set_text (box->label, box->text);
+
+       layout = gtk_label_get_layout (box->label);
+       n_lines = pango_layout_get_line_count (layout);
+
+       gtk_widget_set_visible (GTK_WIDGET (box->button), n_lines > MAX_COLLAPSED_LINES);
+
+       if (box->is_collapsed && n_lines > MAX_COLLAPSED_LINES) {
+               PangoLayoutLine *line;
+
+               line = pango_layout_get_line_readonly (layout, MAX_COLLAPSED_LINES);
+
+               if (line) {
+                       GString *str;
+
+                       str = g_string_sized_new (line->start_index + strlen (THREE_DOTS_TEXT) + 1);
+                       g_string_append_len (str, box->text, line->start_index);
+
+                       /* Cut characters from the end of the string, thus it doesn't look bad with the added 
dots. */
+                       while (str->len > 0 && strchr ("\r\n\t .", str->str[str->len - 1])) {
+                               str->len--;
+                       }
+
+                       g_string_append (str, THREE_DOTS_TEXT);
+
+                       gtk_label_set_text (box->label, str->str);
+
+                       g_string_free (str, TRUE);
+               }
+       }
+
+       gtk_widget_show (GTK_WIDGET (box));
+}
+
+static void
+gs_description_box_read_button_clicked_cb (GtkButton *button,
+                                          gpointer user_data)
+{
+       GsDescriptionBox *box = user_data;
+
+       g_return_if_fail (GS_IS_DESCRIPTION_BOX (box));
+
+       box->is_collapsed = !box->is_collapsed;
+       box->needs_recalc = TRUE;
+
+       gs_description_box_update_content (box);
+}
+
+static void
+gs_description_box_size_allocate (GtkWidget *widget,
+                                 GtkAllocation *allocation)
+{
+       GsDescriptionBox *box = GS_DESCRIPTION_BOX (widget);
+
+       GTK_WIDGET_CLASS (gs_description_box_parent_class)->size_allocate (widget, allocation);
+
+       gs_description_box_update_content (box);
+}
+
+static void
+gs_description_box_finalize (GObject *object)
+{
+       GsDescriptionBox *box = GS_DESCRIPTION_BOX (object);
+
+       g_clear_pointer (&box->text, g_free);
+
+       G_OBJECT_CLASS (gs_description_box_parent_class)->finalize (object);
+}
+
+static void
+gs_description_box_init (GsDescriptionBox *box)
+{
+       GtkStyleContext *style_context;
+       GtkWidget *widget;
+
+       box->is_collapsed = TRUE;
+
+       style_context = gtk_widget_get_style_context (GTK_WIDGET (box));
+       gtk_style_context_add_class (style_context, "application-details-description");
+
+       widget = gtk_label_new ("");
+       g_object_set (G_OBJECT (widget),
+               "hexpand", TRUE,
+               "halign", GTK_ALIGN_FILL,
+               "vexpand", FALSE,
+               "valign", GTK_ALIGN_START,
+               "visible", TRUE,
+               "can-focus", FALSE,
+               "max-width-chars", 40,
+               "width-chars", 40,
+               "selectable", TRUE,
+               "wrap", TRUE,
+               "xalign", 0.0,
+               NULL);
+
+       gtk_box_pack_start (GTK_BOX (box), widget, TRUE, TRUE, 0);
+
+       style_context = gtk_widget_get_style_context (widget);
+       gtk_style_context_add_class (style_context, "label");
+
+       box->label = GTK_LABEL (widget);
+
+       widget = gtk_button_new_with_mnemonic (_("_Read More"));
+
+       g_object_set (G_OBJECT (widget),
+               "hexpand", FALSE,
+               "halign", GTK_ALIGN_CENTER,
+               "vexpand", FALSE,
+               "valign", GTK_ALIGN_CENTER,
+               "visible", TRUE,
+               NULL);
+
+       gtk_box_pack_start (GTK_BOX (box), widget, FALSE, FALSE, 0);
+
+       style_context = gtk_widget_get_style_context (widget);
+       gtk_style_context_add_class (style_context, "button");
+       gtk_style_context_add_class (style_context, "circular");
+
+       box->button = GTK_BUTTON (widget);
+
+       g_signal_connect (box->button, "clicked",
+               G_CALLBACK (gs_description_box_read_button_clicked_cb), box);
+}
+
+static void
+gs_description_box_class_init (GsDescriptionBoxClass *klass)
+{
+       GObjectClass *object_class;
+       GtkWidgetClass *widget_class;
+
+       object_class = G_OBJECT_CLASS (klass);
+       object_class->finalize = gs_description_box_finalize;
+
+       widget_class = GTK_WIDGET_CLASS (klass);
+       widget_class->size_allocate = gs_description_box_size_allocate;
+}
+
+GtkWidget *
+gs_description_box_new (void)
+{
+       return g_object_new (GS_TYPE_DESCRIPTION_BOX,
+               "orientation", GTK_ORIENTATION_VERTICAL,
+               "spacing", 24,
+               NULL);
+}
+
+const gchar *
+gs_description_box_get_text (GsDescriptionBox *box)
+{
+       g_return_val_if_fail (GS_IS_DESCRIPTION_BOX (box), NULL);
+
+       return box->text;
+}
+
+void
+gs_description_box_set_text (GsDescriptionBox *box,
+                            const gchar *text)
+{
+       g_return_if_fail (GS_IS_DESCRIPTION_BOX (box));
+
+       if (g_strcmp0 (text, box->text) != 0) {
+               g_free (box->text);
+               box->text = g_strdup (text);
+               box->needs_recalc = TRUE;
+
+               gs_description_box_update_content (box);
+       }
+}
+
+gboolean
+gs_description_box_get_collapsed (GsDescriptionBox *box)
+{
+       g_return_val_if_fail (GS_IS_DESCRIPTION_BOX (box), FALSE);
+
+       return box->is_collapsed;
+}
+
+void
+gs_description_box_set_collapsed (GsDescriptionBox *box,
+                                 gboolean collapsed)
+{
+       g_return_if_fail (GS_IS_DESCRIPTION_BOX (box));
+
+       if ((collapsed ? 1 : 0) != (box->is_collapsed ? 1 : 0)) {
+               box->is_collapsed = collapsed;
+               box->needs_recalc = TRUE;
+
+               gs_description_box_update_content (box);
+       }
+}
+
+void
+gs_description_box_reset (GsDescriptionBox *box,
+                         const gchar *text,
+                         gboolean collapsed)
+{
+       gboolean changed;
+
+       g_return_if_fail (GS_IS_DESCRIPTION_BOX (box));
+
+       changed = g_strcmp0 (text, box->text) != 0;
+
+       if (changed || (collapsed ? 1 : 0) != (box->is_collapsed ? 1 : 0)) {
+               box->is_collapsed = collapsed;
+
+               if (changed) {
+                       g_free (box->text);
+                       box->text = g_strdup (text);
+               }
+
+               box->needs_recalc = TRUE;
+
+               gs_description_box_update_content (box);
+       }
+}
diff --git a/src/gs-description-box.h b/src/gs-description-box.h
new file mode 100644
index 00000000..02e35455
--- /dev/null
+++ b/src/gs-description-box.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2020 Red Hat <www.redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_DESCRIPTION_BOX (gs_description_box_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsDescriptionBox, gs_description_box, GS, DESCRIPTION_BOX, GtkBox)
+
+GtkWidget      *gs_description_box_new         (void);
+const gchar    *gs_description_box_get_text    (GsDescriptionBox *box);
+void            gs_description_box_set_text    (GsDescriptionBox *box,
+                                                const gchar *text);
+gboolean        gs_description_box_get_collapsed
+                                               (GsDescriptionBox *box);
+void            gs_description_box_set_collapsed
+                                               (GsDescriptionBox *box,
+                                                gboolean collapsed);
+void            gs_description_box_reset       (GsDescriptionBox *box,
+                                                const gchar *text,
+                                                gboolean collapsed);
+
+G_END_DECLS
diff --git a/src/gs-details-page.c b/src/gs-details-page.c
index 7d5a1171..a75dcb2a 100644
--- a/src/gs-details-page.c
+++ b/src/gs-details-page.c
@@ -20,6 +20,7 @@
 
 #include "gs-details-page.h"
 #include "gs-app-addon-row.h"
+#include "gs-description-box.h"
 #include "gs-history-dialog.h"
 #include "gs-origin-popover-row.h"
 #include "gs-screenshot-image.h"
@@ -62,6 +63,7 @@ struct _GsDetailsPage
        GtkWidget               *box_addons;
        GtkWidget               *box_details;
        GtkWidget               *box_details_description;
+       GtkWidget               *label_webapp_warning;
        GtkWidget               *box_details_support;
        GtkWidget               *box_progress;
        GtkWidget               *box_progress2;
@@ -716,50 +718,8 @@ gs_details_page_donate_cb (GtkWidget *widget, GsDetailsPage *self)
 static void
 gs_details_page_set_description (GsDetailsPage *self, const gchar *tmp)
 {
-       GtkStyleContext *style_context;
-       GtkWidget *para;
-       guint i;
-       g_auto(GStrv) split = NULL;
-
-       /* does the description exist? */
-       gtk_widget_set_visible (self->box_details_description, tmp != NULL);
-       if (tmp == NULL)
-               return;
-
-       /* add each paragraph as a new GtkLabel which lets us get the 24px
-        * paragraph spacing */
-       gs_container_remove_all (GTK_CONTAINER (self->box_details_description));
-       split = g_strsplit (tmp, "\n\n", -1);
-       for (i = 0; split[i] != NULL; i++) {
-               para = gtk_label_new (split[i]);
-               gtk_label_set_line_wrap (GTK_LABEL (para), TRUE);
-               gtk_label_set_max_width_chars (GTK_LABEL (para), 40);
-               gtk_label_set_selectable (GTK_LABEL (para), TRUE);
-               gtk_widget_set_visible (para, TRUE);
-               gtk_widget_set_can_focus (para, FALSE);
-               g_object_set (para,
-                             "xalign", 0.0,
-                             NULL);
-
-               /* add style class for theming */
-               style_context = gtk_widget_get_style_context (para);
-               gtk_style_context_add_class (style_context,
-                                            "application-details-description");
-
-               gtk_container_add (GTK_CONTAINER (self->box_details_description), para);
-       }
-
-       /* show the webapp warning */
-       if (gs_app_get_kind (self->app) == AS_APP_KIND_WEB_APP) {
-               GtkWidget *label;
-               /* TRANSLATORS: this is the warning box */
-               label = gtk_label_new (_("This application can only be used when there is an active internet 
connection."));
-               gtk_widget_set_visible (label, TRUE);
-               gtk_label_set_xalign (GTK_LABEL (label), 0.f);
-               gtk_style_context_add_class (gtk_widget_get_style_context (label),
-                                            "application-details-webapp-warning");
-               gtk_container_add (GTK_CONTAINER (self->box_details_description), label);
-       }
+       gs_description_box_reset (GS_DESCRIPTION_BOX (self->box_details_description), tmp, TRUE);
+       gtk_widget_set_visible (self->label_webapp_warning, gs_app_get_kind (self->app) == 
AS_APP_KIND_WEB_APP);
 }
 
 static void
@@ -2851,6 +2811,7 @@ gs_details_page_class_init (GsDetailsPageClass *klass)
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_addons);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_description);
+       gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, label_webapp_warning);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_details_support);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_progress);
        gtk_widget_class_bind_template_child (widget_class, GsDetailsPage, box_progress2);
diff --git a/src/gs-details-page.ui b/src/gs-details-page.ui
index 51c10a2d..f2d25d26 100644
--- a/src/gs-details-page.ui
+++ b/src/gs-details-page.ui
@@ -378,14 +378,27 @@
                           </object>
                         </child>
                         <child>
-                          <object class="GtkBox" id="box_details_description">
-                            <property name="visible">True</property>
-                            <property name="margin_bottom">14</property>
+                          <object class="GsDescriptionBox" id="box_details_description">
                             <property name="orientation">vertical</property>
-                            <property name="spacing">18</property>
-                            <child>
-                              <placeholder/>
-                            </child>
+                            <property name="spacing">12</property>
+                            <property name="halign">fill</property>
+                            <property name="hexpand">True</property>
+                            <property name="valign">start</property>
+                            <property name="visible">FALSE</property>
+                            <property name="margin_bottom">14</property>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkLabel" id="label_webapp_warning">
+                            <property name="visible">False</property>
+                            <property name="halign">center</property>
+                            <property name="hexpand">True</property>
+                            <property name="valign">start</property>
+                            <property name="xalign">0.0</property>
+                            <property name="label" translatable="yes">This application can only be used when 
there is an active internet connection.</property>
+                            <style>
+                              <class name="application-details-webapp-warning"/>
+                            </style>
                           </object>
                         </child>
                         <child>
diff --git a/src/gtk-style-hc.css b/src/gtk-style-hc.css
index c5696b80..81e399a5 100644
--- a/src/gtk-style-hc.css
+++ b/src/gtk-style-hc.css
@@ -118,6 +118,11 @@
        100% { background-position: 0%; }
 }
 
+.application-details-description .button {
+       padding-left:24px;
+       padding-right:24px;
+}
+
 .install-progress {
        background-image: linear-gradient(to top, @theme_selected_bg_color 2px, 
alpha(@theme_selected_bg_color, 0) 2px);
        background-repeat: no-repeat;
diff --git a/src/gtk-style.css b/src/gtk-style.css
index bf1f8ccc..74fdeadc 100644
--- a/src/gtk-style.css
+++ b/src/gtk-style.css
@@ -328,6 +328,11 @@
        100% { background-position: 0%; }
 }
 
+.application-details-description .button {
+       padding-left:24px;
+       padding-right:24px;
+}
+
 .install-progress {
        background-image: linear-gradient(to top, @theme_selected_bg_color 2px, 
alpha(@theme_selected_bg_color, 0) 2px);
        background-repeat: no-repeat;
diff --git a/src/meson.build b/src/meson.build
index 2f9ff234..2e6a2bd8 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -26,6 +26,7 @@ gnome_software_sources = [
   'gs-common.c',
   'gs-css.c',
   'gs-content-rating.c',
+  'gs-description-box.c',
   'gs-details-page.c',
   'gs-extras-page.c',
   'gs-feature-tile.c',


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