Commit 124c911e authored by Milan Crha's avatar Milan Crha

gs-details-page: Collapse long descriptions

Add a button to Read More/Read Less of the application description, starting
with the collapsed state.

Closes #11
Closes !514
parent bc617c53
Pipeline #215386 passed with stage
in 5 minutes and 3 seconds
/* -*- 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 + 8);
g_string_append_len (str, box->text, line->start_index);
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);
}
}
/* -*- 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
......@@ -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;
......@@ -705,50 +707,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
......@@ -2734,6 +2694,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);
......
......@@ -369,14 +369,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>
......
......@@ -111,6 +111,11 @@
.application-details-description {
}
.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;
......
......@@ -289,6 +289,11 @@
.application-details-description {
}
.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;
......
......@@ -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',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment