# HG changeset patch # User Luke Schierer # Date 1181045678 0 # Node ID 82d68fedbd773bce1172a7eb943fa4559f8c5876 # Parent 37469ac119b6838ed15c2235d0c9d6844274c1e8# Parent 58e28ccf08e6ea57ad90df360f8bffe71849fca6 merge of '94a0ea004e3afd9838d6d7c09e8e51aa19b53ac1' and 'f99809b8cf2e8b6fba3e89fc353513a10820f8d5' diff -r 37469ac119b6 -r 82d68fedbd77 COPYRIGHT --- a/COPYRIGHT Tue Jun 05 06:27:48 2007 +0000 +++ b/COPYRIGHT Tue Jun 05 12:14:38 2007 +0000 @@ -146,6 +146,7 @@ Charlie Gordon Ryan C. Gordon Miah Gregory +David Grohmann Christian Hammond Erick Hamness Fred Hampton diff -r 37469ac119b6 -r 82d68fedbd77 ChangeLog --- a/ChangeLog Tue Jun 05 06:27:48 2007 +0000 +++ b/ChangeLog Tue Jun 05 12:14:38 2007 +0000 @@ -9,6 +9,8 @@ (Arjan van de Ven with Intel Corporation) * No longer linkifies screennames containing @ signs in join/part notifications in chats + * With the HTML logger, images in conversations are now saved. + NOTE: Saved images are not yet displayed when loading logs. Pidgin: * Ensure only one copy of Pidgin is running with a given configuration diff -r 37469ac119b6 -r 82d68fedbd77 ChangeLog.API --- a/ChangeLog.API Tue Jun 05 06:27:48 2007 +0000 +++ b/ChangeLog.API Tue Jun 05 12:14:38 2007 +0000 @@ -15,6 +15,10 @@ * purple_core_ensure_single_instance() This is for UIs to use to ensure only one copy is running. * purple_dbus_is_owner() + * purple_image_data_calculate_filename() + * purple_timeout_add_seconds() + Callers should prefer this to purple_timeout_add() for timers + longer than 1 second away. Be aware of the rounding, though. * purple_timeout_add_seconds() Callers should prefer this to purple_timeout_add() for timers longer than 1 second away. Be aware of the rounding, though. diff -r 37469ac119b6 -r 82d68fedbd77 finch/gntaccount.c --- a/finch/gntaccount.c Tue Jun 05 06:27:48 2007 +0000 +++ b/finch/gntaccount.c Tue Jun 05 12:14:38 2007 +0000 @@ -282,7 +282,11 @@ if (dialog->account) { - s = strrchr(username, purple_account_user_split_get_separator(split)); + if(purple_account_user_split_get_reverse(split)) + s = strrchr(username, purple_account_user_split_get_separator(split)); + else + s = strchr(username, purple_account_user_split_get_separator(split)); + if (s != NULL) { *s = '\0'; diff -r 37469ac119b6 -r 82d68fedbd77 libpurple/accountopt.c --- a/libpurple/accountopt.c Tue Jun 05 06:27:48 2007 +0000 +++ b/libpurple/accountopt.c Tue Jun 05 12:14:38 2007 +0000 @@ -308,6 +308,7 @@ split->text = g_strdup(text); split->field_sep = sep; split->default_value = g_strdup(default_value); + split->reverse = TRUE; return split; } @@ -345,3 +346,19 @@ return split->field_sep; } + +gboolean +purple_account_user_split_get_reverse(const PurpleAccountUserSplit *split) +{ + g_return_val_if_fail(split != NULL, FALSE); + + return split->reverse; +} + +void +purple_account_user_split_set_reverse(PurpleAccountUserSplit *split, gboolean reverse) +{ + g_return_if_fail(split != NULL); + + split->reverse = reverse; +} diff -r 37469ac119b6 -r 82d68fedbd77 libpurple/accountopt.h --- a/libpurple/accountopt.h Tue Jun 05 06:27:48 2007 +0000 +++ b/libpurple/accountopt.h Tue Jun 05 12:14:38 2007 +0000 @@ -64,6 +64,9 @@ char *text; /**< The text that will appear to the user. */ char *default_value; /**< The default value. */ char field_sep; /**< The field separator. */ + gboolean reverse; /**< TRUE if the separator should be found + starting a the end of the string, FALSE + otherwise */ } PurpleAccountUserSplit; @@ -353,6 +356,23 @@ */ char purple_account_user_split_get_separator(const PurpleAccountUserSplit *split); +/** + * Returns the 'reverse' value for an account split. + * + * @param split The account username split. + * + * @return The 'reverse' value. + */ +gboolean purple_account_user_split_get_reverse(const PurpleAccountUserSplit *split); + +/** + * Sets the 'reverse' value for an account split. + * + * @param split The account username split. + * @param reverse The 'reverse' value + */ +void purple_account_user_split_set_reverse(PurpleAccountUserSplit *split, gboolean reverse); + /*@}*/ #ifdef __cplusplus diff -r 37469ac119b6 -r 82d68fedbd77 libpurple/buddyicon.c --- a/libpurple/buddyicon.c Tue Jun 05 06:27:48 2007 +0000 +++ b/libpurple/buddyicon.c Tue Jun 05 12:14:38 2007 +0000 @@ -24,7 +24,6 @@ */ #include "internal.h" #include "buddyicon.h" -#include "cipher.h" #include "conversation.h" #include "dbus-maybe.h" #include "debug.h" @@ -93,33 +92,6 @@ } } -static char * -purple_buddy_icon_data_calculate_filename(guchar *icon_data, size_t icon_len) -{ - PurpleCipherContext *context; - gchar digest[41]; - - context = purple_cipher_context_new_by_name("sha1", NULL); - if (context == NULL) - { - purple_debug_error("buddyicon", "Could not find sha1 cipher\n"); - g_return_val_if_reached(NULL); - } - - /* Hash the icon data */ - purple_cipher_context_append(context, icon_data, icon_len); - if (!purple_cipher_context_digest_to_str(context, sizeof(digest), digest, NULL)) - { - purple_debug_error("buddyicon", "Failed to get SHA-1 digest.\n"); - g_return_val_if_reached(NULL); - } - purple_cipher_context_destroy(context); - - /* Return the filename */ - return g_strdup_printf("%s.%s", digest, - purple_util_get_image_extension(icon_data, icon_len)); -} - static void purple_buddy_icon_data_cache(PurpleStoredImage *img) { @@ -238,7 +210,7 @@ if (filename == NULL) { - file = purple_buddy_icon_data_calculate_filename(icon_data, icon_len); + file = purple_util_get_image_filename(icon_data, icon_len); if (file == NULL) { g_free(icon_data); @@ -966,7 +938,7 @@ g_free(path); - new_filename = purple_buddy_icon_data_calculate_filename(icon_data, icon_len); + new_filename = purple_util_get_image_filename(icon_data, icon_len); if (new_filename == NULL) { purple_debug_error("buddyicon", diff -r 37469ac119b6 -r 82d68fedbd77 libpurple/buddyicon.h --- a/libpurple/buddyicon.h Tue Jun 05 06:27:48 2007 +0000 +++ b/libpurple/buddyicon.h Tue Jun 05 12:14:38 2007 +0000 @@ -31,6 +31,7 @@ #include "blist.h" #include "imgstore.h" #include "prpl.h" +#include "util.h" #ifdef __cplusplus extern "C" { diff -r 37469ac119b6 -r 82d68fedbd77 libpurple/log.c --- a/libpurple/log.c Tue Jun 05 06:27:48 2007 +0000 +++ b/libpurple/log.c Tue Jun 05 12:14:38 2007 +0000 @@ -32,6 +32,7 @@ #include "prefs.h" #include "util.h" #include "stringref.h" +#include "imgstore.h" static GSList *loggers = NULL; @@ -690,6 +691,109 @@ return g_strdup(purple_time_format(&tm)); } +/* NOTE: This can return msg (which you may or may not want to g_free()) + * NOTE: or a newly allocated string which you MUST g_free(). */ +static char * +convert_image_tags(const PurpleLog *log, const char *msg) +{ + const char *tmp; + const char *start; + const char *end; + GData *attributes; + GString *newmsg = NULL; + + tmp = msg; + + newmsg = g_string_new(""); + + while (purple_markup_find_tag("img", tmp, &start, &end, &attributes)) { + int imgid = 0; + char *idstr = NULL; + + if (newmsg == NULL) + newmsg = g_string_new(""); + + /* copy any text before the img tag */ + if (tmp < start) + g_string_append_len(newmsg, tmp, start - tmp); + + idstr = g_datalist_get_data(&attributes, "id"); + + imgid = atoi(idstr); + if (imgid != 0) + { + FILE *image_file; + char *dir; + PurpleStoredImage *image; + gconstpointer image_data; + char *new_filename = NULL; + char *path = NULL; + size_t image_byte_count; + + image = purple_imgstore_find_by_id(imgid); + if (image == NULL) + { + /* This should never happen. */ + g_string_free(newmsg, TRUE); + g_return_val_if_reached((char *)msg); + } + + image_data = purple_imgstore_get_data(image); + image_byte_count = purple_imgstore_get_size(image); + dir = purple_log_get_log_dir(log->type, log->name, log->account); + new_filename = purple_util_get_image_filename(image_data, image_byte_count); + + path = g_build_filename(dir, new_filename, NULL); + + /* Only save unique files. */ + if (!g_file_test(path, G_FILE_TEST_EXISTS)) + { + if ((image_file = g_fopen(path, "wb")) != NULL) + { + if (!fwrite(image_data, image_byte_count, 1, image_file)) + { + purple_debug_error("log", "Error writing %s: %s\n", + path, strerror(errno)); + fclose(image_file); + + /* Attempt to not leave half-written files around. */ + unlink(path); + } + else + { + purple_debug_info("log", "Wrote image file: %s\n", path); + fclose(image_file); + } + } + else + { + purple_debug_error("log", "Unable to create file %s: %s\n", + path, strerror(errno)); + } + } + + /* Write the new image tag */ + g_string_append_printf(newmsg, "", new_filename); + g_free(new_filename); + g_free(path); + } + + /* Continue from the end of the tag */ + tmp = end + 1; + } + + if (newmsg == NULL) + { + /* No images were found to change. */ + return (char *)msg; + } + + /* Append any remaining message data */ + g_string_append(newmsg, tmp); + + return g_string_free(newmsg, FALSE); +} + void purple_log_common_writer(PurpleLog *log, const char *ext) { PurpleLogCommonLoggerData *data = log->logger_data; @@ -1191,6 +1295,7 @@ const char *from, time_t time, const char *message) { char *msg_fixed; + char *image_corrected_msg; char *date; char *header; PurplePlugin *plugin = purple_find_prpl(purple_account_get_protocol_id(log->account)); @@ -1231,7 +1336,14 @@ if(!data->file) return 0; - purple_markup_html_to_xhtml(message, &msg_fixed, NULL); + image_corrected_msg = convert_image_tags(log, message); + purple_markup_html_to_xhtml(image_corrected_msg, &msg_fixed, NULL); + + /* Yes, this breaks encapsulation. But it's a static function and + * this saves a needless strdup(). */ + if (image_corrected_msg != message) + g_free(image_corrected_msg); + date = log_get_timestamp(log, time); if(log->type == PURPLE_LOG_SYSTEM){ diff -r 37469ac119b6 -r 82d68fedbd77 libpurple/protocols/jabber/libxmpp.c --- a/libpurple/protocols/jabber/libxmpp.c Tue Jun 05 06:27:48 2007 +0000 +++ b/libpurple/protocols/jabber/libxmpp.c Tue Jun 05 12:14:38 2007 +0000 @@ -195,9 +195,11 @@ /* Translators: 'domain' is used here in the context of Internet domains, e.g. pidgin.im */ split = purple_account_user_split_new(_("Domain"), NULL, '@'); + purple_account_user_split_set_reverse(split, FALSE); prpl_info.user_splits = g_list_append(prpl_info.user_splits, split); split = purple_account_user_split_new(_("Resource"), "Home", '/'); + purple_account_user_split_set_reverse(split, FALSE); prpl_info.user_splits = g_list_append(prpl_info.user_splits, split); option = purple_account_option_bool_new(_("Force old (port 5223) SSL"), "old_ssl", FALSE); diff -r 37469ac119b6 -r 82d68fedbd77 libpurple/util.c --- a/libpurple/util.c Tue Jun 05 06:27:48 2007 +0000 +++ b/libpurple/util.c Tue Jun 05 12:14:38 2007 +0000 @@ -22,6 +22,7 @@ */ #include "internal.h" +#include "cipher.h" #include "conversation.h" #include "core.h" #include "debug.h" @@ -1422,6 +1423,40 @@ plain = g_string_append_c(plain, '\n'); continue; } + if(!g_ascii_strncasecmp(c, "' || *(c+4) == ' ')) { + const char *p = c; + GString *src = NULL; + struct purple_parse_tag *pt; + while(*p && *p != '>') { + if(!g_ascii_strncasecmp(p, "src=", strlen("src="))) { + const char *q = p + strlen("src="); + src = g_string_new(""); + if(*q == '\'' || *q == '\"') + q++; + while(*q && *q != '\"' && *q != '\'' && *q != ' ') { + src = g_string_append_c(src, *q); + q++; + } + p = q; + } + p++; + } + if ((c = strchr(c, '>')) != NULL) + c++; + else + c = p; + pt = g_new0(struct purple_parse_tag, 1); + pt->src_tag = "img"; + pt->dest_tag = "img"; + tags = g_list_prepend(tags, pt); + if(xhtml && src && src->len) + g_string_append_printf(xhtml, "", g_strstrip(src->str)); + else + pt->ignore = TRUE; + if (src) + g_string_free(src, TRUE); + continue; + } if(!g_ascii_strncasecmp(c, "", 3) || !g_ascii_strncasecmp(c, "", strlen(""))) { struct purple_parse_tag *pt = g_new0(struct purple_parse_tag, 1); pt->src_tag = *(c+2) == '>' ? "b" : "bold"; @@ -1563,10 +1598,7 @@ pt->dest_tag = "span"; tags = g_list_prepend(tags, pt); if(style->len) - { - if(xhtml) - g_string_append_printf(xhtml, "", g_strstrip(style->str)); - } + g_string_append_printf(xhtml, "", g_strstrip(style->str)); else pt->ignore = TRUE; g_string_free(style, TRUE); @@ -2692,6 +2724,33 @@ return "icon"; } +char * +purple_util_get_image_filename(gconstpointer image_data, size_t image_len) +{ + PurpleCipherContext *context; + gchar digest[41]; + + context = purple_cipher_context_new_by_name("sha1", NULL); + if (context == NULL) + { + purple_debug_error("util", "Could not find sha1 cipher\n"); + g_return_val_if_reached(NULL); + } + + /* Hash the image data */ + purple_cipher_context_append(context, image_data, image_len); + if (!purple_cipher_context_digest_to_str(context, sizeof(digest), digest, NULL)) + { + purple_debug_error("util", "Failed to get SHA-1 digest.\n"); + g_return_val_if_reached(NULL); + } + purple_cipher_context_destroy(context); + + /* Return the filename */ + return g_strdup_printf("%s.%s", digest, + purple_util_get_image_extension(image_data, image_len)); +} + gboolean purple_program_is_valid(const char *program) { diff -r 37469ac119b6 -r 82d68fedbd77 libpurple/util.h --- a/libpurple/util.h Tue Jun 05 06:27:48 2007 +0000 +++ b/libpurple/util.h Tue Jun 05 12:14:38 2007 +0000 @@ -32,6 +32,7 @@ #include "account.h" #include "xmlnode.h" +#include "notify.h" #ifdef __cplusplus extern "C" { @@ -623,6 +624,12 @@ const char * purple_util_get_image_extension(gconstpointer data, size_t len); +/** + * Returns a SHA-1 hash string of the data passed in with the correct file + * extention appended. + */ +char *purple_util_get_image_filename(gconstpointer image_data, size_t image_len); + /*@}*/ diff -r 37469ac119b6 -r 82d68fedbd77 pidgin/Makefile.mingw --- a/pidgin/Makefile.mingw Tue Jun 05 06:27:48 2007 +0000 +++ b/pidgin/Makefile.mingw Tue Jun 05 12:14:38 2007 +0000 @@ -85,6 +85,7 @@ gtkscrollbook.c \ gtksound.c \ gtksourceiter.c \ + gtksourceundomanager.c \ gtkstatusbox.c \ gtkthemes.c \ gtkutils.c \ diff -r 37469ac119b6 -r 82d68fedbd77 pidgin/gtkaccount.c --- a/pidgin/gtkaccount.c Tue Jun 05 06:27:48 2007 +0000 +++ b/pidgin/gtkaccount.c Tue Jun 05 12:14:38 2007 +0000 @@ -484,7 +484,11 @@ char *c; if (dialog->account != NULL) { - c = strrchr(username, + if(purple_account_user_split_get_reverse(split)) + c = strrchr(username, + purple_account_user_split_get_separator(split)); + else + c = strchr(username, purple_account_user_split_get_separator(split)); if (c != NULL) { @@ -830,7 +834,7 @@ item = gtk_menu_get_active(GTK_MENU(menu)); if (str_value == NULL && g_object_get_data(G_OBJECT(item), "fake") && !strcmp(_("Connect server"), purple_account_option_get_text(option))) - str_value = "talk.google.com"; + str_value = "talk.google.com"; if (str_value != NULL) gtk_entry_set_text(GTK_ENTRY(entry), str_value); diff -r 37469ac119b6 -r 82d68fedbd77 pidgin/gtkimhtml.c --- a/pidgin/gtkimhtml.c Tue Jun 05 06:27:48 2007 +0000 +++ b/pidgin/gtkimhtml.c Tue Jun 05 12:14:38 2007 +0000 @@ -144,6 +144,10 @@ }; static guint signals [LAST_SIGNAL] = { 0 }; +static char *html_clipboard = NULL; +static char *text_clipboard = NULL; +GtkClipboard *clipboard_selection = NULL; + static GtkTargetEntry selection_targets[] = { #ifndef _WIN32 { "text/html", 0, TARGET_HTML }, @@ -875,14 +879,17 @@ static void gtk_imhtml_clipboard_get(GtkClipboard *clipboard, GtkSelectionData *selection_data, guint info, GtkIMHtml *imhtml) { char *text = NULL; - gboolean primary; + gboolean primary = (clipboard != clipboard_selection); GtkTextIter start, end; - GtkTextMark *sel = gtk_text_buffer_get_selection_bound(imhtml->text_buffer); - GtkTextMark *ins = gtk_text_buffer_get_insert(imhtml->text_buffer); - - gtk_text_buffer_get_iter_at_mark(imhtml->text_buffer, &start, sel); - gtk_text_buffer_get_iter_at_mark(imhtml->text_buffer, &end, ins); - primary = gtk_widget_get_clipboard(GTK_WIDGET(imhtml), GDK_SELECTION_PRIMARY) == clipboard; + GtkTextMark *sel = NULL; + GtkTextMark *ins = NULL; + + if (primary) { + ins = gtk_text_buffer_get_insert(imhtml->text_buffer); + sel = gtk_text_buffer_get_selection_bound(imhtml->text_buffer); + gtk_text_buffer_get_iter_at_mark(imhtml->text_buffer, &start, sel); + gtk_text_buffer_get_iter_at_mark(imhtml->text_buffer, &end, ins); + } if (info == TARGET_HTML) { char *selection; @@ -892,7 +899,7 @@ if (primary) { text = gtk_imhtml_get_markup_range(imhtml, &start, &end); } else - text = imhtml->clipboard_html_string; + text = html_clipboard; /* Mozilla asks that we start our text/html with the Unicode byte order mark */ str = g_string_append_unichar(str, 0xfeff); @@ -910,7 +917,7 @@ if (primary) { text = gtk_imhtml_get_text(imhtml, &start, &end); } else - text = imhtml->clipboard_text_string; + text = text_clipboard; gtk_selection_data_set_text(selection_data, text, strlen(text)); } if (primary) /* This was allocated here */ @@ -933,20 +940,32 @@ &insert); } +static void gtk_imhtml_clipboard_clear (GtkClipboard *clipboard, GtkSelectionData *sel_data, + guint info, gpointer user_data_or_owner) +{ + clipboard_selection = NULL; +} + static void copy_clipboard_cb(GtkIMHtml *imhtml, gpointer unused) { GtkTextIter start, end; if (gtk_text_buffer_get_selection_bounds(imhtml->text_buffer, &start, &end)) { - gtk_clipboard_set_with_owner(gtk_widget_get_clipboard(GTK_WIDGET(imhtml), GDK_SELECTION_CLIPBOARD), + if (!clipboard_selection) + clipboard_selection = gtk_widget_get_clipboard(GTK_WIDGET(imhtml), GDK_SELECTION_CLIPBOARD); + gtk_clipboard_set_with_owner(clipboard_selection, selection_targets, sizeof(selection_targets) / sizeof(GtkTargetEntry), (GtkClipboardGetFunc)gtk_imhtml_clipboard_get, - (GtkClipboardClearFunc)NULL, G_OBJECT(imhtml)); + (GtkClipboardClearFunc)gtk_imhtml_clipboard_clear, G_OBJECT(imhtml)); g_free(imhtml->clipboard_html_string); g_free(imhtml->clipboard_text_string); imhtml->clipboard_html_string = gtk_imhtml_get_markup_range(imhtml, &start, &end); imhtml->clipboard_text_string = gtk_imhtml_get_text(imhtml, &start, &end); + + text_clipboard = imhtml->clipboard_text_string; + html_clipboard = imhtml->clipboard_html_string; + } g_signal_stop_emission_by_name(imhtml, "copy-clipboard"); @@ -956,10 +975,12 @@ { GtkTextIter start, end; if (gtk_text_buffer_get_selection_bounds(imhtml->text_buffer, &start, &end)) { - gtk_clipboard_set_with_owner(gtk_widget_get_clipboard(GTK_WIDGET(imhtml), GDK_SELECTION_CLIPBOARD), + if (!clipboard_selection) + clipboard_selection = gtk_widget_get_clipboard(GTK_WIDGET(imhtml), GDK_SELECTION_CLIPBOARD); + gtk_clipboard_set_with_owner(clipboard_selection, selection_targets, sizeof(selection_targets) / sizeof(GtkTargetEntry), (GtkClipboardGetFunc)gtk_imhtml_clipboard_get, - (GtkClipboardClearFunc)NULL, G_OBJECT(imhtml)); + (GtkClipboardClearFunc)gtk_imhtml_clipboard_clear, G_OBJECT(imhtml)); g_free(imhtml->clipboard_html_string); g_free(imhtml->clipboard_text_string); @@ -967,6 +988,9 @@ imhtml->clipboard_html_string = gtk_imhtml_get_markup_range(imhtml, &start, &end); imhtml->clipboard_text_string = gtk_imhtml_get_text(imhtml, &start, &end); + text_clipboard = imhtml->clipboard_text_string; + html_clipboard = imhtml->clipboard_html_string; + if (imhtml->editable) gtk_text_buffer_delete_selection(imhtml->text_buffer, FALSE, FALSE); } @@ -1220,11 +1244,6 @@ g_free(img_data); } - if (imhtml->clipboard_text_string) { - g_free(imhtml->clipboard_text_string); - g_free(imhtml->clipboard_html_string); - } - g_list_free(imhtml->scalables); g_slist_free(imhtml->im_images); g_queue_free(imhtml->animations); @@ -1232,6 +1251,12 @@ g_free(imhtml->search_string); g_object_unref(imhtml->undo_manager); G_OBJECT_CLASS(parent_class)->finalize (object); + if (clipboard_selection) + gtk_clipboard_set_with_owner(clipboard_selection, + selection_targets, sizeof(selection_targets) / sizeof(GtkTargetEntry), + (GtkClipboardGetFunc)gtk_imhtml_clipboard_get, + (GtkClipboardClearFunc)NULL, G_OBJECT(imhtml)); + } /* Boring GTK+ stuff */ @@ -1344,9 +1369,9 @@ gtk_binding_entry_add_signal (binding_set, GDK_r, GDK_CONTROL_MASK, "format_function_clear", 0); gtk_binding_entry_add_signal (binding_set, GDK_KP_Enter, 0, "message_send", 0); gtk_binding_entry_add_signal (binding_set, GDK_Return, 0, "message_send", 0); - gtk_binding_entry_add_signal (binding_set, GDK_z, GDK_CONTROL_MASK, "undo", 0); - gtk_binding_entry_add_signal (binding_set, GDK_z, GDK_CONTROL_MASK | GDK_SHIFT_MASK, "redo", 0); - gtk_binding_entry_add_signal (binding_set, GDK_F14, 0, "undo", 0); + gtk_binding_entry_add_signal (binding_set, GDK_z, GDK_CONTROL_MASK, "undo", 0); + gtk_binding_entry_add_signal (binding_set, GDK_z, GDK_CONTROL_MASK | GDK_SHIFT_MASK, "redo", 0); + gtk_binding_entry_add_signal (binding_set, GDK_F14, 0, "undo", 0); }