1
/* The GIMP -- an image manipulation program
2
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
4
* The GIMP Help Browser
5
* Copyright (C) 1999-2004 Sven Neumann <sven@gimp.org>
6
* Michael Natterer <mitch@gimp.org>
10
* This program is free software; you can redistribute it and/or modify
11
* it under the terms of the GNU General Public License as published by
12
* the Free Software Foundation; either version 2 of the License, or
13
* (at your option) any later version.
15
* This program is distributed in the hope that it will be useful,
16
* but WITHOUT ANY WARRANTY; without even the implied warranty of
17
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
* GNU General Public License for more details.
20
* You should have received a copy of the GNU General Public License
21
* along with this program; if not, write to the Free Software
22
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
29
#include <sys/types.h>
34
#include <libgtkhtml/gtkhtml.h>
36
#include "libgimpwidgets/gimpwidgets.h"
38
#include "libgimp/gimp.h"
39
#include "libgimp/gimpui.h"
42
#include "libgimpbase/gimpwin32-io.h"
49
#include "libgimp/stdplugins-intl.h"
65
/* local function prototypes */
67
static void browser_dialog_404 (HtmlDocument *doc,
69
const gchar *message);
71
static void button_callback (GtkWidget *widget,
73
static void update_toolbar (void);
74
static void combo_changed (GtkWidget *widget,
76
static void drag_begin (GtkWidget *widget,
77
GdkDragContext *context,
79
static void drag_data_get (GtkWidget *widget,
80
GdkDragContext *context,
81
GtkSelectionData *selection_data,
86
static void title_changed (HtmlDocument *doc,
87
const gchar *new_title,
89
static void link_clicked (HtmlDocument *doc,
92
static gboolean request_url (HtmlDocument *doc,
96
static gboolean io_handler (GIOChannel *io,
97
GIOCondition condition,
99
static void load_remote_page (const gchar *ref);
101
static void history_add (GtkComboBox *combo,
105
static gboolean has_case_prefix (const gchar *haystack,
106
const gchar *needle);
109
/* private variables */
111
static const gchar *eek_png_tag = "<h1>Eeek!</h1>";
113
static Queue *queue = NULL;
114
static gchar *current_ref = NULL;
116
static GtkWidget *back_button = NULL;
117
static GtkWidget *forward_button = NULL;
118
static GtkWidget *html = NULL;
120
static GtkTargetEntry help_dnd_target_table[] =
122
{ "_NETSCAPE_URL", 0, 0 },
126
/* public functions */
129
browser_dialog_open (void)
137
GtkWidget *drag_source;
140
GtkListStore *history;
141
GtkCellRenderer *cell;
144
gimp_ui_init ("helpbrowser", TRUE);
146
eek_png_path = g_build_filename (gimp_data_directory (),
147
"themes", "Default", "images",
148
"stock-wilber-eek-64.png", NULL);
150
if (g_file_test (eek_png_path, G_FILE_TEST_EXISTS))
151
eek_png_tag = g_strdup_printf ("<img src=\"%s\">", eek_png_path);
153
g_free (eek_png_path);
155
/* the dialog window */
156
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
157
gtk_window_set_title (GTK_WINDOW (window), _("GIMP Help browser"));
158
gtk_window_set_role (GTK_WINDOW (window), "helpbrowser");
160
gtk_window_set_default_size (GTK_WINDOW (window), 420, 500);
162
g_signal_connect (window, "destroy",
163
G_CALLBACK (gtk_main_quit),
166
vbox = gtk_vbox_new (FALSE, 2);
167
gtk_container_add (GTK_CONTAINER (window), vbox);
168
gtk_widget_show (vbox);
171
bbox = gtk_hbutton_box_new ();
172
gtk_button_box_set_layout (GTK_BUTTON_BOX (bbox), GTK_BUTTONBOX_START);
173
gtk_box_pack_start (GTK_BOX (vbox), bbox, FALSE, FALSE, 0);
174
gtk_widget_show (bbox);
176
button = gtk_button_new_from_stock (GTK_STOCK_INDEX);
177
gtk_container_add (GTK_CONTAINER (bbox), button);
178
gtk_widget_show (button);
180
g_signal_connect (button, "clicked",
181
G_CALLBACK (button_callback),
182
GINT_TO_POINTER (BUTTON_INDEX));
184
back_button = button = gtk_button_new_from_stock (GTK_STOCK_GO_BACK);
185
gtk_container_add (GTK_CONTAINER (bbox), button);
186
gtk_widget_set_sensitive (GTK_WIDGET (button), FALSE);
187
gtk_widget_show (button);
189
g_signal_connect (button, "clicked",
190
G_CALLBACK (button_callback),
191
GINT_TO_POINTER (BUTTON_BACK));
193
forward_button = button = gtk_button_new_from_stock (GTK_STOCK_GO_FORWARD);
194
gtk_container_add (GTK_CONTAINER (bbox), button);
195
gtk_widget_set_sensitive (GTK_WIDGET (button), FALSE);
196
gtk_widget_show (button);
198
g_signal_connect (button, "clicked",
199
G_CALLBACK (button_callback),
200
GINT_TO_POINTER (BUTTON_FORWARD));
202
button = gtk_button_new_from_stock (GTK_STOCK_CLOSE);
203
gtk_container_add (GTK_CONTAINER (bbox), button);
204
gtk_button_box_set_child_secondary (GTK_BUTTON_BOX (bbox), button, TRUE);
205
gtk_widget_show (button);
207
g_signal_connect_swapped (button, "clicked",
208
G_CALLBACK (gtk_widget_destroy),
211
hbox = gtk_hbox_new (FALSE, 2);
212
gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0);
213
gtk_widget_show (hbox);
215
/* the drag source */
216
drag_source = gtk_event_box_new ();
217
gtk_box_pack_start (GTK_BOX (hbox), drag_source, FALSE, FALSE, 4);
218
gtk_widget_show (drag_source);
220
gtk_drag_source_set (GTK_WIDGET (drag_source),
222
help_dnd_target_table,
223
G_N_ELEMENTS (help_dnd_target_table),
226
g_signal_connect (drag_source, "drag_begin",
227
G_CALLBACK (drag_begin),
229
g_signal_connect (drag_source, "drag_data_get",
230
G_CALLBACK (drag_data_get),
233
image = gtk_image_new_from_stock (GTK_STOCK_JUMP_TO, GTK_ICON_SIZE_BUTTON);
234
gtk_container_add (GTK_CONTAINER (drag_source), image);
235
gtk_widget_show (image);
237
/* the title combo */
238
history = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_STRING);
239
combo = gtk_combo_box_new_with_model (GTK_TREE_MODEL (history));
240
g_object_unref (history);
242
cell = gtk_cell_renderer_text_new ();
243
gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (combo), cell, TRUE);
244
gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (combo), cell,
245
"text", HISTORY_TITLE,
248
gtk_widget_set_size_request (GTK_WIDGET (combo), 320, -1);
249
gtk_box_pack_start (GTK_BOX (hbox), combo, TRUE, TRUE, 0);
250
gtk_widget_show (combo);
252
g_signal_connect (combo, "changed",
253
G_CALLBACK (combo_changed),
257
html = html_view_new ();
258
queue = queue_new ();
260
gtk_widget_set_size_request (GTK_WIDGET (html), -1, 200);
263
gtk_scrolled_window_new (gtk_layout_get_hadjustment (GTK_LAYOUT (html)),
264
gtk_layout_get_vadjustment (GTK_LAYOUT (html)));
266
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll),
267
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
268
gtk_box_pack_start (GTK_BOX (vbox), scroll, TRUE, TRUE, 0);
269
gtk_widget_show (scroll);
271
gtk_container_add (GTK_CONTAINER (scroll), html);
272
gtk_widget_show (html);
274
html_view_set_document (HTML_VIEW (html), html_document_new ());
276
g_signal_connect (HTML_VIEW (html)->document, "title_changed",
277
G_CALLBACK (title_changed),
279
g_signal_connect (HTML_VIEW (html)->document, "link_clicked",
280
G_CALLBACK (link_clicked),
282
g_signal_connect (HTML_VIEW (html)->document, "request_url",
283
G_CALLBACK (request_url),
286
gtk_widget_grab_focus (html);
288
gtk_widget_show (window);
292
idle_jump_to_anchor (const gchar *anchor)
295
html_view_jump_to_anchor (HTML_VIEW (html), anchor);
301
browser_dialog_load (const gchar *ref,
302
gboolean add_to_queue)
304
HtmlDocument *doc = HTML_VIEW (html)->document;
305
GtkAdjustment *adj = gtk_layout_get_vadjustment (GTK_LAYOUT (html));
311
g_return_if_fail (ref != NULL);
317
current_ref = g_strdup (ref);
319
slash = strrchr (current_ref, '/');
325
abs = uri_to_abs (ref, current_ref);
329
anchor = strchr (ref, '#');
330
if (anchor && anchor[0] && anchor[1])
332
new_ref = g_strconcat (abs, anchor, NULL);
337
new_ref = g_strdup (abs);
341
if (! has_case_prefix (abs, "file:/"))
343
load_remote_page (ref);
347
tmp = uri_to_abs (current_ref, current_ref);
348
if (!tmp || strcmp (tmp, abs))
350
GError *error = NULL;
352
html_document_clear (doc);
353
html_document_open_stream (doc, "text/html");
354
gtk_adjustment_set_value (adj, 0.0);
357
g_idle_add_full (G_PRIORITY_LOW,
358
(GSourceFunc) idle_jump_to_anchor,
359
g_strdup (anchor), (GDestroyNotify) g_free);
361
if (! request_url (doc, abs, doc->current_stream, &error))
363
browser_dialog_404 (doc, abs, error->message);
364
g_error_free (error);
370
html_view_jump_to_anchor (HTML_VIEW (html), anchor);
372
gtk_adjustment_set_value (adj, 0.0);
377
g_free (current_ref);
378
current_ref = new_ref;
381
queue_add (queue, new_ref);
385
gtk_window_present (GTK_WINDOW (gtk_widget_get_toplevel (html)));
389
/* private functions */
392
browser_dialog_404 (HtmlDocument *doc,
394
const gchar *message)
396
gchar *msg = g_strdup_printf
398
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />"
399
"<head><title>%s</title></head>"
400
"<body bgcolor=\"white\">"
401
"<div align=\"center\">"
409
_("Document not found"),
411
_("The requested URL could not be loaded:"),
415
html_document_write_stream (doc, msg, strlen (msg));
421
button_callback (GtkWidget *widget,
426
switch (GPOINTER_TO_INT (data))
429
browser_dialog_load ("index.html", TRUE);
433
if (!(ref = queue_prev (queue)))
435
browser_dialog_load (ref, FALSE);
436
queue_move_prev (queue);
440
if (!(ref = queue_next (queue)))
442
browser_dialog_load (ref, FALSE);
443
queue_move_next (queue);
454
update_toolbar (void)
457
gtk_widget_set_sensitive (back_button, queue_has_prev (queue));
459
gtk_widget_set_sensitive (forward_button, queue_has_next (queue));
463
combo_changed (GtkWidget *widget,
466
GtkComboBox *combo = GTK_COMBO_BOX (widget);
469
if (gtk_combo_box_get_active_iter (combo, &iter))
471
GValue value = { 0, };
473
gtk_tree_model_get_value (gtk_combo_box_get_model (combo),
474
&iter, HISTORY_REF, &value);
476
browser_dialog_load (g_value_get_string (&value), TRUE);
478
g_value_unset (&value);
483
drag_begin (GtkWidget *widget,
484
GdkDragContext *context,
487
gtk_drag_set_icon_stock (context, GTK_STOCK_JUMP_TO, -8, -8);
491
drag_data_get (GtkWidget *widget,
492
GdkDragContext *context,
493
GtkSelectionData *selection_data,
501
gtk_selection_data_set (selection_data,
502
selection_data->target,
505
strlen (current_ref));
509
title_changed (HtmlDocument *doc,
510
const gchar *new_title,
515
gchar *title = g_strstrip (g_strdup (new_title));
517
history_add (GTK_COMBO_BOX (data), current_ref, title);
523
history_add (GTK_COMBO_BOX (data), current_ref, _("Untitled"));
528
link_clicked (HtmlDocument *doc,
532
browser_dialog_load (url, TRUE);
536
request_url (HtmlDocument *doc,
544
g_return_val_if_fail (url != NULL, TRUE);
545
g_return_val_if_fail (stream != NULL, TRUE);
547
abs = uri_to_abs (url, current_ref);
551
filename = g_filename_from_uri (abs, NULL, NULL);
558
fd = open (filename, O_RDONLY);
562
GIOChannel *io = g_io_channel_unix_new (fd);
564
g_io_channel_set_close_on_unref (io, TRUE);
565
g_io_channel_set_encoding (io, NULL, NULL);
567
g_io_add_watch (io, G_IO_IN | G_IO_ERR | G_IO_HUP | G_IO_NVAL,
576
G_FILE_ERROR, g_file_error_from_errno (errno),
586
io_handler (GIOChannel *io,
587
GIOCondition condition,
590
HtmlStream *stream = data;
594
if (condition & G_IO_IN)
596
if (g_io_channel_read_chars (io, buffer, sizeof (buffer),
597
&bytes, NULL) != G_IO_STATUS_ERROR
600
html_stream_write (stream, buffer, bytes);
604
html_stream_close (stream);
605
g_io_channel_unref (io);
610
if (condition & G_IO_HUP)
612
while (g_io_channel_read_chars (io, buffer, sizeof (buffer),
613
&bytes, NULL) != G_IO_STATUS_ERROR
616
html_stream_write (stream, buffer, bytes);
621
if (condition & (G_IO_ERR | G_IO_HUP | G_IO_NVAL))
623
html_stream_close (stream);
624
g_io_channel_unref (io);
633
load_remote_page (const gchar *ref)
635
GimpParam *return_vals;
638
/* try to call the user specified web browser */
639
return_vals = gimp_run_procedure ("plug_in_web_browser",
641
GIMP_PDB_STRING, ref,
643
gimp_destroy_params (return_vals, nreturn_vals);
647
history_add (GtkComboBox *combo,
651
GtkTreeModel *model = gtk_combo_box_get_model (combo);
654
GValue value = { 0, };
656
for (iter_valid = gtk_tree_model_get_iter_first (model, &iter);
658
iter_valid = gtk_tree_model_iter_next (model, &iter))
660
gtk_tree_model_get_value (model, &iter, HISTORY_REF, &value);
662
if (strcmp (g_value_get_string (&value), ref) == 0)
664
gtk_list_store_move_after (GTK_LIST_STORE (model), &iter, NULL);
665
g_value_unset (&value);
669
g_value_unset (&value);
674
gtk_list_store_prepend (GTK_LIST_STORE (model), &iter);
675
gtk_list_store_set (GTK_LIST_STORE (model), &iter,
676
HISTORY_TITLE, title,
681
g_signal_handlers_block_by_func (combo, combo_changed, NULL);
682
gtk_combo_box_set_active_iter (combo, &iter);
683
g_signal_handlers_unblock_by_func (combo, combo_changed, NULL);
686
/* Taken from glib/gconvert.c:
687
* Test of haystack has the needle prefix, comparing case
688
* insensitive. haystack may be UTF-8, but needle must
689
* contain only ascii.
692
has_case_prefix (const gchar *haystack, const gchar *needle)
694
const gchar *h = haystack;
695
const gchar *n = needle;
697
while (*n && *h && g_ascii_tolower (*n) == g_ascii_tolower (*h))