1 /* GIMP - The GNU Image Manipulation Program
2 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
3 *
4 * gimpcriticaldialog.c
5 * Copyright (C) 2018 Jehan <jehan@gimp.org>
6 *
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 3 of the License, or
10 * (at your option) any later version.
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, see <https://www.gnu.org/licenses/>.
19 */
20
21 /*
22 * This widget is particular that I want to be able to use it
23 * internally but also from an alternate tool (gimp-debug-tool). It
24 * means that the implementation must stay as generic glib/GTK+ as
25 * possible.
26 */
27
28 #include "config.h"
29
30 #include <string.h>
31
32 #include <gtk/gtk.h>
33 #include <gegl.h>
34
35 #ifdef PLATFORM_OSX
36 #import <Cocoa/Cocoa.h>
37 #endif
38
39 #ifdef G_OS_WIN32
40 #undef DATADIR
41 #include <windows.h>
42 #endif
43
44 #include "gimpcriticaldialog.h"
45
46 #include "gimp-intl.h"
47 #include "gimp-version.h"
48
49
50 #define GIMP_CRITICAL_RESPONSE_CLIPBOARD 1
51 #define GIMP_CRITICAL_RESPONSE_URL 2
52 #define GIMP_CRITICAL_RESPONSE_RESTART 3
53 #define GIMP_CRITICAL_RESPONSE_DOWNLOAD 4
54
55 #define BUTTON1_TEXT _("Copy Bug Information")
56 #define BUTTON2_TEXT _("Open Bug Tracker")
57
58 enum
59 {
60 PROP_0,
61 PROP_LAST_VERSION,
62 PROP_RELEASE_DATE
63 };
64
65 static void gimp_critical_dialog_constructed (GObject *object);
66 static void gimp_critical_dialog_finalize (GObject *object);
67 static void gimp_critical_dialog_set_property (GObject *object,
68 guint property_id,
69 const GValue *value,
70 GParamSpec *pspec);
71 static void gimp_critical_dialog_response (GtkDialog *dialog,
72 gint response_id);
73
74 static void gimp_critical_dialog_copy_info (GimpCriticalDialog *dialog);
75 static gboolean browser_open_url (const gchar *url,
76 GError **error);
77
78
G_DEFINE_TYPE(GimpCriticalDialog,gimp_critical_dialog,GTK_TYPE_DIALOG)79 G_DEFINE_TYPE (GimpCriticalDialog, gimp_critical_dialog, GTK_TYPE_DIALOG)
80
81 #define parent_class gimp_critical_dialog_parent_class
82
83
84 static void
85 gimp_critical_dialog_class_init (GimpCriticalDialogClass *klass)
86 {
87 GObjectClass *object_class = G_OBJECT_CLASS (klass);
88 GtkDialogClass *dialog_class = GTK_DIALOG_CLASS (klass);
89
90 object_class->constructed = gimp_critical_dialog_constructed;
91 object_class->finalize = gimp_critical_dialog_finalize;
92 object_class->set_property = gimp_critical_dialog_set_property;
93
94 dialog_class->response = gimp_critical_dialog_response;
95
96 g_object_class_install_property (object_class, PROP_LAST_VERSION,
97 g_param_spec_string ("last-version",
98 NULL, NULL, NULL,
99 G_PARAM_WRITABLE |
100 G_PARAM_CONSTRUCT_ONLY));
101 g_object_class_install_property (object_class, PROP_RELEASE_DATE,
102 g_param_spec_string ("release-date",
103 NULL, NULL, NULL,
104 G_PARAM_WRITABLE |
105 G_PARAM_CONSTRUCT_ONLY));
106 }
107
108 static void
gimp_critical_dialog_init(GimpCriticalDialog * dialog)109 gimp_critical_dialog_init (GimpCriticalDialog *dialog)
110 {
111 PangoAttrList *attrs;
112 PangoAttribute *attr;
113
114 gtk_window_set_role (GTK_WINDOW (dialog), "gimp-critical");
115
116 gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_CLOSE);
117 gtk_window_set_resizable (GTK_WINDOW (dialog), TRUE);
118
119 dialog->main_vbox = gtk_vbox_new (FALSE, 6);
120 gtk_container_set_border_width (GTK_CONTAINER (dialog->main_vbox), 6);
121 gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
122 dialog->main_vbox, TRUE, TRUE, 0);
123 gtk_widget_show (dialog->main_vbox);
124
125 /* The error label. */
126 dialog->top_label = gtk_label_new (NULL);
127 gtk_misc_set_alignment (GTK_MISC (dialog->top_label), 0.0, 0.5);
128 gtk_label_set_ellipsize (GTK_LABEL (dialog->top_label), PANGO_ELLIPSIZE_END);
129 gtk_label_set_selectable (GTK_LABEL (dialog->top_label), TRUE);
130 gtk_box_pack_start (GTK_BOX (dialog->main_vbox), dialog->top_label,
131 FALSE, FALSE, 0);
132
133 attrs = pango_attr_list_new ();
134 attr = pango_attr_weight_new (PANGO_WEIGHT_SEMIBOLD);
135 pango_attr_list_insert (attrs, attr);
136 gtk_label_set_attributes (GTK_LABEL (dialog->top_label), attrs);
137 pango_attr_list_unref (attrs);
138
139 gtk_widget_show (dialog->top_label);
140
141 dialog->center_label = gtk_label_new (NULL);
142
143 gtk_misc_set_alignment (GTK_MISC (dialog->center_label), 0.0, 0.5);
144 gtk_label_set_selectable (GTK_LABEL (dialog->center_label), TRUE);
145 gtk_box_pack_start (GTK_BOX (dialog->main_vbox), dialog->center_label,
146 FALSE, FALSE, 0);
147 gtk_widget_show (dialog->center_label);
148
149 dialog->bottom_label = gtk_label_new (NULL);
150 gtk_misc_set_alignment (GTK_MISC (dialog->bottom_label), 0.0, 0.5);
151 gtk_box_pack_start (GTK_BOX (dialog->main_vbox), dialog->bottom_label, FALSE, FALSE, 0);
152
153 attrs = pango_attr_list_new ();
154 attr = pango_attr_style_new (PANGO_STYLE_ITALIC);
155 pango_attr_list_insert (attrs, attr);
156 gtk_label_set_attributes (GTK_LABEL (dialog->bottom_label), attrs);
157 pango_attr_list_unref (attrs);
158 gtk_widget_show (dialog->bottom_label);
159
160 dialog->pid = 0;
161 dialog->program = NULL;
162 }
163
164 static void
gimp_critical_dialog_constructed(GObject * object)165 gimp_critical_dialog_constructed (GObject *object)
166 {
167 GimpCriticalDialog *dialog = GIMP_CRITICAL_DIALOG (object);
168 GtkWidget *scrolled;
169 GtkTextBuffer *buffer;
170 gchar *version;
171 gchar *text;
172
173 /* Bug details for developers. */
174 scrolled = gtk_scrolled_window_new (NULL, NULL);
175 gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled),
176 GTK_SHADOW_IN);
177 gtk_widget_set_size_request (scrolled, -1, 200);
178
179 if (dialog->last_version)
180 {
181 GtkWidget *expander;
182 GtkWidget *vbox;
183 GtkWidget *button;
184
185 expander = gtk_expander_new (_("See bug details"));
186 gtk_box_pack_start (GTK_BOX (dialog->main_vbox), expander, TRUE, TRUE, 0);
187 gtk_widget_show (expander);
188
189 vbox = gtk_vbox_new (FALSE, 4);
190 gtk_container_add (GTK_CONTAINER (expander), vbox);
191 gtk_widget_show (vbox);
192
193 gtk_box_pack_start (GTK_BOX (vbox), scrolled, TRUE, TRUE, 0);
194 gtk_widget_show (scrolled);
195
196 button = gtk_button_new_with_label (BUTTON1_TEXT);
197 g_signal_connect_swapped (button, "clicked",
198 G_CALLBACK (gimp_critical_dialog_copy_info),
199 dialog);
200 gtk_box_pack_start (GTK_BOX (vbox), button, FALSE, FALSE, 0);
201 gtk_widget_show (button);
202
203 gtk_dialog_add_buttons (GTK_DIALOG (dialog),
204 _("Go to _Download page"), GIMP_CRITICAL_RESPONSE_DOWNLOAD,
205 _("_Close"), GTK_RESPONSE_CLOSE,
206 NULL);
207
208 /* Recommend an update. */
209 text = g_strdup_printf (_("A new version of GIMP (%s) was released on %s.\n"
210 "It is recommended to update."),
211 dialog->last_version, dialog->release_date);
212 gtk_label_set_text (GTK_LABEL (dialog->center_label), text);
213 g_free (text);
214
215 text = _("You are running an unsupported version!");
216 gtk_label_set_text (GTK_LABEL (dialog->bottom_label), text);
217 }
218 else
219 {
220 /* Pack directly (and well visible) the bug details. */
221 gtk_box_pack_start (GTK_BOX (dialog->main_vbox), scrolled, TRUE, TRUE, 0);
222 gtk_widget_show (scrolled);
223
224 gtk_dialog_add_buttons (GTK_DIALOG (dialog),
225 BUTTON1_TEXT, GIMP_CRITICAL_RESPONSE_CLIPBOARD,
226 BUTTON2_TEXT, GIMP_CRITICAL_RESPONSE_URL,
227 _("_Close"), GTK_RESPONSE_CLOSE,
228 NULL);
229
230 /* Generic "report a bug" instructions. */
231 text = g_strdup_printf ("%s\n"
232 " \xe2\x80\xa2 %s %s\n"
233 " \xe2\x80\xa2 %s %s\n"
234 " \xe2\x80\xa2 %s\n"
235 " \xe2\x80\xa2 %s\n"
236 " \xe2\x80\xa2 %s\n"
237 " \xe2\x80\xa2 %s",
238 _("To help us improve GIMP, you can report the bug with "
239 "these simple steps:"),
240 _("Copy the bug information to the clipboard by clicking: "),
241 BUTTON1_TEXT,
242 _("Open our bug tracker in the browser by clicking: "),
243 BUTTON2_TEXT,
244 _("Create a login if you don't have one yet."),
245 _("Paste the clipboard text in a new bug report."),
246 _("Add relevant information in English in the bug report "
247 "explaining what you were doing when this error occurred."),
248 _("This error may have left GIMP in an inconsistent state. "
249 "It is advised to save your work and restart GIMP."));
250 gtk_label_set_text (GTK_LABEL (dialog->center_label), text);
251 g_free (text);
252
253 text = _("You can also close the dialog directly but "
254 "reporting bugs is the best way to make your "
255 "software awesome.");
256 gtk_label_set_text (GTK_LABEL (dialog->bottom_label), text);
257 }
258
259 buffer = gtk_text_buffer_new (NULL);
260 version = gimp_version (TRUE, FALSE);
261 text = g_strdup_printf ("<!-- %s -->\n\n\n```\n%s\n```",
262 _("Copy-paste this whole debug data to report to developers"),
263 version);
264 gtk_text_buffer_set_text (buffer, text, -1);
265 g_free (version);
266 g_free (text);
267
268 dialog->details = gtk_text_view_new_with_buffer (buffer);
269 g_object_unref (buffer);
270 gtk_text_view_set_editable (GTK_TEXT_VIEW (dialog->details), FALSE);
271 gtk_widget_show (dialog->details);
272 gtk_container_add (GTK_CONTAINER (scrolled), dialog->details);
273 }
274
275 static void
gimp_critical_dialog_finalize(GObject * object)276 gimp_critical_dialog_finalize (GObject *object)
277 {
278 GimpCriticalDialog *dialog = GIMP_CRITICAL_DIALOG (object);
279
280 if (dialog->program)
281 g_free (dialog->program);
282 if (dialog->last_version)
283 g_free (dialog->last_version);
284 if (dialog->release_date)
285 g_free (dialog->release_date);
286
287 G_OBJECT_CLASS (parent_class)->finalize (object);
288 }
289
290 static void
gimp_critical_dialog_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)291 gimp_critical_dialog_set_property (GObject *object,
292 guint property_id,
293 const GValue *value,
294 GParamSpec *pspec)
295 {
296 GimpCriticalDialog *dialog = GIMP_CRITICAL_DIALOG (object);
297
298 switch (property_id)
299 {
300 case PROP_LAST_VERSION:
301 dialog->last_version = g_value_dup_string (value);
302 break;
303 case PROP_RELEASE_DATE:
304 dialog->release_date = g_value_dup_string (value);
305 break;
306
307 default:
308 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
309 break;
310 }
311 }
312
313 static void
gimp_critical_dialog_copy_info(GimpCriticalDialog * dialog)314 gimp_critical_dialog_copy_info (GimpCriticalDialog *dialog)
315 {
316 GtkClipboard *clipboard;
317
318 clipboard = gtk_clipboard_get_for_display (gdk_display_get_default (),
319 GDK_SELECTION_CLIPBOARD);
320 if (clipboard)
321 {
322 GtkTextBuffer *buffer;
323 gchar *text;
324 GtkTextIter start;
325 GtkTextIter end;
326
327 buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (dialog->details));
328 gtk_text_buffer_get_iter_at_offset (buffer, &start, 0);
329 gtk_text_buffer_get_iter_at_offset (buffer, &end, -1);
330 text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
331 gtk_clipboard_set_text (clipboard, text, -1);
332 g_free (text);
333 }
334 }
335
336 /* XXX This is taken straight from plug-ins/common/web-browser.c
337 *
338 * This really sucks but this class also needs to be called by
339 * tools/gimp-debug-tool.c as a separate process and therefore cannot
340 * make use of the PDB. Anyway shouldn't we just move this as a utils
341 * function? Why does such basic feature as opening a URL in a
342 * cross-platform way need to be a plug-in?
343 */
344 static gboolean
browser_open_url(const gchar * url,GError ** error)345 browser_open_url (const gchar *url,
346 GError **error)
347 {
348 #ifdef G_OS_WIN32
349
350 HINSTANCE hinst = ShellExecute (GetDesktopWindow(),
351 "open", url, NULL, NULL, SW_SHOW);
352
353 if ((gint) hinst <= 32)
354 {
355 const gchar *err;
356
357 switch ((gint) hinst)
358 {
359 case 0 :
360 err = _("The operating system is out of memory or resources.");
361 break;
362 case ERROR_FILE_NOT_FOUND :
363 err = _("The specified file was not found.");
364 break;
365 case ERROR_PATH_NOT_FOUND :
366 err = _("The specified path was not found.");
367 break;
368 case ERROR_BAD_FORMAT :
369 err = _("The .exe file is invalid (non-Microsoft Win32 .exe or error in .exe image).");
370 break;
371 case SE_ERR_ACCESSDENIED :
372 err = _("The operating system denied access to the specified file.");
373 break;
374 case SE_ERR_ASSOCINCOMPLETE :
375 err = _("The file name association is incomplete or invalid.");
376 break;
377 case SE_ERR_DDEBUSY :
378 err = _("DDE transaction busy");
379 break;
380 case SE_ERR_DDEFAIL :
381 err = _("The DDE transaction failed.");
382 break;
383 case SE_ERR_DDETIMEOUT :
384 err = _("The DDE transaction timed out.");
385 break;
386 case SE_ERR_DLLNOTFOUND :
387 err = _("The specified DLL was not found.");
388 break;
389 case SE_ERR_NOASSOC :
390 err = _("There is no application associated with the given file name extension.");
391 break;
392 case SE_ERR_OOM :
393 err = _("There was not enough memory to complete the operation.");
394 break;
395 case SE_ERR_SHARE:
396 err = _("A sharing violation occurred.");
397 break;
398 default :
399 err = _("Unknown Microsoft Windows error.");
400 }
401
402 g_set_error (error, 0, 0, _("Failed to open '%s': %s"), url, err);
403
404 return FALSE;
405 }
406
407 return TRUE;
408
409 #elif defined(PLATFORM_OSX)
410
411 NSURL *ns_url;
412 gboolean retval;
413
414 NSAutoreleasePool *arp = [NSAutoreleasePool new];
415 {
416 ns_url = [NSURL URLWithString: [NSString stringWithUTF8String: url]];
417 retval = [[NSWorkspace sharedWorkspace] openURL: ns_url];
418 }
419 [arp release];
420
421 return retval;
422
423 #else
424
425 return gtk_show_uri (gdk_screen_get_default (),
426 url,
427 gtk_get_current_event_time(),
428 error);
429
430 #endif
431 }
432
433 static void
gimp_critical_dialog_response(GtkDialog * dialog,gint response_id)434 gimp_critical_dialog_response (GtkDialog *dialog,
435 gint response_id)
436 {
437 GimpCriticalDialog *critical = GIMP_CRITICAL_DIALOG (dialog);
438 const gchar *url = NULL;
439
440 switch (response_id)
441 {
442 case GIMP_CRITICAL_RESPONSE_CLIPBOARD:
443 gimp_critical_dialog_copy_info (critical);
444 break;
445
446 case GIMP_CRITICAL_RESPONSE_DOWNLOAD:
447 url = "https://www.gimp.org/downloads/";
448 case GIMP_CRITICAL_RESPONSE_URL:
449 if (url == NULL)
450 {
451 gchar *temp = g_ascii_strdown (BUG_REPORT_URL, -1);
452
453 /* Only accept custom web links. */
454 if (g_str_has_prefix (temp, "http://") ||
455 g_str_has_prefix (temp, "https://"))
456 url = BUG_REPORT_URL;
457 else
458 /* XXX Ideally I'd find a way to prefill the bug report
459 * through the URL or with POST data. But I could not find
460 * any. Anyway since we may soon ditch bugzilla to follow
461 * GNOME infrastructure changes, I don't want to waste too
462 * much time digging into it.
463 */
464 url = PACKAGE_BUGREPORT;
465
466 g_free (temp);
467 }
468
469 browser_open_url (url, NULL);
470 break;
471
472 case GIMP_CRITICAL_RESPONSE_RESTART:
473 {
474 gchar *args[2] = { critical->program , NULL };
475
476 #ifndef G_OS_WIN32
477 /* It is unneeded to kill the process on Win32. This was run
478 * as an async call and the main process should already be
479 * dead by now.
480 */
481 if (critical->pid > 0)
482 kill ((pid_t ) critical->pid, SIGINT);
483 #endif
484 if (critical->program)
485 g_spawn_async (NULL, args, NULL, G_SPAWN_DEFAULT,
486 NULL, NULL, NULL, NULL);
487 }
488 /* Fall through. */
489 case GTK_RESPONSE_DELETE_EVENT:
490 case GTK_RESPONSE_CLOSE:
491 default:
492 gtk_widget_destroy (GTK_WIDGET (dialog));
493 break;
494 }
495 }
496
497 /* public functions */
498
499 GtkWidget *
gimp_critical_dialog_new(const gchar * title,const gchar * last_version,gint64 release_timestamp)500 gimp_critical_dialog_new (const gchar *title,
501 const gchar *last_version,
502 gint64 release_timestamp)
503 {
504 GtkWidget *dialog;
505 gchar *date = NULL;
506
507 g_return_val_if_fail (title != NULL, NULL);
508
509 if (release_timestamp > 0)
510 {
511 GDateTime *datetime;
512
513 datetime = g_date_time_new_from_unix_local (release_timestamp);
514 date = g_date_time_format (datetime, "%x");
515 g_date_time_unref (datetime);
516 }
517
518 dialog = g_object_new (GIMP_TYPE_CRITICAL_DIALOG,
519 "title", title,
520 "last-version", last_version,
521 "release-date", date,
522 NULL);
523 g_free (date);
524
525 return dialog;
526 }
527
528 void
gimp_critical_dialog_add(GtkWidget * dialog,const gchar * message,const gchar * trace,gboolean is_fatal,const gchar * program,gint pid)529 gimp_critical_dialog_add (GtkWidget *dialog,
530 const gchar *message,
531 const gchar *trace,
532 gboolean is_fatal,
533 const gchar *program,
534 gint pid)
535 {
536 GimpCriticalDialog *critical;
537 GtkTextBuffer *buffer;
538 GtkTextIter end;
539 gchar *text;
540
541 if (! GIMP_IS_CRITICAL_DIALOG (dialog) || ! message)
542 {
543 /* This is a bit hackish. We usually should use
544 * g_return_if_fail(). But I don't want to end up in a critical
545 * recursing loop if our code had bugs. We would crash GIMP with
546 * a CRITICAL which would otherwise not have necessarily ended up
547 * in a crash.
548 */
549 return;
550 }
551 critical = GIMP_CRITICAL_DIALOG (dialog);
552
553 /* The user text, which should be localized. */
554 if (is_fatal)
555 {
556 text = g_strdup_printf (_("GIMP crashed with a fatal error: %s"),
557 message);
558 }
559 else if (! gtk_label_get_text (GTK_LABEL (critical->top_label)) ||
560 strlen (gtk_label_get_text (GTK_LABEL (critical->top_label))) == 0)
561 {
562 /* First error. Let's just display it. */
563 text = g_strdup_printf (_("GIMP encountered an error: %s"),
564 message);
565 }
566 else
567 {
568 /* Let's not display all errors. They will be in the bug report
569 * part anyway.
570 */
571 text = g_strdup_printf (_("GIMP encountered several critical errors!"));
572 }
573 gtk_label_set_text (GTK_LABEL (critical->top_label),
574 text);
575 g_free (text);
576
577 if (is_fatal && ! critical->last_version)
578 {
579 /* Same text as before except that we don't need the last point
580 * about saving and restarting since anyway we are crashing and
581 * manual saving is not possible anymore (or even advisable since
582 * if it fails, one may corrupt files).
583 */
584 text = g_strdup_printf ("%s\n"
585 " \xe2\x80\xa2 %s \"%s\"\n"
586 " \xe2\x80\xa2 %s \"%s\"\n"
587 " \xe2\x80\xa2 %s\n"
588 " \xe2\x80\xa2 %s\n"
589 " \xe2\x80\xa2 %s",
590 _("To help us improve GIMP, you can report the bug with "
591 "these simple steps:"),
592 _("Copy the bug information to the clipboard by clicking: "),
593 BUTTON1_TEXT,
594 _("Open our bug tracker in the browser by clicking: "),
595 BUTTON2_TEXT,
596 _("Create a login if you don't have one yet."),
597 _("Paste the clipboard text in a new bug report."),
598 _("Add relevant information in English in the bug report "
599 "explaining what you were doing when this error occurred."));
600 gtk_label_set_text (GTK_LABEL (critical->center_label), text);
601 g_free (text);
602 }
603
604 /* The details text is untranslated on purpose. This is the message
605 * meant to go to clipboard for the bug report. It has to be in
606 * English.
607 */
608 buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (critical->details));
609 gtk_text_buffer_get_iter_at_offset (buffer, &end, -1);
610 if (trace)
611 text = g_strdup_printf ("\n> %s\n\nStack trace:\n```\n%s\n```", message, trace);
612 else
613 text = g_strdup_printf ("\n> %s\n", message);
614 gtk_text_buffer_insert (buffer, &end, text, -1);
615 g_free (text);
616
617 /* Finally when encountering a fatal message, propose one more button
618 * to restart GIMP.
619 */
620 if (is_fatal)
621 {
622 gtk_dialog_add_buttons (GTK_DIALOG (dialog),
623 _("_Restart GIMP"), GIMP_CRITICAL_RESPONSE_RESTART,
624 NULL);
625 critical->program = g_strdup (program);
626 critical->pid = pid;
627 }
628 }
629