summaryrefslogtreecommitdiff
path: root/help-viewer
diff options
context:
space:
mode:
authorLeandro A. F. Pereira <leandro@hardinfo.org>2010-05-03 09:27:26 -0300
committerLeandro A. F. Pereira <leandro@hardinfo.org>2010-05-03 21:08:06 -0300
commit9273c075a2f993c5154614b70233d8f74515c851 (patch)
treeeb72a8c58e6bc8f4ca3b739d28fbecc269c0052d /help-viewer
parent9a50155ec3e27aa6cedf3f118196f1947c769a29 (diff)
Move files from hardinfo2 to root.
Diffstat (limited to 'help-viewer')
-rw-r--r--help-viewer/egg-markdown.c1586
-rw-r--r--help-viewer/help-viewer.c514
-rw-r--r--help-viewer/markdown-text-view.c637
3 files changed, 2737 insertions, 0 deletions
diff --git a/help-viewer/egg-markdown.c b/help-viewer/egg-markdown.c
new file mode 100644
index 00000000..4056d4f9
--- /dev/null
+++ b/help-viewer/egg-markdown.c
@@ -0,0 +1,1586 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2009 Leandro Pereira <leandro@hardinfo.org>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <string.h>
+#include <glib.h>
+
+#include "egg-markdown.h"
+
+/*******************************************************************************
+ *
+ * This is a simple Markdown parser.
+ * It can output to Pango, HTML or plain text. The following limitations are
+ * already known, and properly deliberate:
+ *
+ * - No code section support
+ * - No ordered list support
+ * - No blockquote section support
+ * - No image support
+ * - No links or email support
+ * - No backslash escapes support
+ * - No HTML escaping support
+ * - Auto-escapes certain word patterns, like http://
+ *
+ * It does support the rest of the standard pretty well, although it's not
+ * been run against any conformance tests. The parsing is single pass, with
+ * a simple enumerated intepretor mode and a single line back-memory.
+ *
+ ******************************************************************************/
+
+static void egg_markdown_finalize (GObject *object);
+
+#define EGG_MARKDOWN_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), EGG_TYPE_MARKDOWN, EggMarkdownPrivate))
+
+typedef gchar *(EggMarkdownLinkBuilder)(gchar *title, gchar *uri, gint link_id);
+typedef gchar *(EggMarkdownImageBuilder)(gchar *alt_text, gchar *path, gint link_id);
+
+typedef enum {
+ EGG_MARKDOWN_MODE_BLANK,
+ EGG_MARKDOWN_MODE_RULE,
+ EGG_MARKDOWN_MODE_BULLETT,
+ EGG_MARKDOWN_MODE_PARA,
+ EGG_MARKDOWN_MODE_H1,
+ EGG_MARKDOWN_MODE_H2,
+ EGG_MARKDOWN_MODE_UNKNOWN
+} EggMarkdownMode;
+
+typedef struct {
+ const gchar *em_start;
+ const gchar *em_end;
+ const gchar *strong_start;
+ const gchar *strong_end;
+ const gchar *code_start;
+ const gchar *code_end;
+ const gchar *h1_start;
+ const gchar *h1_end;
+ const gchar *h2_start;
+ const gchar *h2_end;
+ const gchar *bullett_start;
+ const gchar *bullett_end;
+ const gchar *rule;
+
+ EggMarkdownLinkBuilder *link_builder;
+ EggMarkdownImageBuilder *image_builder;
+} EggMarkdownTags;
+
+struct EggMarkdownPrivate
+{
+ EggMarkdownMode mode;
+ EggMarkdownTags tags;
+ EggMarkdownOutput output;
+ gint max_lines;
+ guint line_count;
+ gboolean smart_quoting;
+ gboolean escape;
+ gboolean autocode;
+ GString *pending;
+ GString *processed;
+ GArray *link_table;
+};
+
+G_DEFINE_TYPE (EggMarkdown, egg_markdown, G_TYPE_OBJECT)
+
+/**
+ * egg_markdown_to_text_line_is_rule:
+ *
+ * Horizontal rules are created by placing three or more hyphens, asterisks,
+ * or underscores on a line by themselves.
+ * You may use spaces between the hyphens or asterisks.
+ **/
+static gboolean
+egg_markdown_to_text_line_is_rule (const gchar *line)
+{
+ guint i;
+ guint len;
+ guint count = 0;
+ gchar *copy = NULL;
+ gboolean ret = FALSE;
+
+ len = strnlen (line, EGG_MARKDOWN_MAX_LINE_LENGTH);
+ if (len == 0)
+ goto out;
+
+ /* replace non-rule chars with ~ */
+ copy = g_strdup (line);
+ g_strcanon (copy, "-*_ ", '~');
+ for (i=0; i<len; i++) {
+ if (copy[i] == '~')
+ goto out;
+ if (copy[i] != ' ')
+ count++;
+ }
+
+ /* if we matched, return true */
+ if (count >= 3)
+ ret = TRUE;
+out:
+ g_free (copy);
+ return ret;
+}
+
+/**
+ * egg_markdown_to_text_line_is_bullett:
+ **/
+static gboolean
+egg_markdown_to_text_line_is_bullett (const gchar *line)
+{
+ return (g_str_has_prefix (line, "- ") ||
+ g_str_has_prefix (line, "* ") ||
+ g_str_has_prefix (line, "+ ") ||
+ g_str_has_prefix (line, " - ") ||
+ g_str_has_prefix (line, " * ") ||
+ g_str_has_prefix (line, " + "));
+}
+
+/**
+ * egg_markdown_to_text_line_is_header1:
+ **/
+static gboolean
+egg_markdown_to_text_line_is_header1 (const gchar *line)
+{
+ return g_str_has_prefix (line, "# ");
+}
+
+/**
+ * egg_markdown_to_text_line_is_header2:
+ **/
+static gboolean
+egg_markdown_to_text_line_is_header2 (const gchar *line)
+{
+ return g_str_has_prefix (line, "## ");
+}
+
+/**
+ * egg_markdown_to_text_line_is_header1_type2:
+ **/
+static gboolean
+egg_markdown_to_text_line_is_header1_type2 (const gchar *line)
+{
+ return g_str_has_prefix (line, "===");
+}
+
+/**
+ * egg_markdown_to_text_line_is_header2_type2:
+ **/
+static gboolean
+egg_markdown_to_text_line_is_header2_type2 (const gchar *line)
+{
+ return g_str_has_prefix (line, "---");
+}
+
+#if 0
+/**
+ * egg_markdown_to_text_line_is_code:
+ **/
+static gboolean
+egg_markdown_to_text_line_is_code (const gchar *line)
+{
+ return (g_str_has_prefix (line, " ") || g_str_has_prefix (line, "\t"));
+}
+
+/**
+ * egg_markdown_to_text_line_is_blockquote:
+ **/
+static gboolean
+egg_markdown_to_text_line_is_blockquote (const gchar *line)
+{
+ return (g_str_has_prefix (line, "> "));
+}
+#endif
+
+/**
+ * egg_markdown_to_text_line_is_blank:
+ **/
+static gboolean
+egg_markdown_to_text_line_is_blank (const gchar *line)
+{
+ guint i;
+ guint len;
+ gboolean ret = FALSE;
+
+ len = strnlen (line, EGG_MARKDOWN_MAX_LINE_LENGTH);
+
+ /* a line with no characters is blank by definition */
+ if (len == 0) {
+ ret = TRUE;
+ goto out;
+ }
+
+ /* find if there are only space chars */
+ for (i=0; i<len; i++) {
+ if (line[i] != ' ' && line[i] != '\t')
+ goto out;
+ }
+
+ /* if we matched, return true */
+ ret = TRUE;
+out:
+ return ret;
+}
+
+/**
+ * egg_markdown_replace:
+ **/
+static gchar *
+egg_markdown_replace (const gchar *haystack, const gchar *needle, const gchar *replace)
+{
+ gchar *new;
+ gchar **split;
+
+ split = g_strsplit (haystack, needle, -1);
+ new = g_strjoinv (replace, split);
+ g_strfreev (split);
+
+ return new;
+}
+
+/**
+ * egg_markdown_strstr_spaces:
+ **/
+static gchar *
+egg_markdown_strstr_spaces (const gchar *haystack, const gchar *needle)
+{
+ gchar *found;
+ const gchar *haystack_new = haystack;
+
+retry:
+ /* don't find if surrounded by spaces */
+ found = strstr (haystack_new, needle);
+ if (found == NULL)
+ return NULL;
+
+ /* start of the string, always valid */
+ if (found == haystack)
+ return found;
+
+ /* end of the string, always valid */
+ if (*(found-1) == ' ' && *(found+1) == ' ') {
+ haystack_new = found+1;
+ goto retry;
+ }
+ return found;
+}
+
+
+/**
+ * egg_markdown_to_text_line_formatter:
+ **/
+static gchar *
+egg_markdown_to_text_line_formatter (const gchar *line, const gchar *formatter, const gchar *left, const gchar *right)
+{
+ guint len;
+ gchar *str1;
+ gchar *str2;
+ gchar *start = NULL;
+ gchar *middle = NULL;
+ gchar *end = NULL;
+ gchar *copy = NULL;
+ gchar *data = NULL;
+ gchar *temp;
+
+ /* needed to know for shifts */
+ len = strnlen (formatter, EGG_MARKDOWN_MAX_LINE_LENGTH);
+ if (len == 0)
+ goto out;
+
+ /* find sections */
+ copy = g_strdup (line);
+ str1 = egg_markdown_strstr_spaces (copy, formatter);
+ if (str1 != NULL) {
+ *str1 = '\0';
+ str2 = egg_markdown_strstr_spaces (str1+len, formatter);
+ if (str2 != NULL) {
+ *str2 = '\0';
+ middle = str1 + len;
+ start = copy;
+ end = str2 + len;
+ }
+ }
+
+ /* if we found, replace and keep looking for the same string */
+ if (start != NULL && middle != NULL && end != NULL) {
+ temp = g_strdup_printf ("%s%s%s%s%s", start, left, middle, right, end);
+ /* recursive */
+ data = egg_markdown_to_text_line_formatter (temp, formatter, left, right);
+ g_free (temp);
+ } else {
+ /* not found, keep return as-is */
+ data = g_strdup (line);
+ }
+out:
+ g_free (copy);
+ return data;
+}
+
+static gchar *
+egg_markdown_to_text_line_formatter_image (EggMarkdown *self, const gchar *line)
+{
+ const guint len = 2; /* needed to know for shifts */
+ gchar *str1;
+ gchar *str2;
+ gchar *start = NULL;
+ gchar *path = NULL;
+ gchar *alt_text = NULL;
+ gchar *end = NULL;
+ gchar *copy = NULL;
+ gchar *data = NULL;
+
+ /* find sections */
+ copy = g_strdup (line);
+ str1 = egg_markdown_strstr_spaces (copy, "![");
+ if (str1 != NULL) {
+ *str1 = '\0';
+ str2 = egg_markdown_strstr_spaces (str1+len, "]");
+ if (str2 != NULL) {
+ *str2 = '\0';
+ start = copy;
+ alt_text = str1 + len;
+
+ str2 = strstr (str2 + 1, "(");
+ if (str2 != NULL) {
+ *str2 = '\0';
+
+ str1 = strstr (str2 + 1, ")");
+ if (str1 != NULL) {
+ *str1 = '\0';
+ path = str2 + 1;
+ end = str1 + 1;
+ }
+ }
+ }
+ }
+
+ /* if we found, replace and keep looking for the same string */
+ if (start && (path && *path) && alt_text && end) {
+ gchar *formatted_img;
+ gchar *path_copy = g_strdup(path);
+
+ g_array_append_val(self->priv->link_table, path_copy);
+
+ formatted_img = self->priv->tags.image_builder(alt_text,
+ path,
+ self->priv->link_table->len - 1);
+
+ data = g_strdup_printf ("%s%s%s",
+ start, formatted_img, end);
+
+ g_free(formatted_img);
+ } else {
+ /* not found, keep return as-is */
+ data = g_strdup (line);
+ }
+
+ g_free (copy);
+ return data;
+}
+
+
+/**
+ * egg_markdown_to_text_line_formatter_link:
+ **/
+static gchar *
+egg_markdown_to_text_line_formatter_link (EggMarkdown *self, const gchar *line)
+{
+ const guint len = 1; /* needed to know for shifts */
+ gchar *str1;
+ gchar *str2;
+ gchar *start = NULL;
+ gchar *link = NULL;
+ gchar *link_title = NULL;
+ gchar *end = NULL;
+ gchar *copy = NULL;
+ gchar *data = NULL;
+
+ /* find sections */
+ copy = g_strdup (line);
+ str1 = egg_markdown_strstr_spaces (copy, "[");
+ if (str1 != NULL) {
+ *str1 = '\0';
+ str2 = egg_markdown_strstr_spaces (str1+len, "]");
+ if (str2 != NULL) {
+ *str2 = '\0';
+ start = copy;
+ link = str1 + len;
+ end = str2 + len;
+
+ str2 = strstr (link, " ");
+ if (str2 != NULL) {
+ *str2 = '\0';
+ link_title = str2 + len;
+ }
+ }
+ }
+
+ /* if we found, replace and keep looking for the same string */
+ if (start && (link && *link) && link_title && end) {
+ gchar *formatted_link;
+ gchar *link_copy = g_strdup(link);
+
+ g_array_append_val(self->priv->link_table, link_copy);
+
+ formatted_link = self->priv->tags.link_builder(link_title,
+ link,
+ self->priv->link_table->len - 1);
+
+ data = g_strdup_printf ("%s%s%s",
+ start, formatted_link, end);
+
+ g_free(formatted_link);
+ } else {
+ /* not found, keep return as-is */
+ data = g_strdup (line);
+ }
+
+ g_free (copy);
+ return data;
+}
+
+void
+egg_markdown_clear(EggMarkdown *self)
+{
+ int i;
+
+ for (i = 0; i < self->priv->link_table->len; i++) {
+ g_free(g_array_index(self->priv->link_table, gchar *, i));
+ }
+
+ g_array_free(self->priv->link_table, TRUE);
+ self->priv->link_table = g_array_new(FALSE, FALSE, sizeof(gchar *));
+}
+
+gchar *
+egg_markdown_get_link_uri(EggMarkdown *self, const gint link_id)
+{
+ g_return_val_if_fail(link_id < self->priv->link_table->len, NULL);
+
+ return g_strdup(g_array_index(self->priv->link_table, gchar *, link_id));
+}
+
+/**
+ * egg_markdown_to_text_line_format_sections:
+ **/
+static gchar *
+egg_markdown_to_text_line_format_sections (EggMarkdown *self, const gchar *line)
+{
+ gchar *data = g_strdup (line);
+ gchar *temp;
+
+ /* smart quoting */
+ if (self->priv->smart_quoting) {
+ if (self->priv->escape) {
+ temp = data;
+ data = egg_markdown_to_text_line_formatter (temp, "&quot;", "“", "”");
+ g_free (temp);
+
+ temp = data;
+ data = egg_markdown_to_text_line_formatter (temp, "&apos;", "‘", "’");
+ g_free (temp);
+ } else {
+ temp = data;
+ data = egg_markdown_to_text_line_formatter (temp, "\"", "“", "”");
+ g_free (temp);
+
+ temp = data;
+ data = egg_markdown_to_text_line_formatter (temp, "'", "‘", "’");
+ g_free (temp);
+ }
+ }
+
+ /* image */
+ temp = data;
+ data = egg_markdown_to_text_line_formatter_image (self, temp);
+ g_free(temp);
+
+ /* link */
+ temp = data;
+ data = egg_markdown_to_text_line_formatter_link (self, temp);
+ g_free(temp);
+
+ /* bold1 */
+ temp = data;
+ data = egg_markdown_to_text_line_formatter (temp, "**", self->priv->tags.strong_start, self->priv->tags.strong_end);
+ g_free (temp);
+
+ /* bold2 */
+ temp = data;
+ data = egg_markdown_to_text_line_formatter (temp, "__", self->priv->tags.strong_start, self->priv->tags.strong_end);
+ g_free (temp);
+
+ /* italic1 */
+ temp = data;
+ data = egg_markdown_to_text_line_formatter (temp, "*", self->priv->tags.em_start, self->priv->tags.em_end);
+ g_free (temp);
+
+ /* italic2 */
+ temp = data;
+ data = egg_markdown_to_text_line_formatter (temp, "_", self->priv->tags.em_start, self->priv->tags.em_end);
+ g_free (temp);
+
+ /* em-dash */
+ temp = data;
+ data = egg_markdown_replace (temp, " -- ", " — ");
+ g_free (temp);
+
+ return data;
+}
+
+/**
+ * egg_markdown_to_text_line_format:
+ **/
+static gchar *
+egg_markdown_to_text_line_format (EggMarkdown *self, const gchar *line)
+{
+ guint i;
+ gchar *text;
+ gboolean mode = FALSE;
+ gchar **codes;
+ GString *string;
+
+ /* optimise the trivial case where we don't have any code tags */
+ text = strstr (line, "`");
+ if (text == NULL) {
+ text = egg_markdown_to_text_line_format_sections (self, line);
+ goto out;
+ }
+
+ /* we want to parse the code sections without formatting */
+ codes = g_strsplit (line, "`", -1);
+ string = g_string_new ("");
+ for (i=0; codes[i] != NULL; i++) {
+ if (!mode) {
+ text = egg_markdown_to_text_line_format_sections (self, codes[i]);
+ g_string_append (string, text);
+ g_free (text);
+ mode = TRUE;
+ } else {
+ /* just append without formatting */
+ g_string_append (string, self->priv->tags.code_start);
+ g_string_append (string, codes[i]);
+ g_string_append (string, self->priv->tags.code_end);
+ mode = FALSE;
+ }
+ }
+ text = g_string_free (string, FALSE);
+out:
+ return text;
+}
+
+/**
+ * egg_markdown_add_pending:
+ **/
+static gboolean
+egg_markdown_add_pending (EggMarkdown *self, const gchar *line)
+{
+ gchar *copy;
+
+ /* would put us over the limit */
+ if (self->priv->line_count >= self->priv->max_lines)
+ return FALSE;
+
+ copy = g_strdup (line);
+
+ /* strip leading and trailing spaces */
+ g_strstrip (copy);
+
+ /* append */
+ g_string_append_printf (self->priv->pending, "%s ", copy);
+
+ g_free (copy);
+ return TRUE;
+}
+
+/**
+ * egg_markdown_add_pending_header:
+ **/
+static gboolean
+egg_markdown_add_pending_header (EggMarkdown *self, const gchar *line)
+{
+ gchar *copy;
+ gboolean ret;
+
+ /* strip trailing # */
+ copy = g_strdup (line);
+ g_strdelimit (copy, "#", ' ');
+ ret = egg_markdown_add_pending (self, copy);
+ g_free (copy);
+ return ret;
+}
+
+/**
+ * egg_markdown_count_chars_in_word:
+ **/
+static guint
+egg_markdown_count_chars_in_word (const gchar *text, gchar find)
+{
+ guint i;
+ guint len;
+ guint count = 0;
+
+ /* get length */
+ len = strnlen (text, EGG_MARKDOWN_MAX_LINE_LENGTH);
+ if (len == 0)
+ goto out;
+
+ /* find matching chars */
+ for (i=0; i<len; i++) {
+ if (text[i] == find)
+ count++;
+ }
+out:
+ return count;
+}
+
+/**
+ * egg_markdown_word_is_code:
+ **/
+static gboolean
+egg_markdown_word_is_code (const gchar *text)
+{
+ /* already code */
+ if (g_str_has_prefix (text, "`"))
+ return FALSE;
+ if (g_str_has_suffix (text, "`"))
+ return FALSE;
+
+ /* paths */
+ if (g_str_has_prefix (text, "/"))
+ return TRUE;
+
+ /* bugzillas */
+ if (g_str_has_prefix (text, "#"))
+ return TRUE;
+
+ /* uri's */
+ if (g_str_has_prefix (text, "http://"))
+ return TRUE;
+ if (g_str_has_prefix (text, "https://"))
+ return TRUE;
+ if (g_str_has_prefix (text, "ftp://"))
+ return TRUE;
+
+ /* patch files */
+ if (g_strrstr (text, ".patch") != NULL)
+ return TRUE;
+ if (g_strrstr (text, ".diff") != NULL)
+ return TRUE;
+
+ /* function names */
+ if (g_strrstr (text, "()") != NULL)
+ return TRUE;
+
+ /* email addresses */
+ if (g_strrstr (text, "@") != NULL)
+ return TRUE;
+
+ /* compiler defines */
+ if (text[0] != '_' &&
+ egg_markdown_count_chars_in_word (text, '_') > 1)
+ return TRUE;
+
+ /* nothing special */
+ return FALSE;
+}
+
+/**
+ * egg_markdown_word_auto_format_code:
+ **/
+static gchar *
+egg_markdown_word_auto_format_code (const gchar *text)
+{
+ guint i;
+ gchar *temp;
+ gchar **words;
+ gboolean ret = FALSE;
+
+ /* split sentence up with space */
+ words = g_strsplit (text, " ", -1);
+
+ /* search each word */
+ for (i=0; words[i] != NULL; i++) {
+ if (egg_markdown_word_is_code (words[i])) {
+ temp = g_strdup_printf ("`%s`", words[i]);
+ g_free (words[i]);
+ words[i] = temp;
+ ret = TRUE;
+ }
+ }
+
+ /* no replacements, so just return a copy */
+ if (!ret) {
+ temp = g_strdup (text);
+ goto out;
+ }
+
+ /* join the array back into a string */
+ temp = g_strjoinv (" ", words);
+out:
+ g_strfreev (words);
+ return temp;
+}
+
+/**
+ * egg_markdown_flush_pending:
+ **/
+static void
+egg_markdown_flush_pending (EggMarkdown *self)
+{
+ gchar *copy;
+ gchar *temp;
+
+ /* no data yet */
+ if (self->priv->mode == EGG_MARKDOWN_MODE_UNKNOWN)
+ return;
+
+ /* remove trailing spaces */
+ while (g_str_has_suffix (self->priv->pending->str, " "))
+ g_string_set_size (self->priv->pending, self->priv->pending->len - 1);
+
+ /* pango requires escaping */
+ copy = g_strdup (self->priv->pending->str);
+ if (!self->priv->escape && self->priv->output == EGG_MARKDOWN_OUTPUT_PANGO) {
+ g_strdelimit (copy, "<", '(');
+ g_strdelimit (copy, ">", ')');
+ }
+
+ /* check words for code */
+ if (self->priv->autocode &&
+ (self->priv->mode == EGG_MARKDOWN_MODE_PARA ||
+ self->priv->mode == EGG_MARKDOWN_MODE_BULLETT)) {
+ temp = egg_markdown_word_auto_format_code (copy);
+ g_free (copy);
+ copy = temp;
+ }
+
+ /* escape */
+ if (self->priv->escape) {
+ temp = g_markup_escape_text (copy, -1);
+ g_free (copy);
+ copy = temp;
+ }
+
+ /* do formatting */
+ temp = egg_markdown_to_text_line_format (self, copy);
+ if (self->priv->mode == EGG_MARKDOWN_MODE_BULLETT) {
+ g_string_append_printf (self->priv->processed, "%s%s%s\n", self->priv->tags.bullett_start, temp, self->priv->tags.bullett_end);
+ self->priv->line_count++;
+ } else if (self->priv->mode == EGG_MARKDOWN_MODE_H1) {
+ g_string_append_printf (self->priv->processed, "%s%s%s\n", self->priv->tags.h1_start, temp, self->priv->tags.h1_end);
+ } else if (self->priv->mode == EGG_MARKDOWN_MODE_H2) {
+ g_string_append_printf (self->priv->processed, "%s%s%s\n", self->priv->tags.h2_start, temp, self->priv->tags.h2_end);
+ } else if (self->priv->mode == EGG_MARKDOWN_MODE_PARA ||
+ self->priv->mode == EGG_MARKDOWN_MODE_RULE) {
+ g_string_append_printf (self->priv->processed, "%s\n", temp);
+ self->priv->line_count++;
+ }
+
+ DEBUG ("adding '%s'", temp);
+
+ /* clear */
+ g_string_truncate (self->priv->pending, 0);
+ g_free (copy);
+ g_free (temp);
+}
+
+/**
+ * egg_markdown_to_text_line_process:
+ **/
+static gboolean
+egg_markdown_to_text_line_process (EggMarkdown *self, const gchar *line)
+{
+ gboolean ret;
+
+ /* blank */
+ ret = egg_markdown_to_text_line_is_blank (line);
+ if (ret) {
+ DEBUG ("blank: '%s'", line);
+ egg_markdown_flush_pending (self);
+ /* a new line after a list is the end of list, not a gap */
+ if (self->priv->mode != EGG_MARKDOWN_MODE_BULLETT)
+ ret = egg_markdown_add_pending (self, "\n");
+ self->priv->mode = EGG_MARKDOWN_MODE_BLANK;
+ goto out;
+ }
+
+ /* header1_type2 */
+ ret = egg_markdown_to_text_line_is_header1_type2 (line);
+ if (ret) {
+ DEBUG ("header1_type2: '%s'", line);
+ if (self->priv->mode == EGG_MARKDOWN_MODE_PARA)
+ self->priv->mode = EGG_MARKDOWN_MODE_H1;
+ goto out;
+ }
+
+ /* header2_type2 */
+ ret = egg_markdown_to_text_line_is_header2_type2 (line);
+ if (ret) {
+ DEBUG ("header2_type2: '%s'", line);
+ if (self->priv->mode == EGG_MARKDOWN_MODE_PARA)
+ self->priv->mode = EGG_MARKDOWN_MODE_H2;
+ goto out;
+ }
+
+ /* rule */
+ ret = egg_markdown_to_text_line_is_rule (line);
+ if (ret) {
+ DEBUG ("rule: '%s'", line);
+ egg_markdown_flush_pending (self);
+ self->priv->mode = EGG_MARKDOWN_MODE_RULE;
+ ret = egg_markdown_add_pending (self, self->priv->tags.rule);
+ goto out;
+ }
+
+ /* bullett */
+ ret = egg_markdown_to_text_line_is_bullett (line);
+ if (ret) {
+ DEBUG ("bullett: '%s'", line);
+ egg_markdown_flush_pending (self);
+ self->priv->mode = EGG_MARKDOWN_MODE_BULLETT;
+ ret = egg_markdown_add_pending (self, &line[2]);
+ goto out;
+ }
+
+ /* header1 */
+ ret = egg_markdown_to_text_line_is_header1 (line);
+ if (ret) {
+ DEBUG ("header1: '%s'", line);
+ egg_markdown_flush_pending (self);
+ self->priv->mode = EGG_MARKDOWN_MODE_H1;
+ ret = egg_markdown_add_pending_header (self, &line[2]);
+ goto out;
+ }
+
+ /* header2 */
+ ret = egg_markdown_to_text_line_is_header2 (line);
+ if (ret) {
+ DEBUG ("header2: '%s'", line);
+ egg_markdown_flush_pending (self);
+ self->priv->mode = EGG_MARKDOWN_MODE_H2;
+ ret = egg_markdown_add_pending_header (self, &line[3]);
+ goto out;
+ }
+
+ /* paragraph */
+ if (self->priv->mode == EGG_MARKDOWN_MODE_BLANK || self->priv->mode == EGG_MARKDOWN_MODE_UNKNOWN) {
+ egg_markdown_flush_pending (self);
+ self->priv->mode = EGG_MARKDOWN_MODE_PARA;
+ }
+
+ /* add to pending */
+ DEBUG ("continue: '%s'", line);
+ ret = egg_markdown_add_pending (self, line);
+out:
+ /* if we failed to add, we don't know the mode */
+ if (!ret)
+ self->priv->mode = EGG_MARKDOWN_MODE_UNKNOWN;
+ return ret;
+}
+
+/**
+ * egg_markdown_linkbuilder_pango:
+ **/
+static gchar *
+egg_markdown_linkbuilder_pango (gchar *title, gchar *uri, gint link_id)
+{
+ /* FIXME: This is a nasty hack, since extending Pango markup to allow new tags
+ * is too complicated. We use the language code as a link index
+ * since it won't allow anything besides letters or numbers.
+ * To obtain the link URI, use egg_markdown_get_link_uri(). */
+ return g_strdup_printf("<span lang=\"%d\" foreground=\"blue\"><u>%s</u></span>",
+ link_id, title);
+}
+
+/**
+ * egg_markdown_linkbuilder_html
+ **/
+static gchar *
+egg_markdown_linkbuilder_html (gchar *title, gchar *uri, gint link_id)
+{
+ return g_strdup_printf("<a href=\"%s\">%s</a>", uri, title);
+}
+
+/**
+ * egg_markdown_linkbuilder_text
+ **/
+static gchar *
+egg_markdown_linkbuilder_text (gchar *title, gchar *uri, gint link_id)
+{
+ return g_strdup_printf("%s (%s)", title, uri);
+}
+
+/**
+ * egg_markdown_imagebuilder_pango:
+ **/
+static gchar *
+egg_markdown_imagebuilder_pango (gchar *alt_text, gchar *uri, gint link_id)
+{
+ /* FIXME See egg_markdown_linkbuilder_pango() */
+ return g_strdup_printf("<span lang=\"%d\" underline=\"double\">%s</span>",
+ link_id, alt_text);
+}
+
+/**
+ * egg_markdown_imagebuilder_html
+ **/
+static gchar *
+egg_markdown_imagebuilder_html (gchar *alt_text, gchar *uri, gint link_id)
+{
+ return g_strdup_printf("<img src=\"%s\" alt=\"%s\">", uri, alt_text);
+}
+
+/**
+ * egg_markdown_imagebuilder_text
+ **/
+static gchar *
+egg_markdown_imagebuilder_text (gchar *alt_text, gchar *uri, gint link_id)
+{
+ return g_strdup(alt_text);
+}
+
+/**
+ * egg_markdown_set_output:
+ **/
+gboolean
+egg_markdown_set_output (EggMarkdown *self, EggMarkdownOutput output)
+{
+ gboolean ret = TRUE;
+ g_return_val_if_fail (EGG_IS_MARKDOWN (self), FALSE);
+
+ /* PangoMarkup */
+ if (output == EGG_MARKDOWN_OUTPUT_PANGO) {
+ self->priv->tags.em_start = "<i>";
+ self->priv->tags.em_end = "</i>";
+ self->priv->tags.strong_start = "<b>";
+ self->priv->tags.strong_end = "</b>";
+ self->priv->tags.code_start = "<tt><span bgcolor=\"#eee\">";
+ self->priv->tags.code_end = "</span></tt>";
+ self->priv->tags.h1_start = "<span color=\"#444\" size=\"xx-large\"><b>";
+ self->priv->tags.h1_end = "</b></span>";
+ self->priv->tags.h2_start = "<big><b>";
+ self->priv->tags.h2_end = "</b></big>";
+ self->priv->tags.bullett_start = " • ";
+ self->priv->tags.bullett_end = "";
+ self->priv->tags.rule = "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯\n";
+ self->priv->tags.link_builder = egg_markdown_linkbuilder_pango;
+ self->priv->tags.image_builder = egg_markdown_imagebuilder_pango;
+
+ /* XHTML */
+ } else if (output == EGG_MARKDOWN_OUTPUT_HTML) {
+ self->priv->tags.em_start = "<em>";
+ self->priv->tags.em_end = "<em>";
+ self->priv->tags.strong_start = "<strong>";
+ self->priv->tags.strong_end = "</strong>";
+ self->priv->tags.code_start = "<code>";
+ self->priv->tags.code_end = "</code>";
+ self->priv->tags.h1_start = "<h1>";
+ self->priv->tags.h1_end = "</h1>";
+ self->priv->tags.h2_start = "<h2>";
+ self->priv->tags.h2_end = "</h2>";
+ self->priv->tags.bullett_start = "<li>";
+ self->priv->tags.bullett_end = "</li>";
+ self->priv->tags.rule = "<hr>";
+ self->priv->tags.link_builder = egg_markdown_linkbuilder_html;
+ self->priv->tags.image_builder = egg_markdown_imagebuilder_html;
+
+ /* plain text */
+ } else if (output == EGG_MARKDOWN_OUTPUT_TEXT) {
+ self->priv->tags.em_start = "";
+ self->priv->tags.em_end = "";
+ self->priv->tags.strong_start = "";
+ self->priv->tags.strong_end = "";
+ self->priv->tags.code_start = "";
+ self->priv->tags.code_end = "";
+ self->priv->tags.h1_start = "[";
+ self->priv->tags.h1_end = "]";
+ self->priv->tags.h2_start = "-";
+ self->priv->tags.h2_end = "-";
+ self->priv->tags.bullett_start = "* ";
+ self->priv->tags.bullett_end = "";
+ self->priv->tags.rule = " ----- \n";
+ self->priv->tags.link_builder = egg_markdown_linkbuilder_text;
+ self->priv->tags.image_builder = egg_markdown_imagebuilder_text;
+
+ /* unknown */
+ } else {
+ g_warning ("unknown output enum");
+ ret = FALSE;
+ }
+
+ /* save if valid */
+ if (ret)
+ self->priv->output = output;
+ return ret;
+}
+
+/**
+ * egg_markdown_set_max_lines:
+ **/
+gboolean
+egg_markdown_set_max_lines (EggMarkdown *self, gint max_lines)
+{
+ g_return_val_if_fail (EGG_IS_MARKDOWN (self), FALSE);
+ self->priv->max_lines = max_lines;
+ return TRUE;
+}
+
+/**
+ * egg_markdown_set_smart_quoting:
+ **/
+gboolean
+egg_markdown_set_smart_quoting (EggMarkdown *self, gboolean smart_quoting)
+{
+ g_return_val_if_fail (EGG_IS_MARKDOWN (self), FALSE);
+ self->priv->smart_quoting = smart_quoting;
+ return TRUE;
+}
+
+/**
+ * egg_markdown_set_escape:
+ **/
+gboolean
+egg_markdown_set_escape (EggMarkdown *self, gboolean escape)
+{
+ g_return_val_if_fail (EGG_IS_MARKDOWN (self), FALSE);
+ self->priv->escape = escape;
+ return TRUE;
+}
+
+/**
+ * egg_markdown_set_autocode:
+ **/
+gboolean
+egg_markdown_set_autocode (EggMarkdown *self, gboolean autocode)
+{
+ g_return_val_if_fail (EGG_IS_MARKDOWN (self), FALSE);
+ self->priv->autocode = autocode;
+ return TRUE;
+}
+
+/**
+ * egg_markdown_parse:
+ **/
+gchar *
+egg_markdown_parse (EggMarkdown *self, const gchar *markdown)
+{
+ gchar **lines;
+ guint i;
+ guint len;
+ gchar *temp;
+ gboolean ret;
+
+ g_return_val_if_fail (EGG_IS_MARKDOWN (self), NULL);
+ g_return_val_if_fail (self->priv->output != EGG_MARKDOWN_OUTPUT_UNKNOWN, NULL);
+
+ DEBUG ("input='%s'", markdown);
+
+ /* process */
+ self->priv->mode = EGG_MARKDOWN_MODE_UNKNOWN;
+ self->priv->line_count = 0;
+ g_string_truncate (self->priv->pending, 0);
+ g_string_truncate (self->priv->processed, 0);
+ lines = g_strsplit (markdown, "\n", -1);
+ len = g_strv_length (lines);
+
+ /* process each line */
+ for (i=0; i<len; i++) {
+ ret = egg_markdown_to_text_line_process (self, lines[i]);
+ if (!ret)
+ break;
+ }
+ g_strfreev (lines);
+ egg_markdown_flush_pending (self);
+
+ /* remove trailing \n */
+ while (g_str_has_suffix (self->priv->processed->str, "\n"))
+ g_string_set_size (self->priv->processed, self->priv->processed->len - 1);
+
+ /* get a copy */
+ temp = g_strdup (self->priv->processed->str);
+ g_string_truncate (self->priv->pending, 0);
+ g_string_truncate (self->priv->processed, 0);
+
+ DEBUG ("output='%s'", temp);
+
+ return temp;
+}
+
+/**
+ * egg_markdown_class_init:
+ * @klass: The EggMarkdownClass
+ **/
+static void
+egg_markdown_class_init (EggMarkdownClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->finalize = egg_markdown_finalize;
+ g_type_class_add_private (klass, sizeof (EggMarkdownPrivate));
+}
+
+/**
+ * egg_markdown_init:
+ **/
+static void
+egg_markdown_init (EggMarkdown *self)
+{
+ self->priv = EGG_MARKDOWN_GET_PRIVATE (self);
+
+ self->priv->mode = EGG_MARKDOWN_MODE_UNKNOWN;
+ self->priv->output = EGG_MARKDOWN_OUTPUT_UNKNOWN;
+ self->priv->pending = g_string_new ("");
+ self->priv->processed = g_string_new ("");
+ self->priv->link_table = g_array_new(FALSE, FALSE, sizeof(gchar *));
+ self->priv->max_lines = -1;
+ self->priv->smart_quoting = FALSE;
+ self->priv->escape = FALSE;
+ self->priv->autocode = FALSE;
+}
+
+/**
+ * egg_markdown_finalize:
+ * @object: The object to finalize
+ **/
+static void
+egg_markdown_finalize (GObject *object)
+{
+ EggMarkdown *self;
+ int i;
+
+ g_return_if_fail (EGG_IS_MARKDOWN (object));
+
+ self = EGG_MARKDOWN (object);
+
+ g_return_if_fail (self->priv != NULL);
+ g_string_free (self->priv->pending, TRUE);
+ g_string_free (self->priv->processed, TRUE);
+
+ for (i = 0; i < self->priv->link_table->len; i++) {
+ g_free(g_array_index(self->priv->link_table, gchar *, i));
+ }
+ g_array_free (self->priv->link_table, TRUE);
+
+ G_OBJECT_CLASS (egg_markdown_parent_class)->finalize (object);
+}
+
+/**
+ * egg_markdown_new:
+ *
+ * Return value: a new EggMarkdown object.
+ **/
+EggMarkdown *
+egg_markdown_new (void)
+{
+ EggMarkdown *self;
+ self = g_object_new (EGG_TYPE_MARKDOWN, NULL);
+ return EGG_MARKDOWN (self);
+}
+
+/***************************************************************************
+ *** MAKE CHECK TESTS ***
+ ***************************************************************************/
+#ifdef EGG_TEST
+#include "egg-test.h"
+
+void
+egg_markdown_test (EggTest *test)
+{
+ EggMarkdown *self;
+ gchar *text;
+ gboolean ret;
+ const gchar *markdown;
+ const gchar *markdown_expected;
+
+ if (!egg_test_start (test, "EggMarkdown"))
+ return;
+
+ /************************************************************
+ **************** line_is_rule **************
+ ************************************************************/
+ ret = egg_markdown_to_text_line_is_rule ("* * *");
+ egg_test_title_assert (test, "is rule (1)", ret);
+
+ /************************************************************/
+ ret = egg_markdown_to_text_line_is_rule ("***");
+ egg_test_title_assert (test, "is rule (2)", ret);
+
+ /************************************************************/
+ ret = egg_markdown_to_text_line_is_rule ("*****");
+ egg_test_title_assert (test, "is rule (3)", ret);
+
+ /************************************************************/
+ ret = egg_markdown_to_text_line_is_rule ("- - -");
+ egg_test_title_assert (test, "is rule (4)", ret);
+
+ /************************************************************/
+ ret = egg_markdown_to_text_line_is_rule ("---------------------------------------");
+ egg_test_title_assert (test, "is rule (5)", ret);
+
+ /************************************************************/
+ ret = egg_markdown_to_text_line_is_rule ("");
+ egg_test_title_assert (test, "is rule (blank)", !ret);
+
+ /************************************************************/
+ ret = egg_markdown_to_text_line_is_rule ("richard hughes");
+ egg_test_title_assert (test, "is rule (text)", !ret);
+
+ /************************************************************/
+ ret = egg_markdown_to_text_line_is_rule ("- richard-hughes");
+ egg_test_title_assert (test, "is rule (bullet)", !ret);
+
+ /************************************************************/
+ ret = egg_markdown_to_text_line_is_blank ("");
+ egg_test_title_assert (test, "is blank (blank)", ret);
+
+ /************************************************************/
+ ret = egg_markdown_to_text_line_is_blank (" \t ");
+ egg_test_title_assert (test, "is blank (mix)", ret);
+
+ /************************************************************/
+ ret = egg_markdown_to_text_line_is_blank ("richard hughes");
+ egg_test_title_assert (test, "is blank (name)", !ret);
+
+ /************************************************************/
+ ret = egg_markdown_to_text_line_is_blank ("ccccccccc");
+ egg_test_title_assert (test, "is blank (full)", !ret);
+
+
+ /************************************************************
+ **************** replace **************
+ ************************************************************/
+ text = egg_markdown_replace ("summary -- really -- sure!", " -- ", " – ");
+ egg_test_title (test, "replace (multiple)");
+ if (g_str_equal (text, "summary – really – sure!"))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got %s", text);
+ g_free (text);
+
+ /************************************************************
+ **************** formatter **************
+ ************************************************************/
+ text = egg_markdown_to_text_line_formatter ("**is important** text", "**", "<b>", "</b>");
+ egg_test_title (test, "formatter (left)");
+ if (g_str_equal (text, "<b>is important</b> text"))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got %s", text);
+ g_free (text);
+
+ /************************************************************/
+ text = egg_markdown_to_text_line_formatter ("this is **important**", "**", "<b>", "</b>");
+ egg_test_title (test, "formatter (right)");
+ if (g_str_equal (text, "this is <b>important</b>"))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got %s", text);
+ g_free (text);
+
+ /************************************************************/
+ text = egg_markdown_to_text_line_formatter ("**important**", "**", "<b>", "</b>");
+ egg_test_title (test, "formatter (only)");
+ if (g_str_equal (text, "<b>important</b>"))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got %s", text);
+ g_free (text);
+
+ /************************************************************/
+ text = egg_markdown_to_text_line_formatter ("***important***", "**", "<b>", "</b>");
+ egg_test_title (test, "formatter (only)");
+ if (g_str_equal (text, "<b>*important</b>*"))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got %s", text);
+ g_free (text);
+
+ /************************************************************/
+ text = egg_markdown_to_text_line_formatter ("I guess * this is * not bold", "*", "<i>", "</i>");
+ egg_test_title (test, "formatter (with spaces)");
+ if (g_str_equal (text, "I guess * this is * not bold"))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got %s", text);
+ g_free (text);
+
+ /************************************************************/
+ text = egg_markdown_to_text_line_formatter ("this **is important** text in **several** places", "**", "<b>", "</b>");
+ egg_test_title (test, "formatter (middle, multiple)");
+ if (g_str_equal (text, "this <b>is important</b> text in <b>several</b> places"))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got %s", text);
+ g_free (text);
+
+ /************************************************************/
+ text = egg_markdown_word_auto_format_code ("this is http://www.hughsie.com/with_spaces_in_url inline link");
+ egg_test_title (test, "auto formatter (url)");
+ if (g_str_equal (text, "this is `http://www.hughsie.com/with_spaces_in_url` inline link"))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got %s", text);
+ g_free (text);
+
+ /************************************************************/
+ text = egg_markdown_to_text_line_formatter ("this was \"triffic\" it was", "\"", "“", "”");
+ egg_test_title (test, "formatter (quotes)");
+ if (g_str_equal (text, "this was “triffic” it was"))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got %s", text);
+ g_free (text);
+
+ /************************************************************/
+ text = egg_markdown_to_text_line_formatter ("This isn't a present", "'", "‘", "’");
+ egg_test_title (test, "formatter (one quote)");
+ if (g_str_equal (text, "This isn't a present"))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got %s", text);
+ g_free (text);
+
+ /************************************************************
+ **************** markdown **************
+ ************************************************************/
+ egg_test_title (test, "get EggMarkdown object");
+ self = egg_markdown_new ();
+ egg_test_assert (test, self != NULL);
+
+ /************************************************************/
+ ret = egg_markdown_set_output (self, EGG_MARKDOWN_OUTPUT_PANGO);
+ egg_test_title_assert (test, "set pango output", ret);
+
+ /************************************************************/
+ markdown = "OEMs\n"
+ "====\n"
+ " - Bullett\n";
+ markdown_expected =
+ "<big>OEMs</big>\n"
+ "• Bullett";
+ egg_test_title (test, "markdown (type2 header)");
+ text = egg_markdown_parse (self, markdown);
+ if (g_str_equal (text, markdown_expected))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got '%s', expected '%s'", text, markdown_expected);
+ g_free (text);
+
+
+ /************************************************************/
+ markdown = "this is http://www.hughsie.com/with_spaces_in_url inline link\n";
+ markdown_expected = "this is <tt>http://www.hughsie.com/with_spaces_in_url</tt> inline link";
+ egg_test_title (test, "markdown (autocode)");
+ egg_markdown_set_autocode (self, TRUE);
+ text = egg_markdown_parse (self, markdown);
+ if (g_str_equal (text, markdown_expected))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got '%s', expected '%s'", text, markdown_expected);
+ g_free (text);
+
+ /************************************************************/
+ markdown = "*** This software is currently in alpha state ***\n";
+ markdown_expected = "<b><i> This software is currently in alpha state </b></i>";
+ egg_test_title (test, "markdown some invalid header");
+ text = egg_markdown_parse (self, markdown);
+ if (g_str_equal (text, markdown_expected))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got '%s', expected '%s'", text, markdown_expected);
+ g_free (text);
+
+ /************************************************************/
+ markdown = " - This is a *very*\n"
+ " short paragraph\n"
+ " that is not usual.\n"
+ " - Another";
+ markdown_expected =
+ "• This is a <i>very</i> short paragraph that is not usual.\n"
+ "• Another";
+ egg_test_title (test, "markdown (complex1)");
+ text = egg_markdown_parse (self, markdown);
+ if (g_str_equal (text, markdown_expected))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got '%s', expected '%s'", text, markdown_expected);
+ g_free (text);
+
+ /************************************************************/
+ markdown = "* This is a *very*\n"
+ " short paragraph\n"
+ " that is not usual.\n"
+ "* This is the second\n"
+ " bullett point.\n"
+ "* And the third.\n"
+ " \n"
+ "* * *\n"
+ " \n"
+ "Paragraph one\n"
+ "isn't __very__ long at all.\n"
+ "\n"
+ "Paragraph two\n"
+ "isn't much better.";
+ markdown_expected =
+ "• This is a <i>very</i> short paragraph that is not usual.\n"
+ "• This is the second bullett point.\n"
+ "• And the third.\n"
+ "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯\n"
+ "Paragraph one isn't <b>very</b> long at all.\n"
+ "Paragraph two isn't much better.";
+ egg_test_title (test, "markdown (complex1)");
+ text = egg_markdown_parse (self, markdown);
+ if (g_str_equal (text, markdown_expected))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got '%s', expected '%s'", text, markdown_expected);
+ g_free (text);
+
+ /************************************************************/
+ markdown = "This is a spec file description or\n"
+ "an **update** description in bohdi.\n"
+ "\n"
+ "* * *\n"
+ "# Big title #\n"
+ "\n"
+ "The *following* things 'were' fixed:\n"
+ "- Fix `dave`\n"
+ "* Fubar update because of \"security\"\n";
+ markdown_expected =
+ "This is a spec file description or an <b>update</b> description in bohdi.\n"
+ "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯\n"
+ "<big>Big title</big>\n"
+ "The <i>following</i> things 'were' fixed:\n"
+ "• Fix <tt>dave</tt>\n"
+ "• Fubar update because of \"security\"";
+ egg_test_title (test, "markdown (complex2)");
+ text = egg_markdown_parse (self, markdown);
+ if (g_str_equal (text, markdown_expected))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got '%s', expected '%s'", text, markdown_expected);
+ g_free (text);
+
+ /************************************************************/
+ markdown = "* list seporated with spaces -\n"
+ " first item\n"
+ "\n"
+ "* second item\n"
+ "\n"
+ "* third item\n";
+ markdown_expected =
+ "• list seporated with spaces - first item\n"
+ "• second item\n"
+ "• third item";
+ egg_test_title (test, "markdown (list with spaces)");
+ text = egg_markdown_parse (self, markdown);
+ if (g_str_equal (text, markdown_expected))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got '%s', expected '%s'", text, markdown_expected);
+ g_free (text);
+
+ /************************************************************/
+ ret = egg_markdown_set_max_lines (self, 1);
+ egg_test_title_assert (test, "set max_lines", ret);
+
+ /************************************************************/
+ markdown = "* list seporated with spaces -\n"
+ " first item\n"
+ "* second item\n";
+ markdown_expected =
+ "• list seporated with spaces - first item";
+ egg_test_title (test, "markdown (one line limit)");
+ text = egg_markdown_parse (self, markdown);
+ if (g_str_equal (text, markdown_expected))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got '%s', expected '%s'", text, markdown_expected);
+ g_free (text);
+
+ /************************************************************/
+ ret = egg_markdown_set_max_lines (self, 1);
+ egg_test_title_assert (test, "set max_lines", ret);
+
+ /************************************************************/
+ markdown = "* list & spaces";
+ markdown_expected =
+ "• list & spaces";
+ egg_test_title (test, "markdown (escaping)");
+ text = egg_markdown_parse (self, markdown);
+ if (g_str_equal (text, markdown_expected))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got '%s', expected '%s'", text, markdown_expected);
+ g_free (text);
+
+ /************************************************************/
+ egg_test_title (test, "markdown (free text)");
+ text = egg_markdown_parse (self, "This isn't a present");
+ if (g_str_equal (text, "This isn't a present"))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got '%s'", text);
+ g_free (text);
+
+ /************************************************************/
+ egg_test_title (test, "markdown (autotext underscore)");
+ text = egg_markdown_parse (self, "This isn't CONFIG_UEVENT_HELPER_PATH present");
+ if (g_str_equal (text, "This isn't <tt>CONFIG_UEVENT_HELPER_PATH</tt> present"))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got '%s'", text);
+ g_free (text);
+
+ /************************************************************/
+ markdown = "*Thu Mar 12 12:00:00 2009* Dan Walsh <dwalsh@redhat.com> - 2.0.79-1\n"
+ "- Update to upstream \n"
+ " * Netlink socket handoff patch from Adam Jackson.\n"
+ " * AVC caching of compute_create results by Eric Paris.\n"
+ "\n"
+ "*Tue Mar 10 12:00:00 2009* Dan Walsh <dwalsh@redhat.com> - 2.0.78-5\n"
+ "- Add patch from ajax to accellerate X SELinux \n"
+ "- Update eparis patch\n";
+ markdown_expected =
+ "<i>Thu Mar 12 12:00:00 2009</i> Dan Walsh <tt>&lt;dwalsh@redhat.com&gt;</tt> - 2.0.79-1\n"
+ "• Update to upstream\n"
+ "• Netlink socket handoff patch from Adam Jackson.\n"
+ "• AVC caching of compute_create results by Eric Paris.\n"
+ "<i>Tue Mar 10 12:00:00 2009</i> Dan Walsh <tt>&lt;dwalsh@redhat.com&gt;</tt> - 2.0.78-5\n"
+ "• Add patch from ajax to accellerate X SELinux\n"
+ "• Update eparis patch";
+ egg_test_title (test, "markdown (end of bullett)");
+ egg_markdown_set_escape (self, TRUE);
+ ret = egg_markdown_set_max_lines (self, 1024);
+ text = egg_markdown_parse (self, markdown);
+ if (g_str_equal (text, markdown_expected))
+ egg_test_success (test, NULL);
+ else
+ egg_test_failed (test, "failed, got '%s', expected '%s'", text, markdown_expected);
+ g_free (text);
+
+ g_object_unref (self);
+
+ egg_test_end (test);
+}
+#endif
+
diff --git a/help-viewer/help-viewer.c b/help-viewer/help-viewer.c
new file mode 100644
index 00000000..ace6ef37
--- /dev/null
+++ b/help-viewer/help-viewer.c
@@ -0,0 +1,514 @@
+/*
+ * HelpViewer - Simple Help file browser
+ * Copyright (C) 2009 Leandro A. F. Pereira <leandro@hardinfo.org>
+ *
+ * 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, version 2.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#define _GNU_SOURCE /* for strcasestr() */
+#include <string.h>
+#include <stdlib.h>
+#include <gtk/gtk.h>
+
+#include "config.h"
+#include "shell.h"
+#include "markdown-text-view.h"
+#include "help-viewer.h"
+#include "hardinfo.h"
+
+static void do_search(HelpViewer *hv, gchar *text);
+
+static void forward_clicked(GtkWidget *widget, gpointer data)
+{
+ HelpViewer *hv = (HelpViewer *)data;
+ GSList *temp;
+
+ /* puts the current file on the back stack */
+ hv->back_stack = g_slist_prepend(hv->back_stack, g_strdup(hv->current_file));
+
+ /* enables the back button */
+ gtk_widget_set_sensitive(hv->btn_back, TRUE);
+
+ /* loads the new current file */
+ if (g_str_has_prefix(hv->forward_stack->data, "search://")) {
+ do_search(hv, hv->forward_stack->data + sizeof("search://") - 1);
+ } else {
+ markdown_textview_load_file(MARKDOWN_TEXTVIEW(hv->text_view), hv->forward_stack->data);
+ }
+
+ /* pops the stack */
+ temp = hv->forward_stack->next;
+ g_free(hv->forward_stack->data);
+ g_slist_free1(hv->forward_stack);
+ hv->forward_stack = temp;
+
+ /* if there aren't items on forward stack anymore, disables the button */
+ if (!hv->forward_stack) {
+ gtk_widget_set_sensitive(hv->btn_forward, FALSE);
+ }
+}
+
+static void back_clicked(GtkWidget *widget, gpointer data)
+{
+ HelpViewer *hv = (HelpViewer *)data;
+ GSList *temp;
+
+ /* puts the current file on the forward stack */
+ hv->forward_stack = g_slist_prepend(hv->forward_stack, g_strdup(hv->current_file));
+
+ /* enables the forward button */
+ gtk_widget_set_sensitive(hv->btn_forward, TRUE);
+
+ /* loads the new current file */
+ if (g_str_has_prefix(hv->back_stack->data, "search://")) {
+ do_search(hv, hv->back_stack->data + sizeof("search://") - 1);
+ } else {
+ markdown_textview_load_file(MARKDOWN_TEXTVIEW(hv->text_view), hv->back_stack->data);
+ }
+
+ /* pops the stack */
+ temp = hv->back_stack->next;
+ g_free(hv->back_stack->data);
+ g_slist_free1(hv->back_stack);
+ hv->back_stack = temp;
+
+ /* if there aren't items on back stack anymore, disables the button */
+ if (!hv->back_stack) {
+ gtk_widget_set_sensitive(hv->btn_back, FALSE);
+ }
+}
+
+static void link_clicked(MarkdownTextView *text_view, gchar *link, gpointer data)
+{
+ HelpViewer *hv = (HelpViewer *)data;
+
+ if (g_str_has_prefix(link, "http://")) {
+ open_url(link);
+ } else {
+ /* adds the current file to the back stack (before loading the new file */
+ hv->back_stack = g_slist_prepend(hv->back_stack, g_strdup(hv->current_file));
+ gtk_widget_set_sensitive(hv->btn_back, TRUE);
+
+ gtk_statusbar_pop(GTK_STATUSBAR(hv->status_bar), 1);
+ markdown_textview_load_file(text_view, link);
+ }
+}
+
+static void file_load_complete(MarkdownTextView *text_view, gchar *file, gpointer data)
+{
+ HelpViewer *hv = (HelpViewer *)data;
+
+ /* sets the currently-loaded file */
+ g_free(hv->current_file);
+ hv->current_file = g_strdup(file);
+
+ gtk_statusbar_push(GTK_STATUSBAR(hv->status_bar), 1, "Done.");
+}
+
+static void hovering_over_link(MarkdownTextView *text_view, gchar *link, gpointer data)
+{
+ HelpViewer *hv = (HelpViewer *)data;
+ gchar *temp;
+
+ temp = g_strconcat("Link to ", link, NULL);
+
+ gtk_statusbar_push(GTK_STATUSBAR(hv->status_bar), 1, temp);
+
+ g_free(temp);
+}
+
+static void hovering_over_text(MarkdownTextView *text_view, gpointer data)
+{
+ HelpViewer *hv = (HelpViewer *)data;
+
+ gtk_statusbar_pop(GTK_STATUSBAR(hv->status_bar), 1);
+}
+
+static void do_search(HelpViewer *hv, gchar *text)
+{
+ GString *markdown;
+ GDir *dir;
+ gchar **terms;
+ gint no_results = 0;
+ int term;
+
+ /*
+ * FIXME: This search is currently pretty slow; think on a better way to do search.
+ * Ideas:
+ * - Build a index the first time the help file is opened
+ * - Search only titles and subtitles
+ */
+
+ terms = g_strsplit(text, " ", 0);
+ markdown = g_string_new("# Search Results\n");
+ g_string_append_printf(markdown, "Search terms: *%s*\n****\n", text);
+
+ gtk_widget_set_sensitive(hv->window, FALSE);
+
+ if ((dir = g_dir_open(hv->help_directory, 0, NULL))) {
+ const gchar *name;
+
+ while ((name = g_dir_read_name(dir))) {
+#if GTK_CHECK_VERSION(2,16,0)
+ gtk_entry_progress_pulse(GTK_ENTRY(hv->text_search));
+#endif /* GTK_CHECK_VERSION(2,16,0) */
+
+ if (g_str_has_suffix(name, ".hlp")) {
+ FILE *file;
+ gchar *path;
+ gchar buffer[256];
+
+ path = g_build_filename(hv->help_directory, name, NULL);
+ if ((file = fopen(path, "rb"))) {
+ gboolean found = FALSE;
+ gchar *title = NULL;
+
+ while (!found && fgets(buffer, sizeof buffer, file)) {
+ if (!title && (g_str_has_prefix(buffer, "# ") || g_str_has_prefix(buffer, " # "))) {
+ title = g_strstrip(strchr(buffer, '#') + 1);
+ title = g_strdup(title);
+ }
+
+ for (term = 0; !found && terms[term]; term++) {
+#ifdef strcasestr
+ found = strcasestr(buffer, terms[term]) != NULL;
+#else
+ gchar *upper1, *upper2;
+
+ upper1 = g_utf8_strup(buffer, -1);
+ upper2 = g_utf8_strup(terms[term], -1);
+
+ found = strstr(upper1, upper2) != NULL;
+
+ g_free(upper1);
+ g_free(upper2);
+#endif
+ }
+ }
+
+ if (found) {
+ no_results++;
+
+ if (title) {
+ g_string_append_printf(markdown,
+ "* [%s %s]\n", name, title);
+ } else {
+ g_string_append_printf(markdown,
+ "* [%s %s]\n", name, name);
+ }
+ }
+
+ g_free(title);
+ fclose(file);
+ }
+
+ g_free(path);
+ }
+ }
+
+ g_dir_close(dir);
+ }
+
+
+ if (no_results == 0) {
+ g_string_append_printf(markdown,
+ "Search returned no results.");
+ } else {
+ g_string_append_printf(markdown,
+ "****\n%d results found.", no_results);
+ }
+
+ /* shows the results inside the textview */
+ markdown_textview_set_text(MARKDOWN_TEXTVIEW(hv->text_view), markdown->str);
+
+ g_free(hv->current_file);
+ hv->current_file = g_strdup_printf("search://%s", text);
+
+#if GTK_CHECK_VERSION(2,16,0)
+ gtk_entry_set_progress_fraction(GTK_ENTRY(hv->text_search), 0.0f);
+#endif /* GTK_CHECK_VERSION(2,16,0) */
+ gtk_widget_set_sensitive(hv->window, TRUE);
+
+ g_string_free(markdown, TRUE);
+ g_strfreev(terms);
+}
+
+static void activate(GtkEntry *entry, gpointer data)
+{
+ HelpViewer *hv = (HelpViewer *)data;
+
+ /* adds the current file to the back stack (before loading the new file */
+ hv->back_stack = g_slist_prepend(hv->back_stack, g_strdup(hv->current_file));
+ gtk_widget_set_sensitive(hv->btn_back, TRUE);
+
+ do_search((HelpViewer *)data, (gchar *)gtk_entry_get_text(entry));
+}
+
+#if GTK_CHECK_VERSION(2,16,0)
+static void icon_press(GtkEntry *entry, gint position,
+ GdkEventButton *event, gpointer data)
+{
+ if (position == GTK_ENTRY_ICON_SECONDARY)
+ activate(entry, data);
+}
+#endif /* GTK_CHECK_VERSION(2,16,0) */
+
+static void home_clicked(GtkWidget *button, gpointer data)
+{
+ HelpViewer *hv = (HelpViewer *)data;
+
+ help_viewer_open_page(hv, "index.hlp");
+}
+
+void help_viewer_open_page(HelpViewer *hv, const gchar *page)
+{
+ gchar *temp;
+
+ temp = g_strdup(hv->current_file);
+
+ if (!markdown_textview_load_file(MARKDOWN_TEXTVIEW(hv->text_view), page)) {
+ GtkWidget *dialog;
+ Shell *shell;
+
+ shell = shell_get_main_shell();
+ dialog = gtk_message_dialog_new(GTK_WINDOW(shell->window),
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_CLOSE,
+ "Cannot open help file (%s).",
+ page);
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+
+ g_free(temp);
+ } else {
+ /* adds the current file to the back stack (before loading the new file */
+ hv->back_stack = g_slist_prepend(hv->back_stack, temp);
+ gtk_widget_set_sensitive(hv->btn_back, TRUE);
+ gtk_window_present(GTK_WINDOW(hv->window));
+ }
+}
+
+void help_viewer_destroy(HelpViewer *hv)
+{
+ Shell *shell;
+ GSList *item;
+
+ for (item = hv->back_stack; item; item = item->next) {
+ g_free(item->data);
+ }
+
+ for (item = hv->forward_stack; item; item = item->next) {
+ g_free(item->data);
+ }
+
+ g_slist_free(hv->back_stack);
+ g_slist_free(hv->forward_stack);
+
+ g_free(hv->current_file);
+ g_free(hv->help_directory);
+
+ shell = shell_get_main_shell();
+ shell->help_viewer = NULL;
+}
+
+static gboolean destroy_me(GtkWidget *widget, gpointer data)
+{
+ HelpViewer *hv = (HelpViewer *)data;
+
+ help_viewer_destroy(hv);
+
+ return FALSE;
+}
+
+HelpViewer *
+help_viewer_new (const gchar *help_dir, const gchar *help_file)
+{
+ Shell *shell;
+ HelpViewer *hv;
+ GtkWidget *help_viewer;
+ GtkWidget *vbox;
+ GtkWidget *hbox;
+ GtkWidget *toolbar1;
+ GtkIconSize tmp_toolbar_icon_size;
+ GtkWidget *btn_back;
+ GtkWidget *btn_forward;
+ GtkWidget *separatortoolitem1;
+ GtkWidget *toolbar2;
+ GtkWidget *toolitem3;
+#if !GTK_CHECK_VERSION(2,16,0)
+ GtkWidget *label1;
+#endif /* GTK_CHECK_VERSION(2,16,0) */
+ GtkWidget *toolitem4;
+ GtkWidget *txt_search;
+ GtkWidget *scrolledhelp_viewer;
+ GtkWidget *markdown_textview;
+ GtkWidget *status_bar;
+ GtkWidget *btn_home;
+ GdkPixbuf *icon;
+
+ shell = shell_get_main_shell();
+
+ help_viewer = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ gtk_widget_set_size_request(help_viewer, 300, 200);
+ gtk_window_set_default_size(GTK_WINDOW(help_viewer), 640, 480);
+ gtk_window_set_title(GTK_WINDOW(help_viewer), "Help Viewer");
+ gtk_window_set_transient_for(GTK_WINDOW(help_viewer), GTK_WINDOW(shell->window));
+
+ icon = gtk_widget_render_icon(help_viewer, GTK_STOCK_HELP,
+ GTK_ICON_SIZE_DIALOG,
+ NULL);
+ gtk_window_set_icon(GTK_WINDOW(help_viewer), icon);
+ g_object_unref(icon);
+
+ vbox = gtk_vbox_new (FALSE, 0);
+ gtk_widget_show (vbox);
+ gtk_container_add (GTK_CONTAINER (help_viewer), vbox);
+
+ hbox = gtk_hbox_new (FALSE, 0);
+ gtk_widget_show (hbox);
+ gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0);
+
+ toolbar1 = gtk_toolbar_new ();
+ gtk_widget_show (toolbar1);
+ gtk_box_pack_start (GTK_BOX (hbox), toolbar1, TRUE, TRUE, 0);
+ gtk_toolbar_set_style (GTK_TOOLBAR (toolbar1), GTK_TOOLBAR_BOTH_HORIZ);
+ tmp_toolbar_icon_size = gtk_toolbar_get_icon_size (GTK_TOOLBAR (toolbar1));
+
+ btn_back = (GtkWidget*) gtk_tool_button_new_from_stock ("gtk-go-back");
+ gtk_widget_show (btn_back);
+ gtk_container_add (GTK_CONTAINER (toolbar1), btn_back);
+ gtk_tool_item_set_is_important (GTK_TOOL_ITEM (btn_back), TRUE);
+ gtk_widget_set_sensitive(btn_back, FALSE);
+
+ btn_forward = (GtkWidget*) gtk_tool_button_new_from_stock ("gtk-go-forward");
+ gtk_widget_show (btn_forward);
+ gtk_container_add (GTK_CONTAINER (toolbar1), btn_forward);
+ gtk_widget_set_sensitive(btn_forward, FALSE);
+
+ separatortoolitem1 = (GtkWidget*) gtk_separator_tool_item_new ();
+ gtk_widget_show (separatortoolitem1);
+ gtk_container_add (GTK_CONTAINER (toolbar1), separatortoolitem1);
+
+ btn_home = (GtkWidget*) gtk_tool_button_new_from_stock ("gtk-home");
+ gtk_widget_show (btn_home);
+ gtk_container_add (GTK_CONTAINER (toolbar1), btn_home);
+
+ toolbar2 = gtk_toolbar_new ();
+ gtk_widget_show (toolbar2);
+ gtk_box_pack_end (GTK_BOX (hbox), toolbar2, FALSE, TRUE, 0);
+ gtk_toolbar_set_style (GTK_TOOLBAR (toolbar2), GTK_TOOLBAR_BOTH_HORIZ);
+ gtk_toolbar_set_show_arrow (GTK_TOOLBAR (toolbar2), FALSE);
+ tmp_toolbar_icon_size = gtk_toolbar_get_icon_size (GTK_TOOLBAR (toolbar2));
+
+ toolitem3 = (GtkWidget*) gtk_tool_item_new ();
+ gtk_widget_show (toolitem3);
+ gtk_container_add (GTK_CONTAINER (toolbar2), toolitem3);
+
+#if !GTK_CHECK_VERSION(2,16,0)
+ label1 = gtk_label_new_with_mnemonic ("_Search:");
+ gtk_widget_show (label1);
+ gtk_container_add (GTK_CONTAINER (toolitem3), label1);
+#endif /* GTK_CHECK_VERSION(2,16,0) */
+
+ toolitem4 = (GtkWidget*) gtk_tool_item_new ();
+ gtk_widget_show (toolitem4);
+ gtk_container_add (GTK_CONTAINER (toolbar2), toolitem4);
+
+ txt_search = gtk_entry_new ();
+ gtk_widget_show (txt_search);
+ gtk_container_add (GTK_CONTAINER (toolitem4), txt_search);
+ gtk_entry_set_invisible_char (GTK_ENTRY (txt_search), 9679);
+#if GTK_CHECK_VERSION(2,16,0)
+ gtk_entry_set_icon_from_stock(GTK_ENTRY(txt_search),
+ GTK_ENTRY_ICON_SECONDARY,
+ GTK_STOCK_FIND);
+#endif /* GTK_CHECK_VERSION(2,16,0) */
+
+ scrolledhelp_viewer = gtk_scrolled_window_new (NULL, NULL);
+ gtk_widget_show (scrolledhelp_viewer);
+ gtk_box_pack_start (GTK_BOX (vbox), scrolledhelp_viewer, TRUE, TRUE, 0);
+ gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolledhelp_viewer), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
+
+ markdown_textview = markdown_textview_new();
+ markdown_textview_set_image_directory(MARKDOWN_TEXTVIEW(markdown_textview), help_dir);
+ gtk_widget_show (markdown_textview);
+ gtk_container_add (GTK_CONTAINER (scrolledhelp_viewer), markdown_textview);
+
+ status_bar = gtk_statusbar_new ();
+ gtk_widget_show (status_bar);
+ gtk_box_pack_start (GTK_BOX (vbox), status_bar, FALSE, FALSE, 0);
+
+ hv = g_new0(HelpViewer, 1);
+ hv->window = help_viewer;
+ hv->status_bar = status_bar;
+ hv->btn_back = btn_back;
+ hv->btn_forward = btn_forward;
+ hv->text_view = markdown_textview;
+ hv->text_search = txt_search;
+ hv->help_directory = g_strdup(help_dir);
+ hv->back_stack = NULL;
+ hv->forward_stack = NULL;
+
+ g_signal_connect(markdown_textview, "link-clicked", G_CALLBACK(link_clicked), hv);
+ g_signal_connect(markdown_textview, "hovering-over-link", G_CALLBACK(hovering_over_link), hv);
+ g_signal_connect(markdown_textview, "hovering-over-text", G_CALLBACK(hovering_over_text), hv);
+ g_signal_connect(markdown_textview, "file-load-complete", G_CALLBACK(file_load_complete), hv);
+
+ g_signal_connect(btn_back, "clicked", G_CALLBACK(back_clicked), hv);
+ g_signal_connect(btn_forward, "clicked", G_CALLBACK(forward_clicked), hv);
+ g_signal_connect(btn_home, "clicked", G_CALLBACK(home_clicked), hv);
+
+ g_signal_connect(help_viewer, "delete-event", G_CALLBACK(destroy_me), hv);
+ g_signal_connect(txt_search, "activate", G_CALLBACK(activate), hv);
+
+#if GTK_CHECK_VERSION(2,16,0)
+ g_signal_connect(txt_search, "icon-press", G_CALLBACK(icon_press), hv);
+#endif /* GTK_CHECK_VERSION(2,16,0) */
+
+ if (!markdown_textview_load_file(MARKDOWN_TEXTVIEW(markdown_textview), help_file ? help_file : "index.hlp")) {
+ GtkWidget *dialog;
+
+ dialog = gtk_message_dialog_new(GTK_WINDOW(shell->window),
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_CLOSE,
+ "Cannot open help file (%s).",
+ help_file ? help_file : "index.hlp");
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+
+ gtk_widget_destroy(hv->window);
+ g_free(hv);
+
+ return NULL;
+ }
+
+ gtk_widget_show_all(hv->window);
+
+ return hv;
+}
+
+#ifdef HELPVIEWER_TEST
+int main(int argc, char **argv)
+{
+ HelpViewer *hv;
+
+ gtk_init(&argc, &argv);
+
+ hv = help_viewer_new("documentation", NULL);
+
+ gtk_main();
+}
+#endif /* HELPVIEWER_TEST */
diff --git a/help-viewer/markdown-text-view.c b/help-viewer/markdown-text-view.c
new file mode 100644
index 00000000..6bfcc131
--- /dev/null
+++ b/help-viewer/markdown-text-view.c
@@ -0,0 +1,637 @@
+/*
+ * Markdown Text View
+ * GtkTextView subclass that supports Markdown syntax
+ *
+ * Copyright (C) 2009 Leandro Pereira <leandro@hardinfo.org>
+ * Portions Copyright (C) 2007-2008 Richard Hughes <richard@hughsie.com>
+ * Portions Copyright (C) GTK+ Team (based on hypertext textview demo)
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+#include <stdlib.h>
+#include <gdk/gdkkeysyms.h>
+
+#include "markdown-text-view.h"
+#include "config.h"
+
+static GdkCursor *hand_cursor = NULL;
+
+G_DEFINE_TYPE(MarkdownTextView, markdown_textview, GTK_TYPE_TEXT_VIEW);
+
+enum {
+ LINK_CLICKED,
+ HOVERING_OVER_LINK,
+ HOVERING_OVER_TEXT,
+ FILE_LOAD_COMPLETE,
+ LAST_SIGNAL
+};
+
+static guint markdown_textview_signals[LAST_SIGNAL] = { 0 };
+
+GtkWidget *markdown_textview_new()
+{
+ return g_object_new(TYPE_MARKDOWN_TEXTVIEW, NULL);
+}
+
+static void markdown_textview_class_init(MarkdownTextViewClass * klass)
+{
+ GObjectClass *object_class;
+
+ if (!hand_cursor) {
+ hand_cursor = gdk_cursor_new(GDK_HAND2);
+ }
+
+ object_class = G_OBJECT_CLASS(klass);
+
+ markdown_textview_signals[LINK_CLICKED] = g_signal_new(
+ "link-clicked",
+ G_OBJECT_CLASS_TYPE(object_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(MarkdownTextViewClass, link_clicked),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__STRING,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_STRING);
+
+ markdown_textview_signals[HOVERING_OVER_LINK] = g_signal_new(
+ "hovering-over-link",
+ G_OBJECT_CLASS_TYPE(object_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(MarkdownTextViewClass, hovering_over_link),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__STRING,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_STRING);
+
+ markdown_textview_signals[HOVERING_OVER_TEXT] = g_signal_new(
+ "hovering-over-text",
+ G_OBJECT_CLASS_TYPE(object_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(MarkdownTextViewClass, hovering_over_text),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE,
+ 0);
+
+ markdown_textview_signals[FILE_LOAD_COMPLETE] = g_signal_new(
+ "file-load-complete",
+ G_OBJECT_CLASS_TYPE(object_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(MarkdownTextViewClass, file_load_complete),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__STRING,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_STRING);
+}
+
+static void
+gtk_text_buffer_insert_markup(GtkTextBuffer * buffer, GtkTextIter * iter,
+ const gchar * markup)
+{
+ PangoAttrIterator *paiter;
+ PangoAttrList *attrlist;
+ GtkTextMark *mark;
+ GError *error = NULL;
+ gchar *text;
+
+ g_return_if_fail(GTK_IS_TEXT_BUFFER(buffer));
+ g_return_if_fail(markup != NULL);
+
+ if (*markup == '\000')
+ return;
+
+ /* invalid */
+ if (!pango_parse_markup(markup, -1, 0, &attrlist, &text, NULL, &error)) {
+ g_warning("Invalid markup string: %s", error->message);
+ g_error_free(error);
+ return;
+ }
+
+ /* trivial, no markup */
+ if (attrlist == NULL) {
+ gtk_text_buffer_insert(buffer, iter, text, -1);
+ g_free(text);
+ return;
+ }
+
+ /* create mark with right gravity */
+ mark = gtk_text_buffer_create_mark(buffer, NULL, iter, FALSE);
+ paiter = pango_attr_list_get_iterator(attrlist);
+
+ do {
+ PangoAttribute *attr;
+ GtkTextTag *tag;
+ GtkTextTag *tag_para;
+ gint start, end;
+
+ pango_attr_iterator_range(paiter, &start, &end);
+
+ if (end == G_MAXINT) /* last chunk */
+ end = start - 1; /* resulting in -1 to be passed to _insert */
+
+ tag = gtk_text_tag_new(NULL);
+
+ if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_LANGUAGE)))
+ g_object_set(tag, "language",
+ pango_language_to_string(((PangoAttrLanguage *)
+ attr)->value), NULL);
+
+ if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_FAMILY)))
+ g_object_set(tag, "family", ((PangoAttrString *) attr)->value,
+ NULL);
+
+ if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_STYLE)))
+ g_object_set(tag, "style", ((PangoAttrInt *) attr)->value,
+ NULL);
+
+ if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_WEIGHT)))
+ g_object_set(tag, "weight", ((PangoAttrInt *) attr)->value,
+ NULL);
+
+ if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_VARIANT)))
+ g_object_set(tag, "variant", ((PangoAttrInt *) attr)->value,
+ NULL);
+
+ if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_STRETCH)))
+ g_object_set(tag, "stretch", ((PangoAttrInt *) attr)->value,
+ NULL);
+
+ if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_SIZE)))
+ g_object_set(tag, "size", ((PangoAttrInt *) attr)->value,
+ NULL);
+
+ if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_FONT_DESC)))
+ g_object_set(tag, "font-desc",
+ ((PangoAttrFontDesc *) attr)->desc, NULL);
+
+ if ((attr =
+ pango_attr_iterator_get(paiter, PANGO_ATTR_FOREGROUND))) {
+ GdkColor col = { 0,
+ ((PangoAttrColor *) attr)->color.red,
+ ((PangoAttrColor *) attr)->color.green,
+ ((PangoAttrColor *) attr)->color.blue
+ };
+
+ g_object_set(tag, "foreground-gdk", &col, NULL);
+ }
+
+ if ((attr =
+ pango_attr_iterator_get(paiter, PANGO_ATTR_BACKGROUND))) {
+ GdkColor col = { 0,
+ ((PangoAttrColor *) attr)->color.red,
+ ((PangoAttrColor *) attr)->color.green,
+ ((PangoAttrColor *) attr)->color.blue
+ };
+
+ g_object_set(tag, "background-gdk", &col, NULL);
+ }
+
+ if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_UNDERLINE)))
+ g_object_set(tag, "underline", ((PangoAttrInt *) attr)->value,
+ NULL);
+
+ if ((attr =
+ pango_attr_iterator_get(paiter, PANGO_ATTR_STRIKETHROUGH)))
+ g_object_set(tag, "strikethrough",
+ (gboolean) (((PangoAttrInt *) attr)->value != 0),
+ NULL);
+
+ if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_RISE)))
+ g_object_set(tag, "rise", ((PangoAttrInt *) attr)->value,
+ NULL);
+
+ if ((attr = pango_attr_iterator_get(paiter, PANGO_ATTR_SCALE)))
+ g_object_set(tag, "scale", ((PangoAttrFloat *) attr)->value,
+ NULL);
+
+ gtk_text_tag_table_add(gtk_text_buffer_get_tag_table(buffer), tag);
+
+ tag_para =
+ gtk_text_tag_table_lookup(gtk_text_buffer_get_tag_table
+ (buffer), "para");
+ gtk_text_buffer_insert_with_tags(buffer, iter, text + start,
+ end - start, tag, tag_para, NULL);
+
+ /* mark had right gravity, so it should be
+ * at the end of the inserted text now */
+ gtk_text_buffer_get_iter_at_mark(buffer, iter, mark);
+ } while (pango_attr_iterator_next(paiter));
+
+ gtk_text_buffer_delete_mark(buffer, mark);
+ pango_attr_iterator_destroy(paiter);
+ pango_attr_list_unref(attrlist);
+ g_free(text);
+}
+
+
+static void
+set_cursor_if_appropriate(MarkdownTextView * self, gint x, gint y)
+{
+ GSList *tags = NULL, *tagp = NULL;
+ GtkTextIter iter;
+ gboolean hovering = FALSE;
+ gchar *link_uri = NULL;
+
+ gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(self), &iter, x,
+ y);
+
+ tags = gtk_text_iter_get_tags(&iter);
+ for (tagp = tags; tagp != NULL; tagp = tagp->next) {
+ GtkTextTag *tag = tagp->data;
+ gint is_underline = 0;
+ gchar *lang = NULL;
+
+ g_object_get(G_OBJECT(tag),
+ "underline", &is_underline,
+ "language", &lang,
+ NULL);
+
+ if (is_underline == 1 && lang) {
+ link_uri = egg_markdown_get_link_uri(self->markdown, atoi(lang));
+ g_free(lang);
+ hovering = TRUE;
+ break;
+ }
+
+ g_free(lang);
+ }
+
+ if (hovering != self->hovering_over_link) {
+ self->hovering_over_link = hovering;
+
+ if (self->hovering_over_link) {
+ g_signal_emit(self, markdown_textview_signals[HOVERING_OVER_LINK],
+ 0, link_uri);
+ gdk_window_set_cursor(gtk_text_view_get_window
+ (GTK_TEXT_VIEW(self),
+ GTK_TEXT_WINDOW_TEXT), hand_cursor);
+ } else {
+ g_signal_emit(self, markdown_textview_signals[HOVERING_OVER_TEXT], 0);
+ gdk_window_set_cursor(gtk_text_view_get_window
+ (GTK_TEXT_VIEW(self),
+ GTK_TEXT_WINDOW_TEXT), NULL);
+ }
+ }
+
+ if (link_uri)
+ g_free(link_uri);
+
+ if (tags)
+ g_slist_free(tags);
+}
+
+/* Update the cursor image if the pointer moved.
+ */
+static gboolean
+motion_notify_event(GtkWidget * self, GdkEventMotion * event)
+{
+ gint x, y;
+
+ gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(self),
+ GTK_TEXT_WINDOW_WIDGET,
+ event->x, event->y, &x, &y);
+
+ set_cursor_if_appropriate(MARKDOWN_TEXTVIEW(self), x, y);
+
+ gdk_window_get_pointer(self->window, NULL, NULL, NULL);
+ return FALSE;
+}
+
+/* Also update the cursor image if the window becomes visible
+ * (e.g. when a window covering it got iconified).
+ */
+static gboolean
+visibility_notify_event(GtkWidget * self, GdkEventVisibility * event)
+{
+ gint wx, wy, bx, by;
+
+ gdk_window_get_pointer(self->window, &wx, &wy, NULL);
+
+ gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(self),
+ GTK_TEXT_WINDOW_WIDGET,
+ wx, wy, &bx, &by);
+
+ set_cursor_if_appropriate(MARKDOWN_TEXTVIEW(self), bx, by);
+
+ return FALSE;
+}
+
+static void
+follow_if_link(GtkWidget * widget, GtkTextIter * iter)
+{
+ GSList *tags = NULL, *tagp = NULL;
+ MarkdownTextView *self = MARKDOWN_TEXTVIEW(widget);
+
+ tags = gtk_text_iter_get_tags(iter);
+ for (tagp = tags; tagp != NULL; tagp = tagp->next) {
+ GtkTextTag *tag = tagp->data;
+ gint is_underline = 0;
+ gchar *lang = NULL;
+
+ g_object_get(G_OBJECT(tag),
+ "underline", &is_underline,
+ "language", &lang,
+ NULL);
+
+ if (is_underline == 1 && lang) {
+ gchar *link = egg_markdown_get_link_uri(self->markdown, atoi(lang));
+ if (link) {
+ g_signal_emit(self, markdown_textview_signals[LINK_CLICKED],
+ 0, link);
+ g_free(link);
+ }
+ g_free(lang);
+ break;
+ }
+
+ g_free(lang);
+ }
+
+ if (tags)
+ g_slist_free(tags);
+}
+
+static gboolean
+key_press_event(GtkWidget * self,
+ GdkEventKey * event)
+{
+ GtkTextIter iter;
+ GtkTextBuffer *buffer;
+
+ switch (event->keyval) {
+ case GDK_Return:
+ case GDK_KP_Enter:
+ buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(self));
+ gtk_text_buffer_get_iter_at_mark(buffer, &iter,
+ gtk_text_buffer_get_insert
+ (buffer));
+ follow_if_link(self, &iter);
+ break;
+
+ default:
+ break;
+ }
+
+ return FALSE;
+}
+
+static gboolean
+event_after(GtkWidget * self, GdkEvent * ev)
+{
+ GtkTextIter start, end, iter;
+ GtkTextBuffer *buffer;
+ GdkEventButton *event;
+ gint x, y;
+
+ if (ev->type != GDK_BUTTON_RELEASE)
+ return FALSE;
+
+ event = (GdkEventButton *) ev;
+
+ if (event->button != 1)
+ return FALSE;
+
+ buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(self));
+
+ /* we shouldn't follow a link if the user has selected something */
+ gtk_text_buffer_get_selection_bounds(buffer, &start, &end);
+ if (gtk_text_iter_get_offset(&start) != gtk_text_iter_get_offset(&end))
+ return FALSE;
+
+ gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(self),
+ GTK_TEXT_WINDOW_WIDGET,
+ event->x, event->y, &x, &y);
+
+ gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(self), &iter, x,
+ y);
+
+ follow_if_link(self, &iter);
+
+ return FALSE;
+}
+
+void
+markdown_textview_clear(MarkdownTextView * self)
+{
+ GtkTextBuffer *text_buffer;
+
+ g_return_if_fail(IS_MARKDOWN_TEXTVIEW(self));
+
+ egg_markdown_clear(self->markdown);
+
+ text_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(self));
+ gtk_text_buffer_set_text(text_buffer, "\n", 1);
+}
+
+static void
+load_images(MarkdownTextView * self)
+{
+ GtkTextBuffer *buffer;
+ GtkTextIter iter;
+ GSList *tags, *tagp;
+ gchar *image_path;
+
+ buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(self));
+ gtk_text_buffer_get_start_iter(buffer, &iter);
+
+ do {
+ tags = gtk_text_iter_get_tags(&iter);
+ for (tagp = tags; tagp != NULL; tagp = tagp->next) {
+ GtkTextTag *tag = tagp->data;
+ gint is_underline = 0;
+ gchar *lang = NULL;
+
+ g_object_get(G_OBJECT(tag),
+ "underline", &is_underline,
+ "language", &lang,
+ NULL);
+
+ if (is_underline == 2 && lang) {
+ GdkPixbuf *pixbuf;
+ gchar *path;
+
+ image_path = egg_markdown_get_link_uri(self->markdown, atoi(lang));
+ path = g_build_filename(self->image_directory, image_path, NULL);
+ pixbuf = gdk_pixbuf_new_from_file(path, NULL);
+ if (pixbuf) {
+ GtkTextMark *mark;
+ GtkTextIter start;
+
+ mark = gtk_text_buffer_create_mark(buffer, NULL, &iter, FALSE);
+
+ gtk_text_buffer_get_iter_at_mark(buffer, &start, mark);
+ gtk_text_iter_forward_to_tag_toggle(&iter, tag);
+ gtk_text_buffer_delete(buffer, &start, &iter);
+
+ gtk_text_buffer_insert_pixbuf(buffer, &iter, pixbuf);
+
+ g_object_unref(pixbuf);
+ gtk_text_buffer_delete_mark(buffer, mark);
+ }
+
+ g_free(image_path);
+ g_free(lang);
+ g_free(path);
+ break;
+ }
+
+ g_free(lang);
+ }
+
+ if (tags)
+ g_slist_free(tags);
+ } while (gtk_text_iter_forward_to_tag_toggle(&iter, NULL));
+}
+
+static gboolean
+append_text(MarkdownTextView * self,
+ const gchar * text)
+{
+ GtkTextIter iter;
+ GtkTextBuffer *text_buffer;
+ gchar *line;
+
+ g_return_val_if_fail(IS_MARKDOWN_TEXTVIEW(self), FALSE);
+
+ text_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(self));
+ gtk_text_buffer_get_end_iter(text_buffer, &iter);
+
+ line = egg_markdown_parse(self->markdown, text);
+ if (line && *line) {
+ gtk_text_buffer_insert_markup(text_buffer, &iter, line);
+ gtk_text_buffer_insert(text_buffer, &iter, "\n", 1);
+ g_free(line);
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+gboolean
+markdown_textview_set_text(MarkdownTextView * self,
+ const gchar * text)
+{
+ gboolean result = TRUE;
+ gchar **lines;
+ gint line;
+
+ g_return_val_if_fail(IS_MARKDOWN_TEXTVIEW(self), FALSE);
+
+ markdown_textview_clear(self);
+
+ lines = g_strsplit(text, "\n", 0);
+ for (line = 0; result && lines[line]; line++) {
+ result = append_text(self, (const gchar *)lines[line]);
+ }
+ g_strfreev(lines);
+
+ load_images(self);
+
+ return result;
+}
+
+gboolean
+markdown_textview_load_file(MarkdownTextView * self,
+ const gchar * file_name)
+{
+ FILE *text_file;
+ gchar *path;
+
+ g_return_val_if_fail(IS_MARKDOWN_TEXTVIEW(self), FALSE);
+
+ path = g_build_filename(self->image_directory, file_name, NULL);
+
+ /* we do assume UTF-8 encoding */
+ if ((text_file = fopen(path, "rb"))) {
+ GtkTextBuffer *text_buffer;
+ GtkTextIter iter;
+ gchar *line;
+ gchar buffer[EGG_MARKDOWN_MAX_LINE_LENGTH];
+
+ text_buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(self));
+
+ gtk_text_buffer_set_text(text_buffer, "\n", 1);
+ gtk_text_buffer_get_start_iter(text_buffer, &iter);
+
+ egg_markdown_clear(self->markdown);
+
+ while (fgets(buffer, EGG_MARKDOWN_MAX_LINE_LENGTH, text_file)) {
+ line = egg_markdown_parse(self->markdown, buffer);
+
+ if (line && *line) {
+ gtk_text_buffer_insert_markup(text_buffer, &iter, line);
+ gtk_text_buffer_insert(text_buffer, &iter, "\n", 1);
+ }
+
+ g_free(line);
+ }
+ fclose(text_file);
+
+ load_images(self);
+
+ g_signal_emit(self, markdown_textview_signals[FILE_LOAD_COMPLETE], 0, file_name);
+
+ g_free(path);
+
+ return TRUE;
+ }
+
+ g_free(path);
+
+ return FALSE;
+}
+
+void
+markdown_textview_set_image_directory(MarkdownTextView * self, const gchar *directory)
+{
+ g_return_if_fail(IS_MARKDOWN_TEXTVIEW(self));
+
+ g_free(self->image_directory);
+ self->image_directory = g_strdup(directory);
+}
+
+static void markdown_textview_init(MarkdownTextView * self)
+{
+ self->markdown = egg_markdown_new();
+ self->image_directory = g_strdup(".");
+
+ egg_markdown_set_output(self->markdown, EGG_MARKDOWN_OUTPUT_PANGO);
+ egg_markdown_set_escape(self->markdown, TRUE);
+ egg_markdown_set_autocode(self->markdown, TRUE);
+ egg_markdown_set_smart_quoting(self->markdown, TRUE);
+
+ gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(self), GTK_WRAP_WORD);
+ gtk_text_view_set_editable(GTK_TEXT_VIEW(self), FALSE);
+ gtk_text_view_set_left_margin(GTK_TEXT_VIEW(self), 10);
+ gtk_text_view_set_right_margin(GTK_TEXT_VIEW(self), 10);
+ gtk_text_view_set_pixels_above_lines(GTK_TEXT_VIEW(self), 3);
+ gtk_text_view_set_pixels_below_lines(GTK_TEXT_VIEW(self), 3);
+
+ g_signal_connect(self, "event-after",
+ G_CALLBACK(event_after), NULL);
+ g_signal_connect(self, "key-press-event",
+ G_CALLBACK(key_press_event), NULL);
+ g_signal_connect(self, "motion-notify-event",
+ G_CALLBACK(motion_notify_event), NULL);
+ g_signal_connect(self, "visibility-notify-event",
+ G_CALLBACK(visibility_notify_event), NULL);
+}
+