20130313
[gdash.git] / src / gtk / gtkui.cpp
blob1745c6ba4ed511f75efc338723a0957525d45149
1 /*
2 * Copyright (c) 2007-2013, Czirkos Zoltan http://code.google.com/p/gdash/
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 #include "config.h"
19 #include <gtk/gtk.h>
20 #include <glib/gi18n.h>
22 #include "fileops/loadfile.hpp"
23 #include "gtk/gtkpixbuffactory.hpp"
24 #include "cave/cavestored.hpp"
25 #include "cave/caveset.hpp"
26 #include "misc/logger.hpp"
27 #include "misc/printf.hpp"
28 #include "misc/util.hpp"
29 #include "cave/elementproperties.hpp"
30 #include "settings.hpp"
32 #include "gtk/gtkui.hpp"
33 #include "gtk/gtkapp.hpp"
34 #include "framework/commands.hpp"
36 /* pixbufs of icons and the like */
37 #include "icons.cpp"
38 /* title image and icon */
39 #include "gdash_icon_48.cpp"
41 static char *caveset_filename=NULL;
42 static char *last_folder=NULL;
44 void gd_register_stock_icons() {
45 /* a table of icon data (guint8*, static arrays included from icons.h) and stock id. */
46 static struct {
47 const guint8 *data;
48 const char *stock_id;
49 } icons[]={
50 { cave_editor, GD_ICON_CAVE_EDITOR },
51 { move, GD_ICON_EDITOR_MOVE },
52 { add_join, GD_ICON_EDITOR_JOIN },
53 { add_freehand, GD_ICON_EDITOR_FREEHAND },
54 { add_point, GD_ICON_EDITOR_POINT },
55 { add_line, GD_ICON_EDITOR_LINE },
56 { add_rectangle, GD_ICON_EDITOR_RECTANGLE },
57 { add_filled_rectangle, GD_ICON_EDITOR_FILLRECT },
58 { add_raster, GD_ICON_EDITOR_RASTER },
59 { add_fill_border, GD_ICON_EDITOR_FILL_BORDER },
60 { add_fill_replace, GD_ICON_EDITOR_FILL_REPLACE },
61 { add_maze, GD_ICON_EDITOR_MAZE },
62 { add_maze_uni, GD_ICON_EDITOR_MAZE_UNI },
63 { add_maze_braid, GD_ICON_EDITOR_MAZE_BRAID },
64 { snapshot, GD_ICON_SNAPSHOT },
65 { restart_level, GD_ICON_RESTART_LEVEL },
66 { random_fill, GD_ICON_RANDOM_FILL },
67 { award, GD_ICON_AWARD },
68 { to_top, GD_ICON_TO_TOP },
69 { to_bottom, GD_ICON_TO_BOTTOM },
70 { object_on_all, GD_ICON_OBJECT_ON_ALL },
71 { object_not_on_all, GD_ICON_OBJECT_NOT_ON_ALL },
72 { object_not_on_current, GD_ICON_OBJECT_NOT_ON_CURRENT },
73 { replay, GD_ICON_REPLAY },
74 { keyboard, GD_ICON_KEYBOARD },
75 { image, GD_ICON_IMAGE },
78 GtkIconFactory *factory=gtk_icon_factory_new();
79 for (unsigned i=0; i<G_N_ELEMENTS(icons); ++i) {
80 /* 3rd param: copy pixels = false */
81 GdkPixbuf *pixbuf=gdk_pixbuf_new_from_inline (-1, icons[i].data, FALSE, NULL);
82 GtkIconSet *iconset=gtk_icon_set_new_from_pixbuf(pixbuf);
83 g_object_unref(pixbuf);
84 gtk_icon_factory_add(factory, icons[i].stock_id, iconset);
86 gtk_icon_factory_add_default(factory);
87 g_object_unref(factory);
91 GdkPixbuf *gd_icon() {
92 GTKPixbuf pb(sizeof(gdash_icon_48), gdash_icon_48);
93 g_object_ref(pb.get_gdk_pixbuf());
94 return pb.get_gdk_pixbuf();
98 /* return a list of image gtk_image_filter's. */
99 /* they have floating reference. */
100 /* the list is to be freed by the caller. */
101 static GList *image_load_filters() {
102 GSList *formats=gdk_pixbuf_get_formats();
103 GSList *iter;
104 GtkFileFilter *all_filter;
105 GList *filters=NULL; /* new list of filters */
107 all_filter=gtk_file_filter_new();
108 gtk_file_filter_set_name(all_filter, _("All image files"));
110 /* iterate the list of formats given by gdk. create file filters for each. */
111 for (iter=formats; iter!=NULL; iter=iter->next) {
112 GdkPixbufFormat *frm=(GdkPixbufFormat *)iter->data;
114 if (!gdk_pixbuf_format_is_disabled(frm)) {
115 GtkFileFilter *filter;
116 char **extensions;
117 int i;
119 filter=gtk_file_filter_new();
120 gtk_file_filter_set_name(filter, gdk_pixbuf_format_get_description(frm));
121 extensions=gdk_pixbuf_format_get_extensions(frm);
122 for (i=0; extensions[i]!=NULL; i++) {
123 char *pattern;
125 pattern=g_strdup_printf("*.%s", extensions[i]);
126 gtk_file_filter_add_pattern(filter, pattern);
127 gtk_file_filter_add_pattern(all_filter, pattern);
128 g_free(pattern);
130 g_strfreev(extensions);
132 filters=g_list_append(filters, filter);
135 g_slist_free(formats);
137 /* add "all image files" filter */
138 filters=g_list_prepend(filters, all_filter);
140 return filters;
144 /* file open dialog, with filters for image types gdk-pixbuf recognizes. */
145 char *gd_select_image_file(const char *title) {
146 GtkWidget *dialog;
147 GList *filters;
148 GList *iter;
149 int result;
150 char *filename;
152 dialog=gtk_file_chooser_dialog_new (title, guess_active_toplevel(), GTK_FILE_CHOOSER_ACTION_OPEN, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT, NULL);
153 gtk_dialog_set_default_response(GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
155 /* obtain list of image filters, and add all to the window */
156 filters=image_load_filters();
157 for (iter=filters; iter!=NULL; iter=iter->next)
158 gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), GTK_FILE_FILTER(iter->data));
159 g_list_free(filters);
161 result=gtk_dialog_run(GTK_DIALOG(dialog));
162 if (result==GTK_RESPONSE_ACCEPT)
163 filename=gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
164 else
165 filename=NULL;
166 gtk_widget_destroy(dialog);
168 return filename;
172 /**
173 * Try to guess which window is active.
175 GtkWindow *guess_active_toplevel() {
176 GtkWidget *parent=NULL;
178 /* before doing anything, process updates, as windows may have been opened or closed right at the previous moment */
179 gdk_window_process_all_updates();
181 /* if we find a modal window, it is active. */
182 GList *toplevels=gtk_window_list_toplevels();
183 for (GList *iter=toplevels; iter!=NULL; iter=iter->next)
184 if (gtk_window_get_modal(GTK_WINDOW(iter->data)))
185 parent=(GtkWidget *)iter->data;
187 /* if no modal window found, search for a focused toplevel */
188 if (!parent)
189 for (GList *iter=toplevels; iter!=NULL; iter=iter->next)
190 if (gtk_window_has_toplevel_focus(GTK_WINDOW(iter->data)))
191 parent=(GtkWidget *)iter->data;
193 /* if any of them is focused, just choose the last from the list as a fallback. */
194 if (!parent && toplevels)
195 parent=(GtkWidget *) g_list_last(toplevels)->data;
196 g_list_free(toplevels);
198 if (parent)
199 return GTK_WINDOW(parent);
200 else
201 return NULL;
206 * Show a message dialog, with the specified message type (warning, error, info) and texts.
208 * @param type GtkMessageType - sets icon to show.
209 * @param primary Primary text.
210 * @param secondary Secondary (small) text - may be null.
212 static void show_message(GtkMessageType type, const char *primary, const char *secondary) {
213 GtkWidget *dialog=gtk_message_dialog_new(guess_active_toplevel(),
214 GtkDialogFlags(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT),
215 type, GTK_BUTTONS_OK,
216 "%s", primary);
217 gtk_window_set_title(GTK_WINDOW(dialog), "GDash");
218 /* secondary message exists an is not empty string: */
219 if (secondary && secondary[0]!=0)
220 gtk_message_dialog_format_secondary_markup(GTK_MESSAGE_DIALOG (dialog), "%s", secondary);
221 gtk_dialog_run(GTK_DIALOG(dialog));
222 gtk_widget_destroy(dialog);
226 void gd_warningmessage(const char *primary, const char *secondary) {
227 show_message(GTK_MESSAGE_WARNING, primary, secondary);
231 void gd_errormessage(const char *primary, const char *secondary) {
232 show_message(GTK_MESSAGE_ERROR, primary, secondary);
236 void gd_infomessage(const char *primary, const char *secondary) {
237 show_message(GTK_MESSAGE_INFO, primary, secondary);
242 * If necessary, ask the user if he doesn't want to save changes to cave.
244 * If the caveset has no modification, this function simply returns true.
246 bool gd_discard_changes(CaveSet const& caveset) {
247 /* save highscore on every ocassion when the caveset is to be removed from memory */
248 caveset.save_highscore(gd_user_config_dir);
250 /* caveset is not edited, so pretend user confirmed */
251 if (!caveset.edited)
252 return TRUE;
254 GtkWidget *dialog=gtk_message_dialog_new(guess_active_toplevel(), GtkDialogFlags(0), GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE, _("Caveset \"%s\" is edited or new replays are added. Discard changes?"), caveset.name.c_str());
255 gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG (dialog), _("If you discard the caveset, all changes and new replays will be lost."));
256 gtk_dialog_add_button(GTK_DIALOG (dialog), GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL);
257 gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_CANCEL);
258 /* create a discard button with a trash icon and Discard text */
259 GtkWidget *button=gtk_button_new_with_mnemonic(_("_Discard"));
260 gtk_button_set_image(GTK_BUTTON (button), gtk_image_new_from_stock(GTK_STOCK_DELETE, GTK_ICON_SIZE_BUTTON));
261 gtk_widget_show (button);
262 gtk_dialog_add_action_widget(GTK_DIALOG (dialog), button, GTK_RESPONSE_YES);
264 bool discard=gtk_dialog_run(GTK_DIALOG (dialog))==GTK_RESPONSE_YES;
265 gtk_widget_destroy(dialog);
267 /* return button pressed */
268 return discard;
272 static void caveset_file_operation_successful(const char *filename) {
273 /* save successful, so remember filename */
274 /* first we make a copy, as it is possible that filename==caveset_filename (the pointers!) */
275 char *uri;
277 /* add to recent chooser */
278 if (g_path_is_absolute(filename))
279 uri=g_filename_to_uri(filename, NULL, NULL);
280 else {
281 /* make an absolute filename if needed */
282 char *currentdir=g_get_current_dir();
283 char *absolute=g_build_path(G_DIR_SEPARATOR_S, currentdir, filename, NULL);
284 uri=g_filename_to_uri(absolute, NULL, NULL);
285 g_free(currentdir);
286 g_free(absolute);
288 gtk_recent_manager_add_item(gtk_recent_manager_get_default(), uri);
289 g_free(uri);
291 /* if it is a bd file, remember new filename */
292 if (g_str_has_suffix(filename, ".bd")) {
293 /* first make copy, then free and set pointer. we might be called with filename=caveset_filename */
294 char *stored=g_strdup(filename);
295 g_free(caveset_filename);
296 caveset_filename=stored;
297 } else {
298 g_free(caveset_filename);
299 caveset_filename=NULL;
305 * Save caveset to specified directory, and pop up error message if failed.
307 static void caveset_save(const gchar *filename, CaveSet &caveset) {
308 try {
309 caveset.save_to_file(filename);
310 caveset_file_operation_successful(filename);
311 } catch (std::exception& e) {
312 gd_errormessage(e.what(), filename);
318 * Pops up a "save file as" dialog to the user, to select a file to save the caveset to.
319 * If selected, saves the file.
321 * @param parent Parent window for the dialog.
322 * @param caveset The caveset to save.
324 void gd_save_caveset_as(CaveSet &caveset) {
325 GtkWidget *dialog=gtk_file_chooser_dialog_new (_("Save File As"), guess_active_toplevel(), GTK_FILE_CHOOSER_ACTION_SAVE, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT, NULL);
326 gtk_dialog_set_default_response(GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
327 gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog), TRUE);
329 GtkFileFilter *filter=gtk_file_filter_new();
330 gtk_file_filter_set_name(filter, _("BDCFF cave sets (*.bd)"));
331 gtk_file_filter_add_pattern(filter, "*.bd");
332 gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
334 filter=gtk_file_filter_new();
335 gtk_file_filter_set_name(filter, _("All files (*)"));
336 gtk_file_filter_add_pattern(filter, "*");
337 gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
339 gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), CPrintf("%s.bd") % caveset.name);
341 char *filename=NULL;
342 if (gtk_dialog_run(GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT)
343 filename=gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
345 /* check if .bd extension should be added */
346 if (filename) {
347 /* if it has no .bd extension, add one */
348 if (!g_str_has_suffix(filename, ".bd")) {
349 char *suffixed=g_strdup_printf("%s.bd", filename);
350 g_free(filename);
351 filename=suffixed;
355 /* if we have a filename, do the save */
356 if (filename) {
357 caveset_save(filename, caveset);
359 g_free(filename);
360 gtk_widget_destroy(dialog);
365 * Save the current caveset. If no filename is stored, asks the user for a new filename before proceeding.
367 * @param parent Parent window for dialogs.
368 * @param caveset Caveset to save.
370 void gd_save_caveset(CaveSet &caveset) {
371 if (!caveset_filename)
372 /* if no filename remembered, rather start the save_as function, which asks for one. */
373 gd_save_caveset_as(caveset);
374 else
375 /* if given, save. */
376 caveset_save(caveset_filename, caveset);
381 * Pops up a file selection dialog; and loads the caveset selected.
383 * Before doing anything, asks the user if he wants to save the current caveset.
384 * If it is edited and not saved, this function will do nothing.
386 void gd_open_caveset(const char *directory, CaveSet &caveset) {
387 char *filename=NULL;
389 /* if caveset is edited, and user does not want to discard changes */
390 if (!gd_discard_changes(caveset))
391 return;
393 GtkWidget *dialog=gtk_file_chooser_dialog_new (_("Open File"), guess_active_toplevel(), GTK_FILE_CHOOSER_ACTION_OPEN, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT, NULL);
394 gtk_dialog_set_default_response(GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
396 GtkFileFilter *filter=gtk_file_filter_new();
397 gtk_file_filter_set_name(filter, _("GDash cave sets"));
398 for (int i=0; gd_caveset_extensions[i]!=NULL; i++)
399 gtk_file_filter_add_pattern(filter, gd_caveset_extensions[i]);
400 gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
402 /* if callback shipped with a directory name, show that directory by default */
403 if (directory)
404 gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER(dialog), directory);
405 else
406 if (last_folder)
407 /* if we previously had an open command, the directory was remembered */
408 gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER(dialog), last_folder);
409 else
410 /* otherwise user home */
411 gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER(dialog), g_get_home_dir());
413 int result=gtk_dialog_run(GTK_DIALOG (dialog));
414 if (result==GTK_RESPONSE_ACCEPT) {
415 filename=gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
416 g_free(last_folder);
417 last_folder=gtk_file_chooser_get_current_folder (GTK_FILE_CHOOSER(dialog));
420 /* WINDOWS GTK+ 20080926 HACK */
421 /* gtk bug - sometimes the above widget destroy creates an error message. */
422 /* so we delete the error flag here. */
423 /* MacOS GTK+ hack */
424 /* well, in MacOS, the file open dialog does report this error: */
425 /* "Unable to find default local directory monitor type" - original text */
426 /* "Vorgegebener Überwachungstyp für lokale Ordner konnte nicht gefunden werden" - german text */
427 /* so better to always clear the error flag. */
429 Logger l;
430 gtk_widget_destroy(dialog);
431 l.clear();
434 try {
435 caveset = create_from_file(filename);
436 } catch (std::exception &e) {
437 gd_errormessage(_("Error loading caveset."), e.what());
439 g_free(filename);
444 * Convenience function to create a label with centered text.
445 * @param markup The text to show (in pango markup format)
446 * @return The new GtkLabel.
448 GtkWidget *gd_label_new_centered(const char *markup) {
449 GtkWidget *lab=gtk_label_new(NULL);
450 gtk_misc_set_alignment(GTK_MISC(lab), 0, 0.5);
451 gtk_label_set_markup(GTK_LABEL(lab), markup);
453 return lab;
458 * Convenience function to create a label with centered text.
459 * @param markup The text to show (in pango markup format)
460 * @return The new GtkLabel.
462 GtkWidget *gd_label_new_leftaligned(const char *markup) {
463 GtkWidget *lab=gtk_label_new(NULL);
464 gtk_misc_set_alignment(GTK_MISC(lab), 0, 0.5);
465 gtk_label_set_markup(GTK_LABEL(lab), markup);
467 return lab;
471 void gd_show_errors(Logger &l, const char *title, bool always_show) {
472 if (l.get_messages().empty() && !always_show)
473 return;
474 /* create text buffer */
475 GtkTextIter iter;
476 GdkPixbuf *pixbuf_error, *pixbuf_warning, *pixbuf_info;
478 GtkWidget *dialog=gtk_dialog_new_with_buttons(title, guess_active_toplevel(), GTK_DIALOG_NO_SEPARATOR, GTK_STOCK_CLOSE, GTK_RESPONSE_OK, NULL);
479 gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_OK);
480 gtk_window_set_default_size (GTK_WINDOW(dialog), 512, 384);
481 GtkWidget *sw = gtk_scrolled_window_new (NULL, NULL);
482 gtk_box_pack_start_defaults(GTK_BOX (GTK_DIALOG (dialog)->vbox), sw);
483 gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (sw), GTK_SHADOW_IN);
484 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (sw), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
486 /* get text and show it */
487 GtkTextBuffer *buffer=gtk_text_buffer_new(NULL);
488 GtkWidget *view=gtk_text_view_new_with_buffer (buffer);
489 gtk_container_add(GTK_CONTAINER(sw), view);
490 g_object_unref(buffer);
492 pixbuf_error=gtk_widget_render_icon(view, GTK_STOCK_DIALOG_ERROR, GTK_ICON_SIZE_MENU, NULL);
493 pixbuf_warning=gtk_widget_render_icon(view, GTK_STOCK_DIALOG_WARNING, GTK_ICON_SIZE_MENU, NULL);
494 pixbuf_info=gtk_widget_render_icon(view, GTK_STOCK_DIALOG_INFO, GTK_ICON_SIZE_MENU, NULL);
495 Logger::Container const& messages=l.get_messages();
496 for (Logger::ConstIterator error=messages.begin(); error!=messages.end(); ++error) {
497 gtk_text_buffer_get_iter_at_offset(buffer, &iter, -1);
498 if (error->sev<=ErrorMessage::Message)
499 gtk_text_buffer_insert_pixbuf(buffer, &iter, pixbuf_info);
500 else if (error->sev<=ErrorMessage::Warning)
501 gtk_text_buffer_insert_pixbuf(buffer, &iter, pixbuf_warning);
502 else
503 gtk_text_buffer_insert_pixbuf(buffer, &iter, pixbuf_error);
504 gtk_text_buffer_insert(buffer, &iter, error->message.c_str(), -1);
505 gtk_text_buffer_insert(buffer, &iter, "\n", -1);
507 g_object_unref(pixbuf_error);
508 g_object_unref(pixbuf_warning);
509 g_object_unref(pixbuf_info);
511 /* set some tags */
512 gtk_text_view_set_editable(GTK_TEXT_VIEW(view), FALSE);
513 gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(view), FALSE);
514 gtk_text_view_set_pixels_above_lines(GTK_TEXT_VIEW(view), 3);
515 gtk_text_view_set_left_margin(GTK_TEXT_VIEW(view), 6);
516 gtk_text_view_set_right_margin(GTK_TEXT_VIEW(view), 6);
517 gtk_widget_show_all(dialog);
518 gtk_dialog_run(GTK_DIALOG (dialog));
519 gtk_widget_destroy(dialog);
521 /* shown to the users - forget them. */
522 l.clear();
527 * Creates a small dialog window with the given text and asks the user a yes/no question
528 * @param primary The primary (upper) text to show in the dialog.
529 * @param secondary The secondary (lower) text to show in the dialog. May be NULL.
530 * @return true, if the user answered yes, no otherwise.
532 bool gd_question_yesno(const char *primary, const char *secondary) {
533 GtkWidget *dialog=gtk_message_dialog_new(guess_active_toplevel(), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO, "%s", primary);
534 if (secondary && !g_str_equal(secondary, ""))
535 gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), "%s", secondary);
536 int response=gtk_dialog_run(GTK_DIALOG (dialog));
537 gtk_widget_destroy(dialog);
539 return response==GTK_RESPONSE_YES;
544 * Adds a hint (text) to the lower part of the dialog.
545 * Also adds a little INFO icon.
546 * Turns off the separator of the dialog, as it looks nicer without.
548 void gd_dialog_add_hint(GtkDialog *dialog, const char *hint) {
549 /* turn off separator, as it does not look nice with the hint */
550 gtk_dialog_set_has_separator(dialog, FALSE);
552 GtkWidget *align=gtk_alignment_new(0.5, 0.5, 0, 0);
553 gtk_box_pack_end(GTK_BOX(GTK_DIALOG(dialog)->vbox), align, FALSE, TRUE, 0);
554 GtkWidget *hbox=gtk_hbox_new(FALSE, 6);
555 gtk_container_add(GTK_CONTAINER(align), hbox);
556 GtkWidget *label=gd_label_new_centered(hint);
557 gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
558 gtk_box_pack_start(GTK_BOX(hbox), gtk_image_new_from_stock(GTK_STOCK_DIALOG_INFO, GTK_ICON_SIZE_DIALOG), FALSE, FALSE, 0);
559 gtk_box_pack_start(GTK_BOX(hbox), label, TRUE, TRUE, 0);