From b94bda2d8e82fe856be0dc59d6c50bfb8581bd69 Mon Sep 17 00:00:00 2001 From: LI Daobing Date: Thu, 23 May 2024 19:26:26 -0700 Subject: [PATCH 01/13] add chat_history_widget --- src/iptux/DialogBase.cpp | 34 ++++++++++++++-------------------- src/iptux/DialogBase.h | 1 + 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/iptux/DialogBase.cpp b/src/iptux/DialogBase.cpp index d0175cab..5b639158 100644 --- a/src/iptux/DialogBase.cpp +++ b/src/iptux/DialogBase.cpp @@ -72,14 +72,12 @@ void DialogBase::ClearSublayerGeneral() { * 清空聊天历史记录. */ void DialogBase::ClearHistoryTextView() { - GtkWidget* widget; GtkTextBuffer* buffer; GtkTextTagTable* table; GtkTextIter start, end; GSList *taglist, *tlist; - widget = GTK_WIDGET(g_datalist_get_data(&widset, "history-textview-widget")); - buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); + buffer = gtk_text_view_get_buffer(chat_history_widget); table = gtk_text_buffer_get_tag_table(buffer); /* 清除用于局部标记的GtkTextTag */ @@ -105,17 +103,14 @@ void DialogBase::ClearHistoryTextView() { * 滚动聊天历史记录区. */ void DialogBase::ScrollHistoryTextview() { - GtkWidget* widget; GtkTextBuffer* buffer; GtkTextIter end; GtkTextMark* mark; - widget = GTK_WIDGET(g_datalist_get_data(&widset, "history-textview-widget")); - buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget)); + buffer = gtk_text_view_get_buffer(chat_history_widget); gtk_text_buffer_get_end_iter(buffer, &end); mark = gtk_text_buffer_create_mark(buffer, NULL, &end, FALSE); - gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(widget), mark, 0.0, TRUE, 0.0, - 0.0); + gtk_text_view_scroll_to_mark(chat_history_widget, mark, 0.0, TRUE, 0.0, 0.0); gtk_text_buffer_delete_mark(buffer, mark); } @@ -287,7 +282,6 @@ GtkWidget* DialogBase::CreateInputArea() { */ GtkWidget* DialogBase::CreateHistoryArea() { GtkWidget *frame, *sw; - GtkWidget* widget; frame = gtk_frame_new(_("Chat History")); gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_ETCHED_IN); @@ -298,20 +292,20 @@ GtkWidget* DialogBase::CreateHistoryArea() { GTK_SHADOW_ETCHED_IN); gtk_container_add(GTK_CONTAINER(frame), sw); - widget = gtk_text_view_new_with_buffer(grpinf->buffer); - gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(widget), FALSE); - gtk_text_view_set_editable(GTK_TEXT_VIEW(widget), FALSE); - gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(widget), GTK_WRAP_WORD); - gtk_container_add(GTK_CONTAINER(sw), widget); - g_signal_connect(widget, "key-press-event", + chat_history_widget = + GTK_TEXT_VIEW(gtk_text_view_new_with_buffer(grpinf->buffer)); + gtk_text_view_set_cursor_visible(chat_history_widget, FALSE); + gtk_text_view_set_editable(chat_history_widget, FALSE); + gtk_text_view_set_wrap_mode(chat_history_widget, GTK_WRAP_WORD); + gtk_container_add(GTK_CONTAINER(sw), GTK_WIDGET(chat_history_widget)); + g_signal_connect(chat_history_widget, "key-press-event", G_CALLBACK(textview_key_press_event), NULL); - g_signal_connect(widget, "event-after", G_CALLBACK(textview_event_after), - NULL); - g_signal_connect(widget, "motion-notify-event", + g_signal_connect(chat_history_widget, "event-after", + G_CALLBACK(textview_event_after), NULL); + g_signal_connect(chat_history_widget, "motion-notify-event", G_CALLBACK(textview_motion_notify_event), NULL); - g_signal_connect(widget, "visibility-notify-event", + g_signal_connect(chat_history_widget, "visibility-notify-event", G_CALLBACK(textview_visibility_notify_event), NULL); - g_datalist_set_data(&widset, "history-textview-widget", widget); /* 滚动消息到最末位置 */ ScrollHistoryTextview(); diff --git a/src/iptux/DialogBase.h b/src/iptux/DialogBase.h index ddfc7c4a..2f7903de 100644 --- a/src/iptux/DialogBase.h +++ b/src/iptux/DialogBase.h @@ -87,6 +87,7 @@ class DialogBase : public SessionAbstract, public sigc::trackable { Application* app; std::shared_ptr progdt; + GtkTextView* chat_history_widget = 0; GtkTreeView* fileSendTree = 0; GtkTextView* inputTextviewWidget = 0; From 371acec99f1d7d42f8b36a54e3a68ae02e73c84a Mon Sep 17 00:00:00 2001 From: LI Daobing Date: Thu, 23 May 2024 20:17:49 -0700 Subject: [PATCH 02/13] use anchor to add image to gtktextview --- src/iptux/DialogBase.cpp | 60 ++++++++++++++++++++++++++++++++++++++++ src/iptux/DialogBase.h | 7 +++++ src/iptux/UiModels.cpp | 26 ++++++----------- 3 files changed, 76 insertions(+), 17 deletions(-) diff --git a/src/iptux/DialogBase.cpp b/src/iptux/DialogBase.cpp index 5b639158..b419d588 100644 --- a/src/iptux/DialogBase.cpp +++ b/src/iptux/DialogBase.cpp @@ -306,6 +306,12 @@ GtkWidget* DialogBase::CreateHistoryArea() { G_CALLBACK(textview_motion_notify_event), NULL); g_signal_connect(chat_history_widget, "visibility-notify-event", G_CALLBACK(textview_visibility_notify_event), NULL); + g_signal_connect_swapped(chat_history_widget, "button-press-event", + G_CALLBACK(DialogBase::OnChatHistoryButtonPress), + this); + g_signal_connect_swapped( + grpinf->buffer, "insert-child-anchor", + G_CALLBACK(DialogBase::OnChatHistoryInsertChildAnchor), this); /* 滚动消息到最末位置 */ ScrollHistoryTextview(); @@ -813,4 +819,58 @@ void DialogBase::OnPasteClipboard(DialogBase* self, GtkTextView* textview) { } } +gboolean DialogBase::OnChatHistoryButtonPress(DialogBase*, + GdkEventButton event, + GtkTextView* textview) { + if (event.type != GDK_BUTTON_PRESS || event.button != 3) { + return FALSE; + } + + GtkTextIter iter; + GtkTextChildAnchor* anchor; + GtkWidget* image; + gint x, y; + + gtk_text_view_window_to_buffer_coords(textview, GTK_TEXT_WINDOW_WIDGET, + (int)event.x, (int)event.y, &x, &y); + gtk_text_view_get_iter_at_location(textview, &iter, x, y); + anchor = gtk_text_iter_get_child_anchor(&iter); + if (anchor == NULL) { + return FALSE; + } + GList* widgets = gtk_text_child_anchor_get_widgets(anchor); + for (GList* l = widgets; l != NULL; l = l->next) { + image = GTK_WIDGET(l->data); + if (GTK_IS_IMAGE(image)) { + // Create context menu + GtkWidget* menu = gtk_menu_new(); + GtkWidget* menu_item = gtk_menu_item_new_with_label("Save Image"); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + // g_signal_connect( + // menu_item, "activate", G_CALLBACK(save_image), + // g_object_ref_sink(gtk_image_get_pixbuf(GTK_IMAGE(image)))); + gtk_widget_show_all(menu); + + gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent*)&event); + return TRUE; + } + } + return FALSE; +} + +void DialogBase::OnChatHistoryInsertChildAnchor(DialogBase* self, + const GtkTextIter*, + GtkTextChildAnchor* anchor, + GtkTextBuffer*) { + const char* path = + (const char*)g_object_get_data(G_OBJECT(anchor), "image-path"); + if (!path) { + LOG_WARN("No image path found in anchor."); + return; + } + GtkWidget* image = gtk_image_new_from_file(path); + gtk_text_view_add_child_at_anchor(self->chat_history_widget, image, anchor); + gtk_widget_show(image); +} + } // namespace iptux diff --git a/src/iptux/DialogBase.h b/src/iptux/DialogBase.h index 2f7903de..e33922ef 100644 --- a/src/iptux/DialogBase.h +++ b/src/iptux/DialogBase.h @@ -82,6 +82,13 @@ class DialogBase : public SessionAbstract, public sigc::trackable { static gboolean UpdateFileSendUI(DialogBase* dlggrp); static void RemoveSelectedEnclosure(DialogBase* self); static void OnPasteClipboard(DialogBase* self, GtkTextView* textview); + static gboolean OnChatHistoryButtonPress(DialogBase* self, + GdkEventButton event, + GtkTextView* textview); + static void OnChatHistoryInsertChildAnchor(DialogBase* self, + const GtkTextIter* location, + GtkTextChildAnchor* anchor, + GtkTextBuffer* buffer); protected: Application* app; diff --git a/src/iptux/UiModels.cpp b/src/iptux/UiModels.cpp index 87fe0a20..0cc610c5 100644 --- a/src/iptux/UiModels.cpp +++ b/src/iptux/UiModels.cpp @@ -668,27 +668,19 @@ static void InsertHeaderToBuffer(GtkTextBuffer* buffer, #define OCCUPY_OBJECT 0x01 /** - * 插入图片到TextBuffer(非UI线程安全). + * 插入图片到TextBuffer. * @param buffer text-buffer * @param path 图片路径 */ static void InsertPixbufToBuffer(GtkTextBuffer* buffer, const gchar* path) { - GtkTextIter start, end; - GdkPixbuf* pixbuf; - - if ((pixbuf = gdk_pixbuf_new_from_file(path, NULL))) { - gtk_text_buffer_get_start_iter(buffer, &start); - if (gtk_text_iter_get_char(&start) == OCCUPY_OBJECT || - gtk_text_iter_forward_find_char( - &start, GtkTextCharPredicate(giter_compare_foreach), - GUINT_TO_POINTER(OCCUPY_OBJECT), NULL)) { - end = start; - gtk_text_iter_forward_char(&end); - gtk_text_buffer_delete(buffer, &start, &end); - } - gtk_text_buffer_insert_pixbuf(buffer, &start, pixbuf); - g_object_unref(pixbuf); - } + GtkTextIter iter; + + gtk_text_buffer_get_end_iter(buffer, &iter); + GtkTextChildAnchor* anchor = + gtk_text_buffer_create_child_anchor(buffer, &iter); + g_object_set_data_full(G_OBJECT(anchor), "image-path", g_strdup(path), + GDestroyNotify(g_free)); + gtk_text_buffer_insert_child_anchor(buffer, &iter, anchor); } void GroupInfo::addMsgPara(const MsgPara& para) { From 110850e2daff583e8e2ea4b9b8d5a3b94b27ca38 Mon Sep 17 00:00:00 2001 From: LI Daobing Date: Thu, 23 May 2024 21:19:39 -0700 Subject: [PATCH 03/13] restrict image size to 300 --- src/iptux/DialogBase.cpp | 74 +++++++++++++++++++--------------------- src/iptux/DialogBase.h | 6 ++-- src/iptux/UiHelper.cpp | 13 +++++++ src/iptux/UiHelper.h | 10 ++++++ src/iptux/UiModels.cpp | 4 ++- src/iptux/UiModels.h | 2 ++ 6 files changed, 66 insertions(+), 43 deletions(-) diff --git a/src/iptux/DialogBase.cpp b/src/iptux/DialogBase.cpp index b419d588..ef00101a 100644 --- a/src/iptux/DialogBase.cpp +++ b/src/iptux/DialogBase.cpp @@ -14,6 +14,7 @@ #include "config.h" #include "DialogBase.h" +#include "UiModels.h" #include #include #include @@ -306,9 +307,6 @@ GtkWidget* DialogBase::CreateHistoryArea() { G_CALLBACK(textview_motion_notify_event), NULL); g_signal_connect(chat_history_widget, "visibility-notify-event", G_CALLBACK(textview_visibility_notify_event), NULL); - g_signal_connect_swapped(chat_history_widget, "button-press-event", - G_CALLBACK(DialogBase::OnChatHistoryButtonPress), - this); g_signal_connect_swapped( grpinf->buffer, "insert-child-anchor", G_CALLBACK(DialogBase::OnChatHistoryInsertChildAnchor), this); @@ -795,7 +793,7 @@ GtkTextBuffer* DialogBase::getInputBuffer() { return grpinf->getInputBuffer(); } -void DialogBase::OnPasteClipboard(DialogBase* self, GtkTextView* textview) { +void DialogBase::OnPasteClipboard(DialogBase*, GtkTextView* textview) { GtkClipboard* clipboard; GtkTextBuffer* buffer; GtkTextIter iter; @@ -819,43 +817,29 @@ void DialogBase::OnPasteClipboard(DialogBase* self, GtkTextView* textview) { } } -gboolean DialogBase::OnChatHistoryButtonPress(DialogBase*, - GdkEventButton event, - GtkTextView* textview) { +gboolean DialogBase::OnImageButtonPress(DialogBase*, + GdkEventButton event, + GtkEventBox* event_box) { if (event.type != GDK_BUTTON_PRESS || event.button != 3) { return FALSE; } - GtkTextIter iter; - GtkTextChildAnchor* anchor; - GtkWidget* image; - gint x, y; - - gtk_text_view_window_to_buffer_coords(textview, GTK_TEXT_WINDOW_WIDGET, - (int)event.x, (int)event.y, &x, &y); - gtk_text_view_get_iter_at_location(textview, &iter, x, y); - anchor = gtk_text_iter_get_child_anchor(&iter); - if (anchor == NULL) { + GtkWidget* image = gtk_bin_get_child(GTK_BIN(event_box)); + if (!GTK_IS_IMAGE(image)) { + LOG_ERROR("image not found in event box."); return FALSE; } - GList* widgets = gtk_text_child_anchor_get_widgets(anchor); - for (GList* l = widgets; l != NULL; l = l->next) { - image = GTK_WIDGET(l->data); - if (GTK_IS_IMAGE(image)) { - // Create context menu - GtkWidget* menu = gtk_menu_new(); - GtkWidget* menu_item = gtk_menu_item_new_with_label("Save Image"); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); - // g_signal_connect( - // menu_item, "activate", G_CALLBACK(save_image), - // g_object_ref_sink(gtk_image_get_pixbuf(GTK_IMAGE(image)))); - gtk_widget_show_all(menu); - - gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent*)&event); - return TRUE; - } - } - return FALSE; + + GtkWidget* menu = gtk_menu_new(); + GtkWidget* menu_item = gtk_menu_item_new_with_label(_("Save Image")); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); + // g_signal_connect( + // menu_item, "activate", G_CALLBACK(save_image), + // g_object_ref_sink(gtk_image_get_pixbuf(GTK_IMAGE(image)))); + gtk_widget_show_all(menu); + + gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent*)&event); + return TRUE; } void DialogBase::OnChatHistoryInsertChildAnchor(DialogBase* self, @@ -863,14 +847,26 @@ void DialogBase::OnChatHistoryInsertChildAnchor(DialogBase* self, GtkTextChildAnchor* anchor, GtkTextBuffer*) { const char* path = - (const char*)g_object_get_data(G_OBJECT(anchor), "image-path"); + (const char*)g_object_get_data(G_OBJECT(anchor), kObjectKeyImagePath); if (!path) { LOG_WARN("No image path found in anchor."); return; } - GtkWidget* image = gtk_image_new_from_file(path); - gtk_text_view_add_child_at_anchor(self->chat_history_widget, image, anchor); - gtk_widget_show(image); + + GtkWidget* event_box = gtk_event_box_new(); + + GtkImage* image = igtk_image_new_with_size(path, 300, 300); + if (!image) { + LOG_ERROR("Failed to create image widget."); + return; + } + gtk_container_add(GTK_CONTAINER(event_box), GTK_WIDGET(image)); + g_signal_connect_swapped(event_box, "button-press-event", + G_CALLBACK(DialogBase::OnImageButtonPress), self); + + gtk_text_view_add_child_at_anchor(self->chat_history_widget, event_box, + anchor); + gtk_widget_show_all(event_box); } } // namespace iptux diff --git a/src/iptux/DialogBase.h b/src/iptux/DialogBase.h index e33922ef..2a6f06b5 100644 --- a/src/iptux/DialogBase.h +++ b/src/iptux/DialogBase.h @@ -82,9 +82,9 @@ class DialogBase : public SessionAbstract, public sigc::trackable { static gboolean UpdateFileSendUI(DialogBase* dlggrp); static void RemoveSelectedEnclosure(DialogBase* self); static void OnPasteClipboard(DialogBase* self, GtkTextView* textview); - static gboolean OnChatHistoryButtonPress(DialogBase* self, - GdkEventButton event, - GtkTextView* textview); + static gboolean OnImageButtonPress(DialogBase* self, + GdkEventButton event, + GtkEventBox* eventbox); static void OnChatHistoryInsertChildAnchor(DialogBase* self, const GtkTextIter* location, GtkTextChildAnchor* anchor, diff --git a/src/iptux/UiHelper.cpp b/src/iptux/UiHelper.cpp index 5008c22f..befcebf0 100644 --- a/src/iptux/UiHelper.cpp +++ b/src/iptux/UiHelper.cpp @@ -363,4 +363,17 @@ string StrFirstNonEmptyLine(const string& s) { return s.substr(pos, pos2 - pos); } +GtkImage* igtk_image_new_with_size(const char* filename, + int width, + int height) { + GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file(filename, NULL); + if (!pixbuf) { + LOG_ERROR("Error loading image."); + return NULL; + } + + pixbuf_shrink_scale_1(&pixbuf, width, height); + return GTK_IMAGE(gtk_image_new_from_pixbuf(pixbuf)); +} + } // namespace iptux diff --git a/src/iptux/UiHelper.h b/src/iptux/UiHelper.h index 4134def6..7b2adc51 100644 --- a/src/iptux/UiHelper.h +++ b/src/iptux/UiHelper.h @@ -28,6 +28,16 @@ void pixbuf_shrink_scale_1(GdkPixbuf** pixbuf, int width, int height); void widget_enable_dnd_uri(GtkWidget* widget); GSList* selection_data_get_path(GtkSelectionData* data); +/** + * @brief create GtkImage with width and height + * + * @param filename image file name + * @param width max width, -1 for no limit + * @param height max height, -1 for no limit + * @return GtkImage* null if failed + */ +GtkImage* igtk_image_new_with_size(const char* filename, int width, int height); + /** * @brief only used for test, after call this, pop_info, pop_warning, * and iptux_open_url will only print log diff --git a/src/iptux/UiModels.cpp b/src/iptux/UiModels.cpp index 0cc610c5..34c0045d 100644 --- a/src/iptux/UiModels.cpp +++ b/src/iptux/UiModels.cpp @@ -17,6 +17,8 @@ using namespace std; namespace iptux { +const char* const kObjectKeyImagePath = "image-path"; + /** * 文件传输树(trans-tree)底层数据结构. * 14,0 status,1 task,2 peer,3 ip,4 filename,5 filelength,6 finishlength,7 @@ -678,7 +680,7 @@ static void InsertPixbufToBuffer(GtkTextBuffer* buffer, const gchar* path) { gtk_text_buffer_get_end_iter(buffer, &iter); GtkTextChildAnchor* anchor = gtk_text_buffer_create_child_anchor(buffer, &iter); - g_object_set_data_full(G_OBJECT(anchor), "image-path", g_strdup(path), + g_object_set_data_full(G_OBJECT(anchor), kObjectKeyImagePath, g_strdup(path), GDestroyNotify(g_free)); gtk_text_buffer_insert_child_anchor(buffer, &iter, anchor); } diff --git a/src/iptux/UiModels.h b/src/iptux/UiModels.h index b386b4c8..97570751 100644 --- a/src/iptux/UiModels.h +++ b/src/iptux/UiModels.h @@ -10,6 +10,8 @@ namespace iptux { +extern const char* const kObjectKeyImagePath; + typedef void (*GActionCallback)(GSimpleAction* action, GVariant* parameter, gpointer user_data); From 10689ec158f8c6116267a214f5f215a325332926 Mon Sep 17 00:00:00 2001 From: LI Daobing Date: Thu, 23 May 2024 21:42:28 -0700 Subject: [PATCH 04/13] save image works --- src/iptux/DialogBase.cpp | 46 +++++++++++++++++++++++++++++++++++++--- src/iptux/DialogBase.h | 1 + 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/iptux/DialogBase.cpp b/src/iptux/DialogBase.cpp index ef00101a..1948f505 100644 --- a/src/iptux/DialogBase.cpp +++ b/src/iptux/DialogBase.cpp @@ -833,9 +833,8 @@ gboolean DialogBase::OnImageButtonPress(DialogBase*, GtkWidget* menu = gtk_menu_new(); GtkWidget* menu_item = gtk_menu_item_new_with_label(_("Save Image")); gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item); - // g_signal_connect( - // menu_item, "activate", G_CALLBACK(save_image), - // g_object_ref_sink(gtk_image_get_pixbuf(GTK_IMAGE(image)))); + g_signal_connect_swapped(menu_item, "activate", + G_CALLBACK(DialogBase::OnSaveImage), image); gtk_widget_show_all(menu); gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent*)&event); @@ -854,12 +853,16 @@ void DialogBase::OnChatHistoryInsertChildAnchor(DialogBase* self, } GtkWidget* event_box = gtk_event_box_new(); + gtk_widget_set_focus_on_click(event_box, TRUE); GtkImage* image = igtk_image_new_with_size(path, 300, 300); if (!image) { LOG_ERROR("Failed to create image widget."); return; } + g_object_set_data_full(G_OBJECT(image), kObjectKeyImagePath, g_strdup(path), + g_free); + gtk_container_add(GTK_CONTAINER(event_box), GTK_WIDGET(image)); g_signal_connect_swapped(event_box, "button-press-event", G_CALLBACK(DialogBase::OnImageButtonPress), self); @@ -869,4 +872,41 @@ void DialogBase::OnChatHistoryInsertChildAnchor(DialogBase* self, gtk_widget_show_all(event_box); } +void DialogBase::OnSaveImage(GtkImage* image) { + const char* path = + (const char*)g_object_get_data(G_OBJECT(image), kObjectKeyImagePath); + if (!path) { + LOG_ERROR("No image path found in image widget."); + return; + } + + GtkWidget* dialog = gtk_file_chooser_dialog_new( + _("Save Image"), GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(image))), + GTK_FILE_CHOOSER_ACTION_SAVE, _("_Cancel"), GTK_RESPONSE_CANCEL, + _("_Save"), GTK_RESPONSE_ACCEPT, NULL); + gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog), + TRUE); + gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), "image.png"); + + if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { + char* save_path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); + + GError* error = NULL; + GFile* source = g_file_new_for_path(path); + GFile* destination = g_file_new_for_path(save_path); + gboolean success = g_file_copy(source, destination, G_FILE_COPY_OVERWRITE, + NULL, NULL, NULL, &error); + if (!success) { + LOG_ERROR("Failed to save image: %s", error->message); + g_error_free(error); + } + + g_object_unref(source); + g_object_unref(destination); + g_free(save_path); + } + + gtk_widget_destroy(dialog); +} + } // namespace iptux diff --git a/src/iptux/DialogBase.h b/src/iptux/DialogBase.h index 2a6f06b5..4d55272d 100644 --- a/src/iptux/DialogBase.h +++ b/src/iptux/DialogBase.h @@ -89,6 +89,7 @@ class DialogBase : public SessionAbstract, public sigc::trackable { const GtkTextIter* location, GtkTextChildAnchor* anchor, GtkTextBuffer* buffer); + static void OnSaveImage(GtkImage* self); protected: Application* app; From 312583c3089ecf00d8ba85d69413005a109f170f Mon Sep 17 00:00:00 2001 From: LI Daobing Date: Thu, 23 May 2024 22:12:29 -0700 Subject: [PATCH 05/13] add test --- src/iptux/UiHelperTest.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/iptux/UiHelperTest.cpp b/src/iptux/UiHelperTest.cpp index 0b9c74af..900cce20 100644 --- a/src/iptux/UiHelperTest.cpp +++ b/src/iptux/UiHelperTest.cpp @@ -1,5 +1,6 @@ #include "gtest/gtest.h" +#include "iptux-utils/TestHelper.h" #include "iptux/UiHelper.h" #include #include @@ -43,3 +44,22 @@ TEST(UiHelper, StrFirstNonEmptyLine) { ASSERT_EQ(StrFirstNonEmptyLine("\n b"), "b"); ASSERT_EQ(StrFirstNonEmptyLine(" \n b\n"), "b"); } + +TEST(UiHelper, igtk_image_new_with_size) { + auto image = + igtk_image_new_with_size(testDataPath("iptux.png").c_str(), 100, 100); + ASSERT_NE(image, nullptr); + auto pixbuf = gtk_image_get_pixbuf(image); + ASSERT_EQ(gdk_pixbuf_get_width(pixbuf), 48); + ASSERT_EQ(gdk_pixbuf_get_height(pixbuf), 48); + g_object_unref(pixbuf); + g_object_unref(image); + + image = igtk_image_new_with_size(testDataPath("iptux.png").c_str(), 20, 30); + ASSERT_NE(image, nullptr); + pixbuf = gtk_image_get_pixbuf(image); + ASSERT_EQ(gdk_pixbuf_get_width(pixbuf), 20); + ASSERT_EQ(gdk_pixbuf_get_height(pixbuf), 20); + g_object_unref(pixbuf); + g_object_unref(image); +} From 60c986d2c38ee40f851cbbda58ed7abfbe168c5e Mon Sep 17 00:00:00 2001 From: LI Daobing Date: Thu, 23 May 2024 22:42:14 -0700 Subject: [PATCH 06/13] add more test --- src/iptux/UiModels.cpp | 2 -- src/iptux/UiModelsTest.cpp | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/iptux/UiModels.cpp b/src/iptux/UiModels.cpp index 34c0045d..aa8d6c67 100644 --- a/src/iptux/UiModels.cpp +++ b/src/iptux/UiModels.cpp @@ -667,8 +667,6 @@ static void InsertHeaderToBuffer(GtkTextBuffer* buffer, } } -#define OCCUPY_OBJECT 0x01 - /** * 插入图片到TextBuffer. * @param buffer text-buffer diff --git a/src/iptux/UiModelsTest.cpp b/src/iptux/UiModelsTest.cpp index d70fa2c1..f9684560 100644 --- a/src/iptux/UiModelsTest.cpp +++ b/src/iptux/UiModelsTest.cpp @@ -1,5 +1,6 @@ #include "gtest/gtest.h" +#include "iptux-utils/TestHelper.h" #include #include @@ -76,3 +77,38 @@ TEST(GroupInfo, GetHintAsMarkup) { "\nSignature:\nhello"); } + +static string igtk_text_get_all_text(GtkTextBuffer* buffer) { + GtkTextIter start, end; + gtk_text_buffer_get_start_iter(buffer, &start); + gtk_text_buffer_get_end_iter(buffer, &end); + char* s = gtk_text_buffer_get_slice(buffer, &start, &end, true); + string ret(s); + g_free(s); + return ret; +} + +TEST(GroupInfo, addMsgPara) { + PalInfo pal("127.0.0.1", 2425); + pal.setVersion("1_iptux"); + pal.setName("palname"); + PalInfo me("127.0.0.2", 2425); + PPalInfo cpal = make_shared(pal); + CPPalInfo cme = make_shared(me); + GroupInfo gi(cpal, cme, nullptr); + gi.buffer = gtk_text_buffer_new(nullptr); + + MsgPara msg(cpal); + msg.dtlist.push_back(ChipData("helloworld")); + + gi.addMsgPara(msg); + ASSERT_EQ(igtk_text_get_all_text(gi.buffer).substr(10), + " palname:\nhelloworld\n"); + + msg = MsgPara(cpal); + msg.dtlist.push_back( + ChipData(MessageContentType::PICTURE, testDataPath("iptux.png"))); + gi.addMsgPara(msg); + ASSERT_EQ(igtk_text_get_all_text(gi.buffer).substr(10), + " palname:\nhelloworld\n\xEF\xBF\xBC"); +} From 6d63251851bdf448244aff2ec3aa4031eb311aa1 Mon Sep 17 00:00:00 2001 From: LI Daobing Date: Thu, 23 May 2024 23:21:39 -0700 Subject: [PATCH 07/13] fix warning --- src/iptux/DialogBase.cpp | 7 ++++--- src/iptux/UiModels.cpp | 5 +++-- src/iptux/UiModelsTest.cpp | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/iptux/DialogBase.cpp b/src/iptux/DialogBase.cpp index 1948f505..1f0496a8 100644 --- a/src/iptux/DialogBase.cpp +++ b/src/iptux/DialogBase.cpp @@ -307,9 +307,10 @@ GtkWidget* DialogBase::CreateHistoryArea() { G_CALLBACK(textview_motion_notify_event), NULL); g_signal_connect(chat_history_widget, "visibility-notify-event", G_CALLBACK(textview_visibility_notify_event), NULL); - g_signal_connect_swapped( - grpinf->buffer, "insert-child-anchor", - G_CALLBACK(DialogBase::OnChatHistoryInsertChildAnchor), this); + g_signal_connect_data(grpinf->buffer, "insert-child-anchor", + G_CALLBACK(DialogBase::OnChatHistoryInsertChildAnchor), + this, NULL, + (GConnectFlags)(G_CONNECT_AFTER | G_CONNECT_SWAPPED)); /* 滚动消息到最末位置 */ ScrollHistoryTextview(); diff --git a/src/iptux/UiModels.cpp b/src/iptux/UiModels.cpp index aa8d6c67..f37d9013 100644 --- a/src/iptux/UiModels.cpp +++ b/src/iptux/UiModels.cpp @@ -676,11 +676,12 @@ static void InsertPixbufToBuffer(GtkTextBuffer* buffer, const gchar* path) { GtkTextIter iter; gtk_text_buffer_get_end_iter(buffer, &iter); - GtkTextChildAnchor* anchor = - gtk_text_buffer_create_child_anchor(buffer, &iter); + GtkTextChildAnchor* anchor = gtk_text_child_anchor_new(); g_object_set_data_full(G_OBJECT(anchor), kObjectKeyImagePath, g_strdup(path), GDestroyNotify(g_free)); gtk_text_buffer_insert_child_anchor(buffer, &iter, anchor); + gtk_text_buffer_get_end_iter(buffer, &iter); + gtk_text_buffer_insert(buffer, &iter, "\n", -1); } void GroupInfo::addMsgPara(const MsgPara& para) { diff --git a/src/iptux/UiModelsTest.cpp b/src/iptux/UiModelsTest.cpp index f9684560..00cf13b0 100644 --- a/src/iptux/UiModelsTest.cpp +++ b/src/iptux/UiModelsTest.cpp @@ -110,5 +110,5 @@ TEST(GroupInfo, addMsgPara) { ChipData(MessageContentType::PICTURE, testDataPath("iptux.png"))); gi.addMsgPara(msg); ASSERT_EQ(igtk_text_get_all_text(gi.buffer).substr(10), - " palname:\nhelloworld\n\xEF\xBF\xBC"); + " palname:\nhelloworld\n\xEF\xBF\xBC\n"); } From 26da0fd9642cdae6f8fd0689421c0a77a52ae9fb Mon Sep 17 00:00:00 2001 From: LI Daobing Date: Thu, 23 May 2024 23:31:13 -0700 Subject: [PATCH 08/13] add more test --- src/iptux/DialogPeerTest.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/iptux/DialogPeerTest.cpp b/src/iptux/DialogPeerTest.cpp index 6ce00f6a..b2a35e9d 100644 --- a/src/iptux/DialogPeerTest.cpp +++ b/src/iptux/DialogPeerTest.cpp @@ -38,5 +38,16 @@ TEST(DialogPeer, Constructor) { do_action(dlgpr, "paste"); g_object_unref(pixbuf); + grpinf->buffer = gtk_text_buffer_new(NULL); + MsgPara msg(pal); + msg.dtlist.push_back(ChipData("helloworld")); + + grpinf->addMsgPara(msg); + + msg = MsgPara(pal); + msg.dtlist.push_back( + ChipData(MessageContentType::PICTURE, testDataPath("iptux.png"))); + grpinf->addMsgPara(msg); + DestroyApplication(app); } From 4208bc7ac2056d876cb1aea1cac1da83e25c8210 Mon Sep 17 00:00:00 2001 From: LI Daobing Date: Thu, 23 May 2024 23:34:00 -0700 Subject: [PATCH 09/13] fix buffer generation --- src/iptux/DialogPeerTest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/iptux/DialogPeerTest.cpp b/src/iptux/DialogPeerTest.cpp index b2a35e9d..b74ef0e1 100644 --- a/src/iptux/DialogPeerTest.cpp +++ b/src/iptux/DialogPeerTest.cpp @@ -21,6 +21,7 @@ TEST(DialogPeer, Constructor) { app->getCoreThread()->AttachPalToList(pal); GroupInfo* grpinf = app->getCoreThread()->GetPalRegularItem(pal.get()); + grpinf->buffer = gtk_text_buffer_new(NULL); DialogPeer* dlgpr = new DialogPeer(app, grpinf); auto clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); @@ -38,7 +39,6 @@ TEST(DialogPeer, Constructor) { do_action(dlgpr, "paste"); g_object_unref(pixbuf); - grpinf->buffer = gtk_text_buffer_new(NULL); MsgPara msg(pal); msg.dtlist.push_back(ChipData("helloworld")); From 82a82ed8e7e5ac09f90807d4178cb5e8af3b2429 Mon Sep 17 00:00:00 2001 From: LI Daobing Date: Thu, 23 May 2024 23:59:24 -0700 Subject: [PATCH 10/13] add header for img --- src/iptux-utils/utils.cpp | 21 +++++++++++++++++++++ src/iptux-utils/utils.h | 4 +++- src/iptux/UiModels.cpp | 27 +++++++++++++++++---------- src/iptux/UiModels.h | 1 + src/iptux/UiModelsTest.cpp | 17 +++++++++++------ 5 files changed, 53 insertions(+), 17 deletions(-) diff --git a/src/iptux-utils/utils.cpp b/src/iptux-utils/utils.cpp index c2e91c7f..d2173222 100644 --- a/src/iptux-utils/utils.cpp +++ b/src/iptux-utils/utils.cpp @@ -186,6 +186,27 @@ char* getformattime(gboolean date, const char* format, ...) { return ptr; } +char* getformattime2(time_t tt, gboolean date, const char* format, ...) { + char buf[MAX_BUFLEN], *msg, *ptr; + va_list ap; + + va_start(ap, format); + msg = g_strdup_vprintf(format, ap); + va_end(ap); + + struct tm tm; + localtime_r(&tt, &tm); + if (date) + strftime(buf, MAX_BUFLEN, "%c", &tm); + else + strftime(buf, MAX_BUFLEN, "%X", &tm); + + ptr = g_strdup_printf("(%s) %s:", buf, msg); + g_free(msg); + + return ptr; +} + /** * 对GtkTextBuffer的迭代器(GtkTextIter)所指的字符进行比较. * @param src 源字符 diff --git a/src/iptux-utils/utils.h b/src/iptux-utils/utils.h index 0197b516..2cf70182 100644 --- a/src/iptux-utils/utils.h +++ b/src/iptux-utils/utils.h @@ -47,7 +47,9 @@ char* convert_encode(const char* string, const char* fromcode); std::string assert_filename_inexist(const char* path); std::string dupPath(const std::string& fname, int idx); -char* getformattime(gboolean date, const char* format, ...); +char* getformattime(gboolean date, const char* format, ...) G_GNUC_PRINTF(2, 3); +char* getformattime2(time_t now, gboolean date, const char* format, ...) + G_GNUC_PRINTF(3, 4); gboolean giter_compare_foreach(gunichar src, gunichar dst); diff --git a/src/iptux/UiModels.cpp b/src/iptux/UiModels.cpp index f37d9013..4a5ad107 100644 --- a/src/iptux/UiModels.cpp +++ b/src/iptux/UiModels.cpp @@ -624,6 +624,8 @@ static void InsertStringToBuffer(GtkTextBuffer* buffer, const gchar* s) { } g_match_info_free(matchinfo); gtk_text_buffer_insert(buffer, &iter, string + urlendp, -1); + gtk_text_buffer_get_end_iter(buffer, &iter); + gtk_text_buffer_insert(buffer, &iter, "\n", -1); } /** @@ -633,7 +635,8 @@ static void InsertStringToBuffer(GtkTextBuffer* buffer, const gchar* s) { */ static void InsertHeaderToBuffer(GtkTextBuffer* buffer, const MsgPara* para, - CPPalInfo me) { + CPPalInfo me, + time_t now) { GtkTextIter iter; gchar* header; @@ -642,21 +645,22 @@ static void InsertHeaderToBuffer(GtkTextBuffer* buffer, */ switch (para->stype) { case MessageSourceType::PAL: - header = getformattime(FALSE, "%s", para->getPal()->getName().c_str()); + header = + getformattime2(now, FALSE, "%s", para->getPal()->getName().c_str()); gtk_text_buffer_get_end_iter(buffer, &iter); gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, header, -1, "pal-color", NULL); g_free(header); break; case MessageSourceType::SELF: - header = getformattime(FALSE, "%s", me->getName().c_str()); + header = getformattime2(now, FALSE, "%s", me->getName().c_str()); gtk_text_buffer_get_end_iter(buffer, &iter); gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, header, -1, "me-color", NULL); g_free(header); break; case MessageSourceType::ERROR: - header = getformattime(FALSE, "%s", _("")); + header = getformattime2(now, FALSE, "%s", _("")); gtk_text_buffer_get_end_iter(buffer, &iter); gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, header, -1, "error-color", NULL); @@ -665,6 +669,8 @@ static void InsertHeaderToBuffer(GtkTextBuffer* buffer, default: break; } + gtk_text_buffer_get_end_iter(buffer, &iter); + gtk_text_buffer_insert(buffer, &iter, "\n", -1); } /** @@ -685,7 +691,11 @@ static void InsertPixbufToBuffer(GtkTextBuffer* buffer, const gchar* path) { } void GroupInfo::addMsgPara(const MsgPara& para) { - GtkTextIter iter; + time_t now = time(NULL); + _addMsgPara(para, now); +} + +void GroupInfo::_addMsgPara(const MsgPara& para, time_t now) { const gchar* data; time(&last_activity_); @@ -695,18 +705,15 @@ void GroupInfo::addMsgPara(const MsgPara& para) { data = chipData->data.c_str(); switch (chipData->type) { case MESSAGE_CONTENT_TYPE_STRING: - InsertHeaderToBuffer(buffer, ¶, me); - gtk_text_buffer_get_end_iter(buffer, &iter); - gtk_text_buffer_insert(buffer, &iter, "\n", -1); + InsertHeaderToBuffer(buffer, ¶, me, now); InsertStringToBuffer(buffer, data); - gtk_text_buffer_get_end_iter(buffer, &iter); - gtk_text_buffer_insert(buffer, &iter, "\n", -1); last_message_ = StrFirstNonEmptyLine(chipData->data); if (logSystem) { logSystem->communicateLog(¶, "[STRING]%s", data); } break; case MESSAGE_CONTENT_TYPE_PICTURE: + InsertHeaderToBuffer(buffer, ¶, me, now); InsertPixbufToBuffer(buffer, data); last_message_ = _("[IMG]"); if (logSystem) { diff --git a/src/iptux/UiModels.h b/src/iptux/UiModels.h index 97570751..53e77866 100644 --- a/src/iptux/UiModels.h +++ b/src/iptux/UiModels.h @@ -83,6 +83,7 @@ class GroupInfo { bool hasPal(PPalInfo pal) const; void addMsgPara(const MsgPara& msg); + void _addMsgPara(const MsgPara& msg, time_t t); void readAllMsg(); int getUnreadMsgCount() const; std::string GetInfoAsMarkup(GroupInfoStyle style) const; diff --git a/src/iptux/UiModelsTest.cpp b/src/iptux/UiModelsTest.cpp index 00cf13b0..cb663b59 100644 --- a/src/iptux/UiModelsTest.cpp +++ b/src/iptux/UiModelsTest.cpp @@ -101,14 +101,19 @@ TEST(GroupInfo, addMsgPara) { MsgPara msg(cpal); msg.dtlist.push_back(ChipData("helloworld")); - gi.addMsgPara(msg); - ASSERT_EQ(igtk_text_get_all_text(gi.buffer).substr(10), - " palname:\nhelloworld\n"); + time_t now = 1716533706; + setenv("TZ", "GMT", 1); + tzset(); + + gi._addMsgPara(msg, now); + ASSERT_EQ(igtk_text_get_all_text(gi.buffer), + "(06:55:06) palname:\nhelloworld\n"); msg = MsgPara(cpal); msg.dtlist.push_back( ChipData(MessageContentType::PICTURE, testDataPath("iptux.png"))); - gi.addMsgPara(msg); - ASSERT_EQ(igtk_text_get_all_text(gi.buffer).substr(10), - " palname:\nhelloworld\n\xEF\xBF\xBC\n"); + gi._addMsgPara(msg, now + 1); + ASSERT_EQ( + igtk_text_get_all_text(gi.buffer), + "(06:55:06) palname:\nhelloworld\n(06:55:07) palname:\n\xEF\xBF\xBC\n"); } From b77976a6867efc375d9dfa3278087948ea78e767 Mon Sep 17 00:00:00 2001 From: LI Daobing Date: Fri, 24 May 2024 00:03:29 -0700 Subject: [PATCH 11/13] use bmp --- src/iptux/DialogBase.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/iptux/DialogBase.cpp b/src/iptux/DialogBase.cpp index 1f0496a8..08ef69d6 100644 --- a/src/iptux/DialogBase.cpp +++ b/src/iptux/DialogBase.cpp @@ -849,7 +849,7 @@ void DialogBase::OnChatHistoryInsertChildAnchor(DialogBase* self, const char* path = (const char*)g_object_get_data(G_OBJECT(anchor), kObjectKeyImagePath); if (!path) { - LOG_WARN("No image path found in anchor."); + LOG_ERROR("No image path found in anchor."); return; } @@ -887,7 +887,7 @@ void DialogBase::OnSaveImage(GtkImage* image) { _("_Save"), GTK_RESPONSE_ACCEPT, NULL); gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog), TRUE); - gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), "image.png"); + gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), "image.bmp"); if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { char* save_path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); From 4f414bb61a4f6f374d20491ffdc950090aaf847a Mon Sep 17 00:00:00 2001 From: LI Daobing Date: Fri, 24 May 2024 00:14:54 -0700 Subject: [PATCH 12/13] Update DialogPeer.cpp --- src/iptux/DialogPeer.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/iptux/DialogPeer.cpp b/src/iptux/DialogPeer.cpp index 5fbb55b2..66ac8db4 100644 --- a/src/iptux/DialogPeer.cpp +++ b/src/iptux/DialogPeer.cpp @@ -427,10 +427,16 @@ bool DialogPeer::SendTextMsg() { /* 保存图片 */ chipmsg = g_strdup_printf("%s" IPTUX_PATH "/%" PRIx32, g_get_user_config_dir(), count++); - gdk_pixbuf_save(pixbuf, chipmsg, "bmp", NULL, NULL); - /* 新建一个碎片数据(图片),并加入数据链表 */ - ChipData chip(MESSAGE_CONTENT_TYPE_PICTURE, chipmsg); - dtlist.push_back(std::move(chip)); + GError* error = nullptr; + gdk_pixbuf_save(pixbuf, chipmsg, "png", &error, NULL); + if (error) { + LOG_ERROR("failed to save image: %s", error->message); + g_error_free(error); + } else { + /* 新建一个碎片数据(图片),并加入数据链表 */ + ChipData chip(MESSAGE_CONTENT_TYPE_PICTURE, chipmsg); + dtlist.push_back(std::move(chip)); + } } } while (gtk_text_iter_forward_find_char( &iter, GtkTextCharPredicate(giter_compare_foreach), From 32ab5143577a849c2f88e0162f185c6ca4a8f545 Mon Sep 17 00:00:00 2001 From: LI Daobing Date: Fri, 24 May 2024 00:15:35 -0700 Subject: [PATCH 13/13] save to png --- src/iptux/DialogBase.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/iptux/DialogBase.cpp b/src/iptux/DialogBase.cpp index 08ef69d6..9d7f8046 100644 --- a/src/iptux/DialogBase.cpp +++ b/src/iptux/DialogBase.cpp @@ -887,7 +887,7 @@ void DialogBase::OnSaveImage(GtkImage* image) { _("_Save"), GTK_RESPONSE_ACCEPT, NULL); gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog), TRUE); - gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), "image.bmp"); + gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog), "image.png"); if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { char* save_path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));