/* GIMP - The GNU Image Manipulation Program
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
*
* gimpcriticaldialog.c
* Copyright (C) 2018 Jehan <jehan@gimp.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; either version 3 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, see <https://www.gnu.org/licenses/>.
*/
/*
* This widget is particular that I want to be able to use it
* internally but also from an alternate tool (gimp-debug-tool). It
* means that the implementation must stay as generic glib/GTK+ as
* possible.
*/
#include "config.h"
#include <string.h>
#include <gtk/gtk.h>
#include <gegl.h>
#ifdef PLATFORM_OSX
#import <Cocoa/Cocoa.h>
#endif
#ifdef G_OS_WIN32
#undef DATADIR
#include <dwmapi.h>
#include <gdk/gdkwin32.h>
#include <windows.h>
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
#define DWMWA_USE_IMMERSIVE_DARK_MODE
20
#endif
#endif
#include "gimpcriticaldialog.h"
#include "gimp-intl.h"
#include "gimp-version.h"
#define GIMP_CRITICAL_RESPONSE_CLIPBOARD
1
#define GIMP_CRITICAL_RESPONSE_URL
2
#define GIMP_CRITICAL_RESPONSE_RESTART
3
#define GIMP_CRITICAL_RESPONSE_DOWNLOAD
4
#define BUTTON1_TEXT _(
"Copy Bug Information")
#define BUTTON2_TEXT _(
"Open Bug Tracker")
enum
{
PROP_0,
PROP_LAST_VERSION,
PROP_RELEASE_DATE
};
static void gimp_critical_dialog_constructed (GObject *object);
static void gimp_critical_dialog_finalize (GObject *object);
static void gimp_critical_dialog_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec);
static void gimp_critical_dialog_response (GtkDialog *dialog,
gint response_id);
static void gimp_critical_dialog_copy_info (GimpCriticalDialog *dialog);
static gboolean browser_open_url (GtkWindow *window,
const gchar *url,
GError **error);
#ifdef G_OS_WIN32
static void gimp_critical_dialog_realize (GtkWidget *widget,
GimpCriticalDialog *dialog);
#endif
G_DEFINE_TYPE (GimpCriticalDialog, gimp_critical_dialog, GTK_TYPE_DIALOG)
#define parent_class gimp_critical_dialog_parent_class
static void
gimp_critical_dialog_class_init (GimpCriticalDialogClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkDialogClass *dialog_class = GTK_DIALOG_CLASS (klass);
object_class->constructed = gimp_critical_dialog_constructed;
object_class->finalize = gimp_critical_dialog_finalize;
object_class->set_property = gimp_critical_dialog_set_property;
dialog_class->response = gimp_critical_dialog_response;
g_object_class_install_property (object_class, PROP_LAST_VERSION,
g_param_spec_string (
"last-version",
NULL, NULL, NULL,
G_PARAM_WRITABLE |
G_PARAM_CONSTRUCT_ONLY));
g_object_class_install_property (object_class, PROP_RELEASE_DATE,
g_param_spec_string (
"release-date",
NULL, NULL, NULL,
G_PARAM_WRITABLE |
G_PARAM_CONSTRUCT_ONLY));
}
static void
gimp_critical_dialog_init (GimpCriticalDialog *dialog)
{
GtkWidget *container;
PangoAttrList *attrs;
PangoAttribute *attr;
gtk_window_set_role (GTK_WINDOW (dialog),
"gimp-critical");
gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_CLOSE);
gtk_window_set_resizable (GTK_WINDOW (dialog),
TRUE);
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_CENTER);
gtk_window_set_titlebar (GTK_WINDOW (dialog), NULL);
dialog->main_vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL,
6);
container = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
gtk_container_set_border_width (GTK_CONTAINER (container),
12);
gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))),
dialog->main_vbox,
TRUE,
TRUE,
0);
gtk_widget_show (dialog->main_vbox);
/* The error label. */
dialog->top_label = gtk_label_new (NULL);
gtk_widget_set_halign (dialog->top_label, GTK_ALIGN_START);
gtk_label_set_ellipsize (GTK_LABEL (dialog->top_label), PANGO_ELLIPSIZE_END);
gtk_label_set_selectable (GTK_LABEL (dialog->top_label),
TRUE);
gtk_box_pack_start (GTK_BOX (dialog->main_vbox), dialog->top_label,
FALSE,
FALSE,
0);
attrs = pango_attr_list_new ();
attr = pango_attr_weight_new (PANGO_WEIGHT_SEMIBOLD);
pango_attr_list_insert (attrs, attr);
gtk_label_set_attributes (GTK_LABEL (dialog->top_label), attrs);
pango_attr_list_unref (attrs);
gtk_widget_show (dialog->top_label);
dialog->center_label = gtk_label_new (NULL);
gtk_widget_set_halign (dialog->center_label, GTK_ALIGN_START);
gtk_label_set_selectable (GTK_LABEL (dialog->center_label),
TRUE);
gtk_box_pack_start (GTK_BOX (dialog->main_vbox), dialog->center_label,
FALSE,
FALSE,
0);
gtk_widget_show (dialog->center_label);
dialog->bottom_label = gtk_label_new (NULL);
gtk_widget_set_halign (dialog->bottom_label, GTK_ALIGN_START);
gtk_box_pack_start (GTK_BOX (dialog->main_vbox), dialog->bottom_label,
FALSE,
FALSE,
0);
attrs = pango_attr_list_new ();
attr = pango_attr_style_new (PANGO_STYLE_ITALIC);
pango_attr_list_insert (attrs, attr);
gtk_label_set_attributes (GTK_LABEL (dialog->bottom_label), attrs);
pango_attr_list_unref (attrs);
gtk_widget_show (dialog->bottom_label);
dialog->pid =
0;
dialog->program = NULL;
}
static void
gimp_critical_dialog_constructed (GObject *object)
{
GimpCriticalDialog *dialog = GIMP_CRITICAL_DIALOG (object);
GtkWidget *scrolled;
GtkTextBuffer *buffer;
gchar *version;
gchar *text;
/* Bug details for developers. */
scrolled = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled),
GTK_SHADOW_IN);
gtk_widget_set_size_request (scrolled, -
1,
200);
if (dialog->last_version)
{
GtkWidget *expander;
GtkWidget *vbox;
GtkWidget *button;
expander = gtk_expander_new (_(
"See bug details"));
gtk_box_pack_start (GTK_BOX (dialog->main_vbox), expander,
TRUE,
TRUE,
0);
gtk_widget_show (expander);
vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL,
4);
gtk_container_add (GTK_CONTAINER (expander), vbox);
gtk_widget_show (vbox);
gtk_box_pack_start (GTK_BOX (vbox), scrolled,
TRUE,
TRUE,
0);
gtk_widget_show (scrolled);
button = gtk_button_new_with_label (BUTTON1_TEXT);
g_signal_connect_swapped (button,
"clicked",
G_CALLBACK (gimp_critical_dialog_copy_info),
dialog);
gtk_box_pack_start (GTK_BOX (vbox), button,
FALSE,
FALSE,
6);
gtk_widget_show (button);
gtk_dialog_add_buttons (GTK_DIALOG (dialog),
_(
"Go to _Download page"), GIMP_CRITICAL_RESPONSE_DOWNLOAD,
_(
"_Close"), GTK_RESPONSE_CLOSE,
NULL);
/* Recommend an update. */
text = g_strdup_printf (_(
"A new version of GIMP (%s) was released on %s.\n"
"It is recommended to update."),
dialog->last_version, dialog->release_date);
gtk_label_set_text (GTK_LABEL (dialog->center_label), text);
g_free (text);
text = _(
"You are running an unsupported version!");
gtk_label_set_text (GTK_LABEL (dialog->bottom_label), text);
}
else
{
/* Pack directly (and well visible) the bug details. */
gtk_box_pack_start (GTK_BOX (dialog->main_vbox), scrolled,
TRUE,
TRUE,
6);
gtk_widget_show (scrolled);
gtk_dialog_add_buttons (GTK_DIALOG (dialog),
BUTTON1_TEXT, GIMP_CRITICAL_RESPONSE_CLIPBOARD,
BUTTON2_TEXT, GIMP_CRITICAL_RESPONSE_URL,
_(
"_Close"), GTK_RESPONSE_CLOSE,
NULL);
/* Generic "report a bug" instructions. */
text = g_strdup_printf (
"%s\n"
" \xe2\x80\xa2 %s %s\n"
" \xe2\x80\xa2 %s %s\n"
" \xe2\x80\xa2 %s\n"
" \xe2\x80\xa2 %s\n"
" \xe2\x80\xa2 %s\n"
" \xe2\x80\xa2 %s",
_(
"To help us improve GIMP, you can report the bug with "
"these simple steps:"),
_(
"Copy the bug information to the clipboard by clicking: "),
BUTTON1_TEXT,
_(
"Open our bug tracker in the browser by clicking: "),
BUTTON2_TEXT,
_(
"Create a login if you don't have one yet."),
_(
"Paste the clipboard text in a new bug report."),
_(
"Add relevant information in English in the bug report "
"explaining what you were doing when this error occurred."),
_(
"This error may have left GIMP in an inconsistent state. "
"It is advised to save your work and restart GIMP."));
gtk_label_set_text (GTK_LABEL (dialog->center_label), text);
g_free (text);
text = _(
"You can also close the dialog directly but "
"reporting bugs is the best way to make your "
"software awesome.");
gtk_label_set_text (GTK_LABEL (dialog->bottom_label), text);
}
buffer = gtk_text_buffer_new (NULL);
version = gimp_version (
TRUE,
FALSE);
text = g_strdup_printf (
"<!-- %s -->\n\n\n```\n%s\n```",
_(
"Copy-paste this whole debug data to report to developers"),
version);
gtk_text_buffer_set_text (buffer, text, -
1);
g_free (version);
g_free (text);
dialog->details = gtk_text_view_new_with_buffer (buffer);
g_object_unref (buffer);
gtk_text_view_set_editable (GTK_TEXT_VIEW (dialog->details),
FALSE);
gtk_widget_show (dialog->details);
gtk_container_add (GTK_CONTAINER (scrolled), dialog->details);
}
/* Copied from app/widgets/gimpwidgets-utils.c, to reduce dependency
* on internal GIMP procedures */
#ifdef G_OS_WIN32
static void
gimp_critical_dialog_realize (GtkWidget *widget,
GimpCriticalDialog *dialog)
{
HWND hwnd;
GdkWindow *window = NULL;
GtkStyleContext *style;
GdkRGBA *color = NULL;
gboolean use_dark_mode =
FALSE;
window = gtk_widget_get_window (GTK_WIDGET (widget));
if (window)
{
/* Workaround if we don't have access to GimpGuiConfig.
* If the background color is below the threshold, then we're
* likely in dark mode.
*/
style = gtk_widget_get_style_context (widget);
gtk_style_context_get (style, gtk_style_context_get_state (style),
GTK_STYLE_PROPERTY_BACKGROUND_COLOR, &color,
NULL);
if (color)
{
if (color->red <
0.
5 && color->green <
0.
5 && color->blue <
0.
5)
use_dark_mode =
TRUE;
gdk_rgba_free (color);
}
hwnd = (HWND) gdk_win32_window_get_handle (window);
DwmSetWindowAttribute (hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE,
&use_dark_mode,
sizeof (use_dark_mode));
}
}
#endif
static void
gimp_critical_dialog_finalize (GObject *object)
{
GimpCriticalDialog *dialog = GIMP_CRITICAL_DIALOG (object);
if (dialog->program)
g_free (dialog->program);
if (dialog->last_version)
g_free (dialog->last_version);
if (dialog->release_date)
g_free (dialog->release_date);
G_OBJECT_CLASS (parent_class)->finalize (object);
}
static void
gimp_critical_dialog_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
GimpCriticalDialog *dialog = GIMP_CRITICAL_DIALOG (object);
switch (property_id)
{
case PROP_LAST_VERSION:
dialog->last_version = g_value_dup_string (value);
break;
case PROP_RELEASE_DATE:
dialog->release_date = g_value_dup_string (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
gimp_critical_dialog_copy_info (GimpCriticalDialog *dialog)
{
GtkClipboard *clipboard;
clipboard = gtk_clipboard_get_for_display (gdk_display_get_default (),
GDK_SELECTION_CLIPBOARD);
if (clipboard)
{
GtkTextBuffer *buffer;
gchar *text;
GtkTextIter start;
GtkTextIter end;
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (dialog->details));
gtk_text_buffer_get_iter_at_offset (buffer, &start,
0);
gtk_text_buffer_get_iter_at_offset (buffer, &end, -
1);
text = gtk_text_buffer_get_text (buffer, &start, &end,
FALSE);
gtk_clipboard_set_text (clipboard, text, -
1);
g_free (text);
}
}
/* XXX This is taken straight from plug-ins/common/web-browser.c
*
* This really sucks but this class also needs to be called by
* tools/gimp-debug-tool.c as a separate process and therefore cannot
* make use of the PDB. Anyway shouldn't we just move this as a utils
* function? Why does such basic feature as opening a URL in a
* cross-platform way need to be a plug-in?
*/
static gboolean
browser_open_url (GtkWindow *window,
const gchar *url,
GError **error)
{
#ifdef G_OS_WIN32
HINSTANCE hinst = ShellExecute (GetDesktopWindow(),
"open", url, NULL, NULL, SW_SHOW);
if ((intptr_t) hinst <=
32)
{
const gchar *err;
switch ((intptr_t) hinst)
{
case 0 :
err = _(
"The operating system is out of memory or resources.");
break;
case ERROR_FILE_NOT_FOUND :
err = _(
"The specified file was not found.");
break;
case ERROR_PATH_NOT_FOUND :
err = _(
"The specified path was not found.");
break;
case ERROR_BAD_FORMAT :
err = _(
"The .exe file is invalid (non-Microsoft Win32 .exe or error in .exe image).");
break;
case SE_ERR_ACCESSDENIED :
err = _(
"The operating system denied access to the specified file.");
break;
case SE_ERR_ASSOCINCOMPLETE :
err = _(
"The file name association is incomplete or invalid.");
break;
case SE_ERR_DDEBUSY :
err = _(
"DDE transaction busy");
break;
case SE_ERR_DDEFAIL :
err = _(
"The DDE transaction failed.");
break;
case SE_ERR_DDETIMEOUT :
err = _(
"The DDE transaction timed out.");
break;
case SE_ERR_DLLNOTFOUND :
err = _(
"The specified DLL was not found.");
break;
case SE_ERR_NOASSOC :
err = _(
"There is no application associated with the given file name extension.");
break;
case SE_ERR_OOM :
err = _(
"There was not enough memory to complete the operation.");
break;
case SE_ERR_SHARE:
err = _(
"A sharing violation occurred.");
break;
default :
err = _(
"Unknown Microsoft Windows error.");
}
g_set_error (error,
0,
0, _(
"Failed to open '%s': %s"), url, err);
return FALSE;
}
return TRUE;
#elif defined(PLATFORM_OSX)
NSURL *ns_url;
gboolean retval;
@autoreleasepool
{
ns_url = [NSURL URLWithString: [NSString stringWithUTF8String: url]];
retval = [[NSWorkspace sharedWorkspace] openURL: ns_url];
}
return retval;
#else
return gtk_show_uri_on_window (window,
url,
GDK_CURRENT_TIME,
error);
#endif
}
static void
gimp_critical_dialog_response (GtkDialog *dialog,
gint response_id)
{
GimpCriticalDialog *critical = GIMP_CRITICAL_DIALOG (dialog);
const gchar *url = NULL;
switch (response_id)
{
case GIMP_CRITICAL_RESPONSE_CLIPBOARD:
gimp_critical_dialog_copy_info (critical);
break;
case GIMP_CRITICAL_RESPONSE_DOWNLOAD:
#ifdef GIMP_UNSTABLE
url =
"https://www.gimp.org/downloads/devel/";
#else
url =
"https://www.gimp.org/downloads/";
#endif
case GIMP_CRITICAL_RESPONSE_URL:
if (url == NULL)
{
gchar *temp = g_ascii_strdown (BUG_REPORT_URL, -
1);
/* Only accept custom web links. */
if (g_str_has_prefix (temp,
"http://") ||
g_str_has_prefix (temp,
"https://"))
url = BUG_REPORT_URL;
else
/* XXX Ideally I'd find a way to prefill the bug report
* through the URL or with POST data. But I could not find
* any. Anyway since we may soon ditch bugzilla to follow
* GNOME infrastructure changes, I don't want to waste too
* much time digging into it.
*/
url = PACKAGE_BUGREPORT;
g_free (temp);
}
browser_open_url (GTK_WINDOW (dialog), url, NULL);
break;
case GIMP_CRITICAL_RESPONSE_RESTART:
{
gchar *args[
2] = { critical->program , NULL };
#ifndef G_OS_WIN32
/* It is unneeded to kill the process on Win32. This was run
* as an async call and the main process should already be
* dead by now.
*/
if (critical->pid >
0)
kill ((pid_t ) critical->pid, SIGINT);
#endif
if (critical->program)
g_spawn_async (NULL, args, NULL, G_SPAWN_DEFAULT,
NULL, NULL, NULL, NULL);
}
/* Fall through. */
case GTK_RESPONSE_DELETE_EVENT:
case GTK_RESPONSE_CLOSE:
default:
gtk_widget_destroy (GTK_WIDGET (dialog));
break;
}
}
/* public functions */
GtkWidget *
gimp_critical_dialog_new (
const gchar *title,
const gchar *last_version,
gint64 release_timestamp)
{
GtkWidget *dialog;
gchar *date = NULL;
g_return_val_if_fail (title != NULL, NULL);
if (release_timestamp >
0)
{
GDateTime *datetime;
datetime = g_date_time_new_from_unix_local (release_timestamp);
date = g_date_time_format (datetime,
"%x");
g_date_time_unref (datetime);
}
dialog = g_object_new (GIMP_TYPE_CRITICAL_DIALOG,
"title", title,
"last-version", last_version,
"release-date", date,
NULL);
g_free (date);
#ifdef G_OS_WIN32
g_signal_connect_object (dialog,
"realize",
G_CALLBACK (gimp_critical_dialog_realize),
NULL,
0);
#endif
return dialog;
}
void
gimp_critical_dialog_add (GtkWidget *dialog,
const gchar *message,
const gchar *trace,
gboolean is_fatal,
const gchar *program,
gint pid)
{
GimpCriticalDialog *critical;
GtkTextBuffer *buffer;
GtkTextIter end;
gchar *text;
if (! GIMP_IS_CRITICAL_DIALOG (dialog) || ! message)
{
/* This is a bit hackish. We usually should use
* g_return_if_fail(). But I don't want to end up in a critical
* recursing loop if our code had bugs. We would crash GIMP with
* a CRITICAL which would otherwise not have necessarily ended up
* in a crash.
*/
return;
}
critical = GIMP_CRITICAL_DIALOG (dialog);
/* The user text, which should be localized. */
if (is_fatal)
{
text = g_strdup_printf (_(
"GIMP crashed with a fatal error: %s"),
message);
}
else if (! gtk_label_get_text (GTK_LABEL (critical->top_label)) ||
strlen (gtk_label_get_text (GTK_LABEL (critical->top_label))) ==
0)
{
/* First error. Let's just display it. */
text = g_strdup_printf (_(
"GIMP encountered an error: %s"),
message);
}
else
{
/* Let's not display all errors. They will be in the bug report
* part anyway.
*/
text = g_strdup_printf (_(
"GIMP encountered several critical errors!"));
}
gtk_label_set_text (GTK_LABEL (critical->top_label),
text);
g_free (text);
if (is_fatal && ! critical->last_version)
{
/* Same text as before except that we don't need the last point
* about saving and restarting since anyway we are crashing and
* manual saving is not possible anymore (or even advisable since
* if it fails, one may corrupt files).
*/
text = g_strdup_printf (
"%s\n"
" \xe2\x80\xa2 %s \"%s\
"\n"
" \xe2\x80\xa2 %s \"%s\
"\n"
" \xe2\x80\xa2 %s\n"
" \xe2\x80\xa2 %s\n"
" \xe2\x80\xa2 %s",
_(
"To help us improve GIMP, you can report the bug with "
"these simple steps:"),
_(
"Copy the bug information to the clipboard by clicking: "),
BUTTON1_TEXT,
_(
"Open our bug tracker in the browser by clicking: "),
BUTTON2_TEXT,
_(
"Create a login if you don't have one yet."),
_(
"Paste the clipboard text in a new bug report."),
_(
"Add relevant information in English in the bug report "
"explaining what you were doing when this error occurred."));
gtk_label_set_text (GTK_LABEL (critical->center_label), text);
g_free (text);
}
/* The details text is untranslated on purpose. This is the message
* meant to go to clipboard for the bug report. It has to be in
* English.
*/
buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (critical->details));
gtk_text_buffer_get_iter_at_offset (buffer, &end, -
1);
if (trace)
text = g_strdup_printf (
"\n> %s\n\nStack trace:\n```\n%s\n```", message, trace);
else
text = g_strdup_printf (
"\n> %s\n", message);
gtk_text_buffer_insert (buffer, &end, text, -
1);
g_free (text);
/* Finally when encountering a fatal message, propose one more button
* to restart GIMP.
*/
if (is_fatal)
{
gtk_dialog_add_buttons (GTK_DIALOG (dialog),
_(
"_Restart GIMP"), GIMP_CRITICAL_RESPONSE_RESTART,
NULL);
critical->program = g_strdup (program);
critical->pid = pid;
}
}