/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
/*
* Copyright (C) 2008 Imendio AB
* Copyright (C) 2008 Sven Herzberg
*
* 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"
#include "dh-assistant-view.h"
#include <string.h>
#include <glib/gi18n-lib.h>
#include "dh-util.h"
#include "dh-book.h"
#include "dh-book-manager.h"
/**
* SECTION:dh-assistant-view
* @Title: DhAssistantView
* @Short_description: A small “assistant” widget for displaying just one hit
*
* #DhAssistantView is a subclass of #WebKitWebView for displaying the
* documentation of just one symbol.
*
* A possible use-case: in a text editor, pressing a keyboard shortcut could
* display this widget for the symbol under the cursor.
*
* With the Devhelp application, an assistant can easily be launched with the
* command line option `--search-assistant`.
*/
typedef struct {
DhLink *link;
gchar *current_search;
guint snippet_loaded : 1;
} DhAssistantViewPrivate;
enum {
SIGNAL_OPEN_URI,
N_SIGNALS
};
static guint signals[N_SIGNALS] = { 0 };
G_DEFINE_TYPE_WITH_PRIVATE (DhAssistantView, dh_assistant_view, WEBKIT_TYPE_WEB_VIEW);
static void
view_finalize (GObject *object)
{
DhAssistantView *view = DH_ASSISTANT_VIEW (object);
DhAssistantViewPrivate *priv = dh_assistant_view_get_instance_private (view);
if (priv->link) {
g_object_unref (priv->link);
}
g_free (priv->current_search);
G_OBJECT_CLASS (dh_assistant_view_parent_class)->finalize (object);
}
static gboolean
assistant_decide_policy (WebKitWebView *web_view,
WebKitPolicyDecision *decision,
WebKitPolicyDecisionType decision_type)
{
DhAssistantViewPrivate *priv;
const gchar *uri;
WebKitNavigationPolicyDecision *navigation_decision;
WebKitNavigationAction *navigation_action;
WebKitNavigationType navigation_type;
WebKitURIRequest *request;
if (decision_type != WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION) {
webkit_policy_decision_ignore (decision);
return TRUE;
}
priv = dh_assistant_view_get_instance_private (DH_ASSISTANT_VIEW (web_view));
navigation_decision = WEBKIT_NAVIGATION_POLICY_DECISION (decision);
navigation_action = webkit_navigation_policy_decision_get_navigation_action (navigation_decision);
navigation_type = webkit_navigation_action_get_navigation_type (navigation_action);
if (navigation_type != WEBKIT_NAVIGATION_TYPE_LINK_CLICKED) {
if (! priv->snippet_loaded) {
priv->snippet_loaded = TRUE;
webkit_policy_decision_use (decision);
}
webkit_policy_decision_ignore (decision);
return TRUE;
}
request = webkit_navigation_action_get_request (navigation_action);
uri = webkit_uri_request_get_uri (request);
if (strcmp (uri, "about:blank") == 0) {
webkit_policy_decision_use (decision);
return TRUE;
}
g_signal_emit (web_view, signals[SIGNAL_OPEN_URI], 0, uri);
webkit_policy_decision_ignore (decision);
return TRUE;
}
static gboolean
assistant_button_press_event (GtkWidget *widget,
GdkEventButton *event)
{
/* Block webkit's builtin context menu. */
if (event->button != 1) {
return TRUE;
}
return GTK_WIDGET_CLASS (dh_assistant_view_parent_class)->button_press_event (widget, event);
}
static void
dh_assistant_view_class_init (DhAssistantViewClass* klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
WebKitWebViewClass *web_view_class = WEBKIT_WEB_VIEW_CLASS (klass);
object_class->finalize = view_finalize;
widget_class->button_press_event = assistant_button_press_event;
web_view_class->decide_policy = assistant_decide_policy;
/**
* DhAssistantView::open-uri:
* @view: the view on which the signal is emitted
* @uri: the uri to open
*/
signals[SIGNAL_OPEN_URI] = g_signal_new ("open-uri",
G_TYPE_FROM_CLASS (object_class),
0, 0,
NULL, NULL,
NULL,
G_TYPE_NONE, 1,
G_TYPE_STRING);
}
static void
dh_assistant_view_init (DhAssistantView *view)
{
}
/**
* dh_assistant_view_new:
*
* Returns: (transfer floating): a new #DhAssistantView widget.
*/
GtkWidget *
dh_assistant_view_new (void)
{
return g_object_new (DH_TYPE_ASSISTANT_VIEW, NULL);
}
static const gchar *
find_in_buffer (const gchar *buffer,
const gchar *key,
gsize length,
gsize key_length)
{
gsize m = 0;
gsize i = 0;
while (i < length) {
if (key[m] == buffer[i]) {
m++;
if (m == key_length) {
return buffer + i - m + 1;
}
} else {
m = 0;
}
i++;
}
return NULL;
}
/**
* dh_assistant_view_set_link:
* @view: a #DhAssistantView.
* @link: (nullable): a #DhLink to set or %NULL.
*
* Open @link in the assistant view, if %NULL the view will be blanked.
*
* Returns: %TRUE if the requested link is open, %FALSE otherwise.
*/
gboolean
dh_assistant_view_set_link (DhAssistantView *view,
DhLink *link)
{
DhAssistantViewPrivate *priv;
gchar *uri;
const gchar *anchor;
gchar *filename;
GMappedFile *file;
const gchar *contents;
gsize length;
gchar *key;
gsize key_length;
gsize offset = 0;
const gchar *start;
const gchar *end;
g_return_val_if_fail (DH_IS_ASSISTANT_VIEW (view), FALSE);
priv = dh_assistant_view_get_instance_private (view);
if (priv->link == link) {
return TRUE;
}
if (priv->link) {
dh_link_unref (priv->link);
priv->link = NULL;
}
if (link) {
link = dh_link_ref (link);
} else {
webkit_web_view_load_uri (WEBKIT_WEB_VIEW (view), "about:blank");
return TRUE;
}
/* FIXME uri can be NULL. */
uri = dh_link_get_uri (link);
anchor = strrchr (uri, '#');
if (anchor) {
filename = g_strndup (uri, anchor - uri);
anchor++;
} else {
g_free (uri);
return FALSE;
}
if (g_str_has_prefix (filename, "file://"))
offset = 7;
file = g_mapped_file_new (filename + offset, FALSE, NULL);
if (!file) {
g_free (filename);
g_free (uri);
return FALSE;
}
contents = g_mapped_file_get_contents (file);
length = g_mapped_file_get_length (file);
key = g_strdup_printf ("<a name=\"%s\"", anchor);
g_free (uri);
key_length = strlen (key);
start = find_in_buffer (contents, key, length, key_length);
g_free (key);
end = NULL;
if (start) {
const gchar *start_key;
const gchar *end_key;
length -= start - contents;
start_key = "<pre class=\"programlisting\">";
start = find_in_buffer (start,
start_key,
length,
strlen (start_key));
end_key = "<div class=\"refsect";
if (start) {
end = find_in_buffer (start, end_key,
length - strlen (start_key),
strlen (end_key));
if (!end) {
end_key = "<div class=\"footer";
end = find_in_buffer (start, end_key,
length - strlen (start_key),
strlen (end_key));
}
}
}
if (start && end) {
gchar *buf;
gboolean break_line;
const gchar *function;
gchar *stylesheet;
gchar *stylesheet_uri;
gchar *stylesheet_html = NULL;
gchar *javascript;
gchar *javascript_uri;
gchar *javascript_html = NULL;
gchar *html;
buf = g_strndup (start, end-start);
/* Try to reformat function signatures so they take less
* space and look nicer. Don't reformat things that don't
* look like functions.
*/
switch (dh_link_get_link_type (link)) {
case DH_LINK_TYPE_FUNCTION:
break_line = TRUE;
function = "onload=\"reformatSignature()\"";
break;
case DH_LINK_TYPE_MACRO:
break_line = TRUE;
function = "onload=\"cleanupSignature()\"";
break;
case DH_LINK_TYPE_BOOK:
case DH_LINK_TYPE_PAGE:
case DH_LINK_TYPE_KEYWORD:
case DH_LINK_TYPE_STRUCT:
case DH_LINK_TYPE_ENUM:
case DH_LINK_TYPE_TYPEDEF:
case DH_LINK_TYPE_PROPERTY:
case DH_LINK_TYPE_SIGNAL:
default:
break_line = FALSE;
function = "";
break;
}
if (break_line) {
gchar *name;
name = strstr (buf, dh_link_get_name (link));
if (name && name > buf) {
name[-1] = '\n';
}
}
stylesheet = dh_util_build_data_filename ("devhelp",
"assistant",
"assistant.css",
NULL);
stylesheet_uri = dh_util_create_data_uri_for_filename (stylesheet,
"text/css");
g_free (stylesheet);
if (stylesheet_uri)
stylesheet_html = g_strdup_printf ("<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\"/>",
stylesheet_uri);
g_free (stylesheet_uri);
javascript = dh_util_build_data_filename ("devhelp",
"assistant",
"assistant.js",
NULL);
javascript_uri = dh_util_create_data_uri_for_filename (javascript,
"application/javascript");
g_free (javascript);
if (javascript_uri)
javascript_html = g_strdup_printf ("<script src=\"%s\"></script>", javascript_uri);
g_free (javascript_uri);
html = g_strdup_printf (
"<html>"
"<head>"
"%s"
"%s"
"</head>"
"<body %s>"
"<div class=\"title\">%s: <a href=\"%s\">%s</a></div>"
"<div class=\"subtitle\">%s %s</div>"
"<div class=\"content\">%s</div>"
"</body>"
"</html>",
stylesheet_html,
javascript_html,
function,
dh_link_type_to_string (dh_link_get_link_type (link)),
dh_link_get_uri (link),
dh_link_get_name (link),
_("Book:"),
dh_link_get_book_title (link),
buf);
g_free (buf);
g_free (stylesheet_html);
g_free (javascript_html);
priv->snippet_loaded = FALSE;
webkit_web_view_load_html (
WEBKIT_WEB_VIEW (view),
html,
filename);
g_free (html);
} else {
webkit_web_view_load_uri (WEBKIT_WEB_VIEW (view), "about:blank");
}
g_mapped_file_unref (file);
g_free (filename);
return TRUE;
}
/**
* dh_assistant_view_search:
* @view: a #DhAssistantView.
* @str: the search query.
*
* Search for @str in the current assistant view.
*
* Returns: %TRUE if @str was found, %FALSE otherwise.
*/
gboolean
dh_assistant_view_search (DhAssistantView *view,
const gchar *str)
{
DhAssistantViewPrivate *priv;
DhBookManager *book_manager;
const gchar *name;
DhLink *link;
DhLink *exact_link;
DhLink *prefix_link;
GList *books;
g_return_val_if_fail (DH_IS_ASSISTANT_VIEW (view), FALSE);
g_return_val_if_fail (str, FALSE);
priv = dh_assistant_view_get_instance_private (view);
/* Filter out very short strings. */
if (strlen (str) < 4) {
return FALSE;
}
if (priv->current_search && strcmp (priv->current_search, str) == 0) {
return FALSE;
}
g_free (priv->current_search);
priv->current_search = g_strdup (str);
prefix_link = NULL;
exact_link = NULL;
book_manager = dh_book_manager_get_singleton ();
for (books = dh_book_manager_get_books (book_manager);
!exact_link && books;
books = g_list_next (books)) {
GList *l;
for (l = dh_book_get_links (DH_BOOK (books->data));
l && exact_link == NULL;
l = l->next) {
DhLinkType type;
link = l->data;
type = dh_link_get_link_type (link);
if (type == DH_LINK_TYPE_BOOK ||
type == DH_LINK_TYPE_PAGE ||
type == DH_LINK_TYPE_KEYWORD) {
continue;
}
name = dh_link_get_name (link);
if (strcmp (name, str) == 0) {
exact_link = link;
}
else if (g_str_has_prefix (name, str)) {
/* Prefer shorter prefix matches. */
if (!prefix_link) {
prefix_link = link;
}
else if (strlen (dh_link_get_name (prefix_link)) > strlen (name)) {
prefix_link = link;
}
}
}
}
if (exact_link) {
/*g_print ("exact hit: '%s' '%s'\n", exact_link->name, str);*/
dh_assistant_view_set_link (view, exact_link);
}
else if (prefix_link) {
/*g_print ("prefix hit: '%s' '%s'\n", prefix_link->name, str);*/
dh_assistant_view_set_link (view, prefix_link);
} else {
/*g_print ("no hit\n");*/
/*assistant_view_set_link (view, NULL);*/
return FALSE;
}
return TRUE;
}