1
2 /*
3 * dialog-hyperlink.c: Add or edit a hyperlink
4 *
5 * Copyright (C) 2002 Jody Goldberg (jody@gnome.org)
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License as
9 * published by the Free Software Foundation; either version 2 of the
10 * License, or (at your option) version 3.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
20 * USA
21 */
22
23 #include <gnumeric-config.h>
24 #include <gnumeric.h>
25 #include <dialogs/dialogs.h>
26 #include <dialogs/help.h>
27
28 #include <commands.h>
29 #include <widgets/gnm-expr-entry.h>
30 #include <expr-name.h>
31 #include <expr.h>
32 #include <gui-util.h>
33 #include <hlink.h>
34 #include <mstyle.h>
35 #include <style-color.h>
36 #include <sheet-control.h>
37 #include <sheet-view.h>
38 #include <sheet-style.h>
39 #include <value.h>
40 #include <wbc-gtk.h>
41 #include <goffice/goffice.h>
42
43 #include <glib/gi18n-lib.h>
44
45 #include <string.h>
46
47
48 typedef struct {
49 WBCGtk *wbcg;
50 Workbook *wb;
51 SheetControl *sc;
52 Sheet *sheet;
53
54 GtkBuilder *gui;
55 GtkWidget *dialog;
56
57 GtkImage *type_image;
58 GtkLabel *type_descriptor;
59 GnmExprEntry *internal_link_ee;
60 GnmHLink *link;
61 gboolean is_new;
62
63 GtkWidget *use_def_widget;
64 } HyperlinkState;
65
66 static void
dhl_free(HyperlinkState * state)67 dhl_free (HyperlinkState *state)
68 {
69 if (state->gui != NULL) {
70 g_object_unref (state->gui);
71 state->gui = NULL;
72 }
73 if (state->link != NULL) {
74 g_object_unref (state->link);
75 state->link = NULL;
76 }
77 state->dialog = NULL;
78 g_free (state);
79 }
80
81 static char *
dhl_get_default_tip(char const * const target)82 dhl_get_default_tip (char const * const target) {
83 char *default_text = _("Left click once to follow this link.\n"
84 "Middle click once to select this cell");
85 if (target == NULL)
86 return g_strdup (default_text);
87 else
88 return g_strjoin ("\n", target, default_text, NULL);
89 }
90
91 static void
dhl_set_tip(HyperlinkState * state)92 dhl_set_tip (HyperlinkState* state)
93 {
94 char const *tip = gnm_hlink_get_tip (state->link);
95 GtkTextBuffer *tb;
96 GtkWidget *w;
97
98 if (state->is_new) {
99 w = go_gtk_builder_get_widget (state->gui, "use-default-tip");
100 gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (w), TRUE);
101 return;
102 }
103
104 if (tip != NULL) {
105 char const * const target = gnm_hlink_get_target (state->link);
106 char *default_tip = dhl_get_default_tip (target);
107 gboolean is_default = (strcmp (tip, default_tip) == 0);
108 gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (state->use_def_widget),
109 is_default);
110 g_free (default_tip);
111 if (is_default)
112 return;
113 }
114 w = go_gtk_builder_get_widget (state->gui, "use-this-tip");
115 gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (w), TRUE);
116
117 tb = gtk_text_view_get_buffer
118 (GTK_TEXT_VIEW (go_gtk_builder_get_widget (state->gui, "tip-entry")));
119
120 gtk_text_buffer_set_text (tb, (tip == NULL) ? "" : tip, -1);
121 }
122
123 static char *
dhl_get_tip(HyperlinkState * state,char const * target)124 dhl_get_tip (HyperlinkState *state, char const *target)
125 {
126 if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (state->use_def_widget)))
127 return NULL;
128 else {
129 char *tip;
130 GtkTextBuffer *tb = gtk_text_view_get_buffer
131 (GTK_TEXT_VIEW (go_gtk_builder_get_widget (state->gui, "tip-entry")));
132 GtkTextIter start_iter, end_iter;
133
134 gtk_text_buffer_get_start_iter (tb, &start_iter);
135 gtk_text_buffer_get_end_iter (tb, &end_iter);
136
137 tip = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE);
138
139 if (tip != NULL && strlen (tip) == 0) {
140 g_free (tip);
141 tip = NULL;
142 }
143
144 return tip;
145 }
146 }
147
148 static void
dhl_set_target_cur_wb(HyperlinkState * state,const char * const target)149 dhl_set_target_cur_wb (HyperlinkState *state, const char* const target)
150 {
151 gnm_expr_entry_load_from_text (state->internal_link_ee, target);
152 }
153
154 static char *
dhl_get_target_cur_wb(HyperlinkState * state,gboolean * success)155 dhl_get_target_cur_wb (HyperlinkState *state, gboolean *success)
156 {
157 char *ret = NULL;
158 GnmExprEntry *gee = state->internal_link_ee;
159 char const *target = gnm_expr_entry_get_text (gee);
160 Sheet *sheet = sc_sheet (state->sc);
161 GnmValue *val;
162
163 *success = FALSE;
164 if (strlen (target) == 0) {
165 *success = TRUE;
166 } else {
167 val = gnm_expr_entry_parse_as_value (gee, sheet);
168 if (!val) {
169 /* not an address, is it a name ? */
170 GnmParsePos pp;
171 GnmNamedExpr *nexpr;
172
173 parse_pos_init_sheet (&pp, sheet);
174 nexpr = expr_name_lookup (&pp, target);
175 if (nexpr != NULL)
176 val = gnm_expr_top_get_range (nexpr->texpr);
177 }
178 if (val) {
179 *success = TRUE;
180 ret = g_strdup (target);
181 value_release (val);
182 } else {
183 go_gtk_notice_dialog (GTK_WINDOW (state->dialog),
184 GTK_MESSAGE_ERROR,
185 _("Not a range or name"));
186 gnm_expr_entry_grab_focus (gee, TRUE);
187 }
188 }
189 return ret;
190 }
191
192 static void
dhl_set_target_external(HyperlinkState * state,const char * const target)193 dhl_set_target_external (HyperlinkState *state, const char* const target)
194 {
195 GtkWidget *w = go_gtk_builder_get_widget (state->gui, "external-link");
196
197 gtk_entry_set_text (GTK_ENTRY (w), target);
198 }
199
200 static char *
dhl_get_target_external(HyperlinkState * state,gboolean * success)201 dhl_get_target_external (HyperlinkState *state, gboolean *success)
202 {
203 GtkWidget *w = go_gtk_builder_get_widget (state->gui, "external-link");
204 const char *target = gtk_entry_get_text (GTK_ENTRY (w));
205
206 *success = TRUE;
207 return strlen (target) > 0 ? g_strdup (target) : NULL;
208 }
209
210 static void
dhl_set_target_email(HyperlinkState * state,const char * const target)211 dhl_set_target_email (HyperlinkState *state, const char* const target)
212 {
213 GtkWidget *w = go_gtk_builder_get_widget (state->gui, "email-address");
214 GtkWidget *w2 = go_gtk_builder_get_widget (state->gui, "email-subject");
215 gchar* cursor;
216 gchar* subject;
217 gchar* guitext;
218
219 if (!target || *target == '\0')
220 return;
221
222 if( strncmp (target, "mailto:", strlen ("mailto:")) != 0)
223 return;
224
225 cursor = g_strdup (target + strlen ("mailto:"));
226
227 subject = strstr (cursor, "?subject=");
228 if (subject) {
229 guitext = g_uri_unescape_string (subject + strlen ("?subject="),
230 NULL);
231 gtk_entry_set_text (GTK_ENTRY (w2), guitext);
232 *subject = '\0';
233 g_free (guitext);
234 }
235
236 guitext = g_uri_unescape_string (cursor, NULL);
237
238 gtk_entry_set_text (GTK_ENTRY (w), guitext);
239
240 g_free (guitext);
241 g_free (cursor);
242 }
243
244 static char*
dhl_get_target_email(HyperlinkState * state,gboolean * success)245 dhl_get_target_email (HyperlinkState *state, gboolean *success)
246 {
247 GtkWidget *w = go_gtk_builder_get_widget (state->gui, "email-address");
248 GtkWidget *w2 = go_gtk_builder_get_widget (state->gui, "email-subject");
249 const char *address = gtk_entry_get_text (GTK_ENTRY (w));
250 const char *subject = gtk_entry_get_text (GTK_ENTRY (w2));
251 gchar* enc_subj, *enc_addr;
252 gchar* result;
253
254 *success = TRUE;
255 if (!address || *address == '\0') {
256 return NULL;
257 }
258
259 enc_addr = go_url_encode (address, 0);
260 if (!subject || *subject == '\0') {
261 result = g_strconcat ("mailto:", enc_addr, NULL);
262 } else {
263 enc_subj = go_url_encode (subject, 0);
264
265 result = g_strconcat ("mailto:", enc_addr,
266 "?subject=", enc_subj, NULL);
267 g_free (enc_subj);
268 }
269
270 g_free (enc_addr);
271
272 return result;
273 }
274
275 static void
dhl_set_target_url(HyperlinkState * state,const char * const target)276 dhl_set_target_url (HyperlinkState *state, const char* const target)
277 {
278 GtkWidget *w = go_gtk_builder_get_widget (state->gui, "url");
279
280 gtk_entry_set_text (GTK_ENTRY (w), target);
281 }
282
283 static char *
dhl_get_target_url(HyperlinkState * state,gboolean * success)284 dhl_get_target_url (HyperlinkState *state, gboolean *success)
285 {
286 GtkWidget *w = go_gtk_builder_get_widget (state->gui, "url");
287 const char *target = gtk_entry_get_text (GTK_ENTRY (w));
288
289 *success = TRUE;
290 return strlen (target) > 0 ? g_strdup (target) : NULL;
291 }
292
293 static struct {
294 char const *label;
295 char const *icon_name;
296 char const *name;
297 char const *widget_name;
298 char const *descriptor;
299 void (*set_target) (HyperlinkState *state, const char* const target);
300 char * (*get_target) (HyperlinkState *state, gboolean *success);
301 } const type[] = {
302 { N_("Internal Link"), "gnumeric-link-internal",
303 "GnmHLinkCurWB", "internal-link-grid",
304 N_("Jump to specific cells or named range in the current workbook"),
305 dhl_set_target_cur_wb,
306 dhl_get_target_cur_wb },
307
308 { N_("External Link"), "gnumeric-link-external",
309 "GnmHLinkExternal", "external-link-grid" ,
310 N_("Open an external file with the specified name"),
311 dhl_set_target_external,
312 dhl_get_target_external },
313 { N_("Email Link"), "gnumeric-link-email",
314 "GnmHLinkEMail", "email-grid" ,
315 N_("Prepare an email"),
316 dhl_set_target_email,
317 dhl_get_target_email },
318 { N_("Web Link"), "gnumeric-link-url",
319 "GnmHLinkURL", "url-grid" ,
320 N_("Browse to the specified URL"),
321 dhl_set_target_url,
322 dhl_get_target_url }
323 };
324
325 static void
dhl_set_target(HyperlinkState * state)326 dhl_set_target (HyperlinkState* state)
327 {
328 unsigned i;
329 char const * const target = gnm_hlink_get_target (state->link);
330 char const * type_name;
331
332 if (target) {
333 type_name = G_OBJECT_TYPE_NAME (state->link);
334 for (i = 0 ; i < G_N_ELEMENTS (type); i++) {
335 if (strcmp (type_name, type[i].name) == 0) {
336 if (type[i].set_target)
337 (type[i].set_target) (state, target);
338 break;
339 }
340 }
341 }
342 }
343
344 static char *
dhl_get_target(HyperlinkState * state,gboolean * success)345 dhl_get_target (HyperlinkState *state, gboolean *success)
346 {
347 unsigned i;
348 char const *type_name = G_OBJECT_TYPE_NAME (state->link);
349
350 *success = FALSE;
351 for (i = 0 ; i < G_N_ELEMENTS (type); i++) {
352 if (strcmp (type_name, type[i].name) == 0) {
353 if (type[i].get_target)
354 return (type[i].get_target) (state, success);
355 break;
356 }
357 }
358
359 return NULL;
360 }
361
362
363 static void
dhl_cb_cancel(G_GNUC_UNUSED GtkWidget * button,HyperlinkState * state)364 dhl_cb_cancel (G_GNUC_UNUSED GtkWidget *button, HyperlinkState *state)
365 {
366 gtk_widget_destroy (state->dialog);
367 }
368
369 static void
dhl_cb_ok(G_GNUC_UNUSED GtkWidget * button,HyperlinkState * state)370 dhl_cb_ok (G_GNUC_UNUSED GtkWidget *button, HyperlinkState *state)
371 {
372 GnmStyle *style;
373 char *cmdname;
374 char *target;
375 char *tip;
376 gboolean success;
377
378 target = dhl_get_target (state, &success);
379 if (!success)
380 return; /* Let user continue editing */
381
382 wb_control_sheet_focus (GNM_WBC (state->wbcg), state->sheet);
383
384 if (target) {
385 GnmHLink *new_link = gnm_hlink_dup_to (state->link, state->sheet);
386 gnm_hlink_set_target (new_link, target);
387 tip = dhl_get_tip (state, target);
388 gnm_hlink_set_tip (new_link, tip);
389 g_free (tip);
390 style = gnm_style_new ();
391 gnm_style_set_hlink (style, new_link);
392 gnm_style_set_font_uline (style, UNDERLINE_SINGLE);
393 gnm_style_set_font_color (style, gnm_color_new_go (GO_COLOR_BLUE));
394
395 if (state->is_new) {
396 cmdname = _("Add Hyperlink");
397 cmd_selection_hyperlink (GNM_WBC (state->wbcg),
398 style,
399 cmdname, target);
400 } else {
401 cmdname = _("Edit Hyperlink");
402 cmd_selection_hyperlink (GNM_WBC (state->wbcg),
403 style,
404 cmdname, NULL);
405 g_free (target);
406 }
407 } else if (!state->is_new) {
408 style = gnm_style_new ();
409 gnm_style_set_hlink (style, NULL);
410 cmdname = _("Remove Hyperlink");
411 cmd_selection_hyperlink (GNM_WBC (state->wbcg), style,
412 cmdname, NULL);
413 }
414 gtk_widget_destroy (state->dialog);
415 }
416
417 static void
dhl_setup_type(HyperlinkState * state)418 dhl_setup_type (HyperlinkState *state)
419 {
420 GtkWidget *w;
421 char const *name = G_OBJECT_TYPE_NAME (state->link);
422 unsigned i;
423
424 for (i = 0 ; i < G_N_ELEMENTS (type); i++) {
425 w = go_gtk_builder_get_widget (state->gui, type[i].widget_name);
426
427 if (!strcmp (name, type[i].name)) {
428 gtk_widget_show_all (w);
429 gtk_image_set_from_icon_name
430 (state->type_image, type[i].icon_name,
431 GTK_ICON_SIZE_DIALOG);
432 gtk_label_set_text (state->type_descriptor,
433 _(type[i].descriptor));
434 } else
435 gtk_widget_hide (w);
436 }
437 }
438
439 static void
dhl_set_type(HyperlinkState * state,GType type)440 dhl_set_type (HyperlinkState *state, GType type)
441 {
442 GnmHLink *old = state->link;
443
444 state->link = gnm_hlink_new (type, state->sheet);
445 if (old != NULL) {
446 gnm_hlink_set_target (state->link, gnm_hlink_get_target (old));
447 gnm_hlink_set_tip (state->link, gnm_hlink_get_tip (old));
448 g_object_unref (old);
449 }
450 dhl_setup_type (state);
451 }
452
453 static void
dhl_cb_menu_changed(GtkComboBox * box,HyperlinkState * state)454 dhl_cb_menu_changed (GtkComboBox *box, HyperlinkState *state)
455 {
456 int i = gtk_combo_box_get_active (box);
457 dhl_set_type (state, g_type_from_name (
458 type [i].name));
459 }
460
461 static gboolean
dhl_init(HyperlinkState * state)462 dhl_init (HyperlinkState *state)
463 {
464 static char const * const label[] = {
465 "internal-link-label",
466 "external-link-label",
467 "email-address-label",
468 "email-subject-label",
469 "url-label",
470 "use-this-tip"
471 };
472 GtkWidget *w;
473 GtkSizeGroup *size_group;
474 GnmExprEntry *expr_entry;
475 unsigned i, select = 0;
476 GtkListStore *store;
477 GtkTreeIter iter;
478 GtkCellRenderer *renderer;
479
480 #ifdef GNM_NO_MAILTO
481 gtk_widget_hide (go_gtk_builder_get_widget (state->gui, "email-grid"));
482 #endif
483 size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
484 for (i = 0 ; i < G_N_ELEMENTS (label); i++)
485 gtk_size_group_add_widget (size_group,
486 go_gtk_builder_get_widget (state->gui, label[i]));
487 g_object_unref (size_group);
488
489 w = go_gtk_builder_get_widget (state->gui, "link-type-image");
490 state->type_image = GTK_IMAGE (w);
491 w = go_gtk_builder_get_widget (state->gui, "link-type-descriptor");
492 state->type_descriptor = GTK_LABEL (w);
493
494 w = go_gtk_builder_get_widget (state->gui, "internal-link-grid");
495 expr_entry = gnm_expr_entry_new (state->wbcg, TRUE);
496 gtk_widget_set_hexpand (GTK_WIDGET (expr_entry), TRUE);
497 gtk_container_add (GTK_CONTAINER (w), GTK_WIDGET (expr_entry));
498 gtk_entry_set_activates_default
499 (gnm_expr_entry_get_entry (expr_entry), TRUE);
500 state->internal_link_ee = expr_entry;
501
502 w = go_gtk_builder_get_widget (state->gui, "cancel_button");
503 g_signal_connect (G_OBJECT (w),
504 "clicked",
505 G_CALLBACK (dhl_cb_cancel), state);
506
507 w = go_gtk_builder_get_widget (state->gui, "ok_button");
508 g_signal_connect (G_OBJECT (w),
509 "clicked",
510 G_CALLBACK (dhl_cb_ok), state);
511 gtk_window_set_default (GTK_WINDOW (state->dialog), w);
512
513 gnm_init_help_button (
514 go_gtk_builder_get_widget (state->gui, "help_button"),
515 GNUMERIC_HELP_LINK_HYPERLINK);
516
517 store = gtk_list_store_new (2, GDK_TYPE_PIXBUF, G_TYPE_STRING);
518 w = go_gtk_builder_get_widget (state->gui, "link-type-menu");
519 gtk_combo_box_set_model (GTK_COMBO_BOX (w), GTK_TREE_MODEL (store));
520 g_object_unref (store);
521
522 for (i = 0 ; i < G_N_ELEMENTS (type); i++) {
523 GdkPixbuf *pixbuf = go_gtk_widget_render_icon_pixbuf
524 (GTK_WIDGET (wbcg_toplevel (state->wbcg)),
525 type[i].icon_name, GTK_ICON_SIZE_MENU);
526 gtk_list_store_append (store, &iter);
527 gtk_list_store_set (store, &iter,
528 0, pixbuf,
529 1, _(type[i].label),
530 -1);
531 g_object_unref (pixbuf);
532
533 if (strcmp (G_OBJECT_TYPE_NAME (state->link),
534 type [i].name) == 0)
535 select = i;
536 }
537
538 renderer = gtk_cell_renderer_pixbuf_new ();
539 gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (w),
540 renderer,
541 FALSE);
542 gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (w), renderer,
543 "pixbuf", 0,
544 NULL);
545
546 renderer = gtk_cell_renderer_text_new ();
547 gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (w),
548 renderer,
549 TRUE);
550 gtk_cell_layout_set_attributes (GTK_CELL_LAYOUT (w), renderer,
551 "text", 1,
552 NULL);
553 gtk_combo_box_set_active (GTK_COMBO_BOX (w), select);
554
555 g_signal_connect (G_OBJECT (w), "changed",
556 G_CALLBACK (dhl_cb_menu_changed),
557 state);
558
559 gnm_link_button_and_entry (go_gtk_builder_get_widget (state->gui, "use-this-tip"),
560 go_gtk_builder_get_widget (state->gui, "tip-entry"));
561
562 gnm_dialog_setup_destroy_handlers (GTK_DIALOG (state->dialog),
563 state->wbcg,
564 GNM_DIALOG_DESTROY_CURRENT_SHEET_REMOVED);
565
566 return FALSE;
567 }
568
569 #define DIALOG_KEY "hyperlink-dialog"
570 void
dialog_hyperlink(WBCGtk * wbcg,SheetControl * sc)571 dialog_hyperlink (WBCGtk *wbcg, SheetControl *sc)
572 {
573 GtkBuilder *gui;
574 HyperlinkState* state;
575 GnmHLink *link = NULL;
576 GSList *ptr;
577
578 g_return_if_fail (wbcg != NULL);
579
580 if (gnm_dialog_raise_if_exists (wbcg, DIALOG_KEY))
581 return;
582
583 gui = gnm_gtk_builder_load ("res:ui/hyperlink.ui", NULL, GO_CMD_CONTEXT (wbcg));
584 if (gui == NULL)
585 return;
586
587 state = g_new (HyperlinkState, 1);
588 state->wbcg = wbcg;
589 state->wb = wb_control_get_workbook (GNM_WBC (wbcg));
590 state->sc = sc;
591 state->gui = gui;
592 state->dialog = go_gtk_builder_get_widget (state->gui, "hyperlink-dialog");
593
594 state->use_def_widget = go_gtk_builder_get_widget (state->gui, "use-default-tip");
595
596 state->sheet = sc_sheet (sc);
597 for (ptr = sc_view (sc)->selections; ptr != NULL; ptr = ptr->next) {
598 GnmRange const *r = ptr->data;
599 link = sheet_style_region_contains_link (state->sheet, r);
600 if (link)
601 break;
602 }
603
604 /* We are creating a new link since the existing link */
605 /* may be used in many places. */
606 /* We are duplicating it here rather than in an ok handler in case */
607 /* The link is changed for a differnet cell in a different view. */
608 if (link == NULL) {
609 state->link = gnm_hlink_new (gnm_hlink_url_get_type (), state->sheet);
610 state->is_new = TRUE;
611 } else {
612 state->link = gnm_hlink_new (G_OBJECT_TYPE (link), state->sheet);
613 state->is_new = FALSE;
614 gnm_hlink_set_target (state->link, gnm_hlink_get_target (link));
615 gnm_hlink_set_tip (state->link, gnm_hlink_get_tip (link));
616 }
617
618 if (dhl_init (state)) {
619 go_gtk_notice_dialog (wbcg_toplevel (wbcg), GTK_MESSAGE_ERROR,
620 _("Could not create the hyperlink dialog."));
621 g_free (state);
622 return;
623 }
624
625 dhl_setup_type (state);
626
627 dhl_set_target (state);
628 dhl_set_tip (state);
629
630 /* a candidate for merging into attach guru */
631 gnm_keyed_dialog (state->wbcg, GTK_WINDOW (state->dialog),
632 DIALOG_KEY);
633 go_gtk_nonmodal_dialog (wbcg_toplevel (state->wbcg),
634 GTK_WINDOW (state->dialog));
635
636 wbc_gtk_attach_guru (state->wbcg, state->dialog);
637 g_object_set_data_full (G_OBJECT (state->dialog),
638 "state", state, (GDestroyNotify) dhl_free);
639 gtk_widget_show (state->dialog);
640 }
641