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.
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 */
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. */
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();
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
;
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
++) {
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
);
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
);
144 /* file open dialog, with filters for image types gdk-pixbuf recognizes. */
145 char *gd_select_image_file(const char *title
) {
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
));
166 gtk_widget_destroy(dialog
);
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 */
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
);
199 return GTK_WINDOW(parent
);
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
,
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 */
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 */
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!) */
277 /* add to recent chooser */
278 if (g_path_is_absolute(filename
))
279 uri
=g_filename_to_uri(filename
, NULL
, NULL
);
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
);
288 gtk_recent_manager_add_item(gtk_recent_manager_get_default(), 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
;
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
) {
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
);
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 */
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
);
355 /* if we have a filename, do the save */
357 caveset_save(filename
, caveset
);
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
);
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
) {
389 /* if caveset is edited, and user does not want to discard changes */
390 if (!gd_discard_changes(caveset
))
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 */
404 gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER(dialog
), directory
);
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
);
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
));
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. */
430 gtk_widget_destroy(dialog
);
435 caveset
= create_from_file(filename
);
436 } catch (std::exception
&e
) {
437 gd_errormessage(_("Error loading caveset."), e
.what());
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
);
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
);
471 void gd_show_errors(Logger
&l
, const char *title
, bool always_show
) {
472 if (l
.get_messages().empty() && !always_show
)
474 /* create text buffer */
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
);
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
);
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. */
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);