[rhythmbox] podcast: use webkit to display HTML episode descriptions (bug #320507)



commit f2b94b4e4a7dcd653226ab032b91099d9011a6e8
Author: Jonathan Matthew <jonathan d14n org>
Date:   Mon Apr 5 21:12:38 2010 +1000

    podcast: use webkit to display HTML episode descriptions (bug #320507)
    
    The main interesting bit here is that we need to distinguish between
    plain text and HTML descriptions, as interpreting plain text as HTML
    removes all formatting.  Since we're dealing with small document
    fragments here, normal content type detection doesn't work.  Instead, we
    search for a set of strings, such as common tags and entities, that
    reliably indicate HTML content.
    
    The dependency on webkit is optional.

 configure.ac                           |   17 ++++
 data/ui/podcast-properties.ui          |   15 +---
 podcast/Makefile.am                    |    1 +
 podcast/rb-podcast-properties-dialog.c |  162 +++++++++++++++++++++++++++++++-
 shell/Makefile.am                      |    2 +
 5 files changed, 184 insertions(+), 13 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 0b85901..18eace2 100644
--- a/configure.ac
+++ b/configure.ac
@@ -57,6 +57,7 @@ LIBSOUP_REQS=2.26.0
 GUDEV_REQS=143
 LIBMTP_REQS=0.3.0
 PYGOBJECT_REQUIRED=2.15.4
+WEBKIT_MIN_REQS=1.1.17
 
 AC_MSG_CHECKING([for GNU extension fwrite_unlocked])
 AC_LINK_IFELSE(
@@ -517,6 +518,22 @@ if test x"$enable_lirc" != xno; then
 fi
 AM_CONDITIONAL(WITH_LIRC, test x"$with_lirc" = xyes)
 
+dnl WebKit
+AC_ARG_WITH(webkit,
+	    AC_HELP_STRING([--with-webkit],
+			   [Use WebKit to display HTML]),,
+	    with_webkit_gtk=auto)
+if test "x$with_webkit_gtk" != xno; then
+	PKG_CHECK_MODULES(WEBKIT, [webkit-1.0 >= $WEBKIT_MIN_REQS], have_webkit=yes, have_webkit=no)
+	if test "x$have_webkit" = "xno" -a "x$with_webkit" = "xyes"; then
+		AC_MSG_ERROR([WebKit support explicitly requested, but WebKit could not be found])
+	fi
+	if test "x$have_webkit" = "xyes"; then
+		AC_DEFINE(WITH_WEBKIT, 1, [Define if WebKit is enabled])
+	fi
+fi
+AM_CONDITIONAL(WITH_WEBKIT, test x"$have_webkit" = xyes)
+
 
 AC_ARG_ENABLE(uninstalled-build,
               AC_HELP_STRING([--enable-uninstalled-build],
diff --git a/data/ui/podcast-properties.ui b/data/ui/podcast-properties.ui
index 359ce79..6c389d6 100644
--- a/data/ui/podcast-properties.ui
+++ b/data/ui/podcast-properties.ui
@@ -117,7 +117,6 @@
                 <property name="xalign">0</property>
                 <property name="yalign">0</property>
                 <property name="label" translatable="yes">Description:</property>
-                <property name="mnemonic_widget">descriptionLabel</property>
               </object>
             </child>
           </object>
@@ -129,26 +128,18 @@
           </packing>
         </child>
         <child>
-          <object class="GtkScrolledWindow" id="scrolledwindow1">
+          <object class="GtkScrolledWindow" id="descriptionWindow">
             <property name="visible">True</property>
             <property name="can_focus">True</property>
             <property name="hscrollbar_policy">never</property>
             <property name="vscrollbar_policy">automatic</property>
             <child>
-              <object class="GtkViewport" id="viewport1">
+              <object class="GtkViewport" id="descriptionViewport">
                 <property name="visible">True</property>
                 <property name="resize_mode">queue</property>
                 <property name="shadow_type">none</property>
                 <child>
-                  <object class="GtkLabel" id="descriptionLabel">
-                    <property name="visible">True</property>
-                    <property name="can_focus">True</property>
-                    <property name="xalign">0</property>
-                    <property name="yalign">0</property>
-                    <property name="label">-</property>
-                    <property name="wrap">True</property>
-                    <property name="selectable">True</property>
-                  </object>
+                  <placeholder/>
                 </child>
               </object>
             </child>
diff --git a/podcast/Makefile.am b/podcast/Makefile.am
index 601d912..6a93149 100644
--- a/podcast/Makefile.am
+++ b/podcast/Makefile.am
@@ -35,6 +35,7 @@ AM_CFLAGS =						\
 	-I$(top_srcdir)/library				\
 	-I$(top_builddir)/lib 				\
 	$(RHYTHMBOX_CFLAGS)				\
+	$(WEBKIT_CFLAGS)				\
 	$(TOTEM_PLPARSER_CFLAGS)
 
 librbpodcast_la_LDFLAGS = -export-dynamic
diff --git a/podcast/rb-podcast-properties-dialog.c b/podcast/rb-podcast-properties-dialog.c
index 52a5791..0b22f11 100644
--- a/podcast/rb-podcast-properties-dialog.c
+++ b/podcast/rb-podcast-properties-dialog.c
@@ -35,6 +35,10 @@
 #include <gtk/gtk.h>
 #include <glib.h>
 
+#if defined(WITH_WEBKIT)
+#include <webkit/webkit.h>
+#endif
+
 #include "rb-podcast-properties-dialog.h"
 #include "rb-file-helpers.h"
 #include "rb-builder-helpers.h"
@@ -42,6 +46,7 @@
 #include "rb-rating.h"
 #include "rb-util.h"
 #include "rb-cut-and-paste-code.h"
+#include "rb-debug.h"
 
 static void rb_podcast_properties_dialog_class_init (RBPodcastPropertiesDialogClass *klass);
 static void rb_podcast_properties_dialog_init (RBPodcastPropertiesDialog *dialog);
@@ -95,6 +100,7 @@ struct RBPodcastPropertiesDialogPrivate
 	GtkWidget   *rating;
 	GtkWidget   *date;
 	GtkWidget   *description;
+	GtkWidget   *description_window;
 
 	GtkWidget   *close_button;
 };
@@ -108,6 +114,29 @@ enum
 
 G_DEFINE_TYPE (RBPodcastPropertiesDialog, rb_podcast_properties_dialog, GTK_TYPE_DIALOG)
 
+#if defined(WITH_WEBKIT)
+/* list of HTML-ish strings that we search for to distinguish plain text from HTML podcast
+ * descriptions.  we don't really have anything else to go on - regular content type
+ * sniffing only works for proper HTML documents, but these are just tiny fragments, usually
+ * with some simple formatting tags.  if we find any of these in a podcast description,
+ * we'll display it as HTML rather than text.
+ */
+static const char *html_clues[] = {
+	"<a ",
+	"<b>",
+	"<i>",
+	"<ul>",
+	"<br",
+	"<img ",
+	"&lt;",
+	"&gt;",
+	"&amp;",
+	"&quo;",
+	"&#8",
+	"&#x"
+};
+#endif
+
 static void
 rb_podcast_properties_dialog_class_init (RBPodcastPropertiesDialogClass *klass)
 {
@@ -130,10 +159,73 @@ rb_podcast_properties_dialog_class_init (RBPodcastPropertiesDialogClass *klass)
 	g_type_class_add_private (klass, sizeof (RBPodcastPropertiesDialogPrivate));
 }
 
+#if defined(WITH_WEBKIT)
+
+static WebKitNavigationResponse
+navigation_requested_cb (WebKitWebView *web_view,
+			 WebKitWebFrame *frame,
+			 WebKitNetworkRequest *request,
+			 RBPodcastPropertiesDialog *dialog)
+{
+	const char *uri;
+	GError *error = NULL;
+
+	uri = webkit_network_request_get_uri (request);
+	gtk_show_uri (gtk_widget_get_screen (GTK_WIDGET (dialog)), uri, GDK_CURRENT_TIME, &error);
+	if (error != NULL) {
+		rb_error_dialog (NULL, _("Unable to display requested URI"), "%s", error->message);
+		g_error_free (error);
+	}
+
+	return WEBKIT_NAVIGATION_RESPONSE_IGNORE;
+}
+
+static void
+set_webkit_settings (WebKitWebView *view)
+{
+	WebKitWebSettings *settings;
+
+	settings = webkit_web_settings_new ();
+	g_object_set (settings,
+		      "enable-scripts", FALSE,
+		      "enable-plugins", FALSE,
+		      NULL);
+	webkit_web_view_set_settings (view, settings);
+}
+
+static void
+set_webkit_font_from_gtk_style (WebKitWebView *view)
+{
+	WebKitWebSettings *settings;
+	GtkStyle *style;
+	int font_size;
+	const char *font_family;
+
+	style = gtk_widget_get_style (GTK_WIDGET (view));
+	settings = webkit_web_view_get_settings (view);
+
+	font_size = pango_font_description_get_size (style->font_desc);
+	if (pango_font_description_get_size_is_absolute (style->font_desc) == FALSE)
+		font_size /= PANGO_SCALE;
+
+	font_family = pango_font_description_get_family (style->font_desc);
+
+	rb_debug ("setting font settings: %s / %d", font_family, font_size);
+	g_object_set (settings,
+		      "default-font-size", font_size,
+		      "default-monospace-font-size", font_size,
+		      "sans-serif-font-family", font_family,
+		      "monospace-font-family", font_family,
+		      NULL);
+}
+#endif
+
 static void
 rb_podcast_properties_dialog_init (RBPodcastPropertiesDialog *dialog)
 {
 	GtkWidget  *content_area;
+	GtkWidget  *bin;
+	GtkWidget  *widget;
 	GtkBuilder *builder;
 	AtkObject *lobj, *robj;
 
@@ -174,7 +266,32 @@ rb_podcast_properties_dialog_init (RBPodcastPropertiesDialog *dialog)
 	dialog->priv->playcount = GTK_WIDGET (gtk_builder_get_object (builder, "playcountLabel"));
 	dialog->priv->bitrate = GTK_WIDGET (gtk_builder_get_object (builder, "bitrateLabel"));
 	dialog->priv->date = GTK_WIDGET (gtk_builder_get_object (builder, "dateLabel"));
-	dialog->priv->description = GTK_WIDGET (gtk_builder_get_object (builder, "descriptionLabel"));
+#if defined(WITH_WEBKIT)
+	dialog->priv->description = webkit_web_view_new ();
+	set_webkit_settings (WEBKIT_WEB_VIEW (dialog->priv->description));
+	set_webkit_font_from_gtk_style (WEBKIT_WEB_VIEW (dialog->priv->description));
+
+	g_signal_connect_object (dialog->priv->description,
+				 "navigation-requested",
+				 G_CALLBACK (navigation_requested_cb),
+				 dialog,
+				 0);
+#else
+	dialog->priv->description = gtk_label_new (NULL);
+	gtk_label_set_line_wrap (GTK_LABEL (dialog->priv->description), TRUE);
+#endif
+	/* add relationship between the description label and the description widget */
+	widget = GTK_WIDGET (gtk_builder_get_object (builder, "descriptionDescLabel"));
+	gtk_label_set_mnemonic_widget (GTK_LABEL (widget), dialog->priv->description);
+	lobj = gtk_widget_get_accessible (widget);
+	robj = gtk_widget_get_accessible (dialog->priv->description);
+	atk_object_add_relationship (lobj, ATK_RELATION_LABEL_FOR, robj);
+	atk_object_add_relationship (robj, ATK_RELATION_LABELLED_BY, lobj);
+
+	bin = GTK_WIDGET (gtk_builder_get_object (builder, "descriptionViewport"));
+	gtk_container_add (GTK_CONTAINER (bin), dialog->priv->description);
+
+	dialog->priv->description_window = GTK_WIDGET (gtk_builder_get_object (builder, "descriptionWindow"));
 
 	rb_builder_boldify_label (builder, "titleDescLabel");
 	rb_builder_boldify_label (builder, "feedDescLabel");
@@ -518,13 +635,56 @@ rb_podcast_properties_dialog_update_date (RBPodcastPropertiesDialog *dialog)
 	g_free (time);
 }
 
+#if defined(WITH_WEBKIT)
+static gboolean
+update_scrollbar_policy_cb (WebKitWebFrame *frame, RBPodcastPropertiesDialog *dialog)
+{
+	gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (dialog->priv->description_window),
+					webkit_web_frame_get_horizontal_scrollbar_policy (frame),
+					webkit_web_frame_get_vertical_scrollbar_policy (frame));
+	return TRUE;
+}
+
+#endif
+
 static void
 rb_podcast_properties_dialog_update_description (RBPodcastPropertiesDialog *dialog)
 {
+#if defined(WITH_WEBKIT)
+	WebKitWebFrame *frame;
 	const char *str;
+	int i;
+	gboolean loaded = FALSE;
+	str = rhythmdb_entry_get_string (dialog->priv->current_entry, RHYTHMDB_PROP_DESCRIPTION);
+	for (i = 0; i < G_N_ELEMENTS (html_clues); i++) {
+		if (g_strstr_len (str, -1, html_clues[i]) != NULL) {
+			webkit_web_view_load_html_string (WEBKIT_WEB_VIEW (dialog->priv->description),
+							  str,
+							  "");
+			loaded = TRUE;
+		}
+	}
 
+	if (loaded == FALSE) {
+		webkit_web_view_load_string (WEBKIT_WEB_VIEW (dialog->priv->description),
+					     str,
+					     "text/plain",
+					     "utf-8",
+					     "");
+	}
+
+	/* ensure scrollbar policy for the frame matches the viewport */
+	frame = webkit_web_view_get_main_frame (WEBKIT_WEB_VIEW (dialog->priv->description));
+	g_signal_connect_object (frame,
+				 "scrollbars-policy-changed",
+				 G_CALLBACK (update_scrollbar_policy_cb),
+				 dialog, 0);
+	update_scrollbar_policy_cb (frame, dialog);
+#else
+	const char *str;
 	str = rhythmdb_entry_get_string (dialog->priv->current_entry, RHYTHMDB_PROP_DESCRIPTION);
 	gtk_label_set_text (GTK_LABEL (dialog->priv->description), str);
+#endif
 }
 
 static char *
diff --git a/shell/Makefile.am b/shell/Makefile.am
index 9dcbcb6..8f200bd 100644
--- a/shell/Makefile.am
+++ b/shell/Makefile.am
@@ -46,6 +46,7 @@ INCLUDES = 						\
 	$(TOTEM_PLPARSER_CFLAGS)			\
 	$(DBUS_CFLAGS)					\
 	$(GUDEV_CFLAGS)					\
+	$(WEBKIT_FLAGS)				\
 	-D_XOPEN_SOURCE -D__EXTENSIONS__ -D_BSD_SOURCE
 
 librhythmbox_core_la_SOURCES =				\
@@ -112,6 +113,7 @@ librhythmbox_core_la_LIBADD =				\
 	$(DBUS_LIBS)					\
 	$(TOTEM_PLPARSER_LIBS)				\
 	$(GUDEV_LIBS)					\
+	$(WEBKIT_LIBS)				\
 	$(RHYTHMBOX_LIBS)				\
 	-lgstpbutils-0.10				\
 	-lgstcontroller-0.10



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