messenger-gtk

Gtk+3 graphical user interfaces for GNUnet Messenger
Log | Files | Refs | Submodules | README | LICENSE

commit a001a54216f37fd789420ae64605f57e26635c83
parent e0b8f28cf38b9a73fa151f59479fe6bf98cb6f65
Author: Jacki <jacki@thejackimonster.de>
Date:   Sat, 23 Mar 2024 20:51:22 +0100

Add media previews to chat details

Signed-off-by: Jacki <jacki@thejackimonster.de>

Diffstat:
Mresources/ui.gresource.xml | 1+
Aresources/ui/media_preview.ui | 41+++++++++++++++++++++++++++++++++++++++++
Msrc/ui/chat.c | 92++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/ui/file_entry.h | 2--
Asrc/ui/media_preview.c | 247+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/ui/media_preview.h | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/ui/meson.build | 1+
Msrc/ui/message.c | 4++--
8 files changed, 457 insertions(+), 9 deletions(-)

diff --git a/resources/ui.gresource.xml b/resources/ui.gresource.xml @@ -12,6 +12,7 @@ <file compressed="true">ui/file_entry.ui</file> <file compressed="true">ui/file_load_entry.ui</file> <file compressed="true">ui/invite_contact.ui</file> + <file compressed="true">ui/media_preview.ui</file> <file compressed="true">ui/message_content.ui</file> <file compressed="true">ui/message.ui</file> <file compressed="true">ui/message-sent.ui</file> diff --git a/resources/ui/media_preview.ui b/resources/ui/media_preview.ui @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.40.0 + +Copyright (C) 2024 GNUnet e.V. + +GNUnet is free software: you can redistribute it and/or modify it +under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, +or (at your option) any later version. + +GNUnet is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. + +SPDX-License-Identifier: AGPL3.0-or-later +Author: Tobias Frisch + +--> +<interface> + <requires lib="gtk+" version="3.24"/> + <object class="GtkBox" id="media_box"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkDrawingArea" id="preview_drawing_area"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> +</interface> diff --git a/src/ui/chat.c b/src/ui/chat.c @@ -33,6 +33,7 @@ #include "chat_entry.h" #include "file_entry.h" #include "file_load_entry.h" +#include "media_preview.h" #include "message.h" #include "messenger.h" #include "picker.h" @@ -1857,7 +1858,7 @@ ui_chat_new(MESSENGER_Application *app) struct IterateChatClosure { MESSENGER_Application *app; - GtkListBox *listbox; + GtkContainer *container; }; static enum GNUNET_GenericReturnValue @@ -1869,7 +1870,7 @@ iterate_ui_chat_update_group_contacts(void *cls, (struct IterateChatClosure*) cls ); - GtkListBox *listbox = closure->listbox; + GtkListBox *listbox = GTK_LIST_BOX(closure->container); UI_ACCOUNT_ENTRY_Handle* entry = ui_account_entry_new(closure->app); ui_account_entry_set_contact(entry, contact); @@ -1920,7 +1921,7 @@ _chat_update_contacts(UI_CHAT_Handle *handle, { struct IterateChatClosure closure; closure.app = app; - closure.listbox = handle->chat_contacts_listbox; + closure.container = GTK_CONTAINER(handle->chat_contacts_listbox); GNUNET_CHAT_group_iterate_contacts( group, @@ -1944,7 +1945,7 @@ iterate_ui_chat_update_context_files(void *cls, (struct IterateChatClosure*) cls ); - GtkListBox *listbox = closure->listbox; + GtkListBox *listbox = GTK_LIST_BOX(closure->container); UI_FILE_ENTRY_Handle* entry = ui_file_entry_new(closure->app); ui_file_entry_update(entry, file); @@ -1992,7 +1993,7 @@ _chat_update_files(UI_CHAT_Handle *handle, struct IterateChatClosure closure; closure.app = app; - closure.listbox = handle->chat_files_listbox; + closure.container = GTK_CONTAINER(handle->chat_files_listbox); const int count = GNUNET_CHAT_context_iterate_files( context, @@ -2006,6 +2007,86 @@ _chat_update_files(UI_CHAT_Handle *handle, ); } +static enum GNUNET_GenericReturnValue +iterate_ui_chat_update_context_media(void *cls, + struct GNUNET_CHAT_Context *context, + struct GNUNET_CHAT_File *file) +{ + struct IterateChatClosure *closure = ( + (struct IterateChatClosure*) cls + ); + + GtkFlowBox *flowbox = GTK_FLOW_BOX(closure->container); + UI_MEDIA_PREVIEW_Handle* handle = ui_media_preview_new(closure->app); + ui_media_preview_update(handle, file); + + if ((! handle->preview_animation) && (! handle->preview_image)) + { + ui_media_preview_delete(handle); + return GNUNET_YES; + } + + gtk_flow_box_insert(flowbox, handle->media_box, -1); + + GtkFlowBoxChild *child = GTK_FLOW_BOX_CHILD( + gtk_widget_get_parent(handle->media_box) + ); + + g_object_set_qdata(G_OBJECT(child), closure->app->quarks.data, file); + g_object_set_qdata_full( + G_OBJECT(child), + closure->app->quarks.ui, + handle, + (GDestroyNotify) ui_media_preview_delete + ); + + gtk_widget_set_size_request(GTK_WIDGET(child), 80, 80); + + gtk_widget_show_all(GTK_WIDGET(child)); + return GNUNET_YES; +} + +static void +_chat_update_media(UI_CHAT_Handle *handle, + MESSENGER_Application *app, + struct GNUNET_CHAT_Context *context) +{ + g_assert((handle) && (app) && (context)); + + GList* children = gtk_container_get_children( + GTK_CONTAINER(handle->chat_media_flowbox) + ); + + GList *item = children; + while (item) { + GtkWidget *widget = GTK_WIDGET(item->data); + item = item->next; + + gtk_container_remove( + GTK_CONTAINER(handle->chat_media_flowbox), + widget + ); + } + + if (children) + g_list_free(children); + + struct IterateChatClosure closure; + closure.app = app; + closure.container = GTK_CONTAINER(handle->chat_media_flowbox); + + const int count = GNUNET_CHAT_context_iterate_files( + context, + iterate_ui_chat_update_context_media, + &closure + ); + + gtk_widget_set_visible( + GTK_WIDGET(handle->chat_details_media_box), + count? TRUE : FALSE + ); +} + void ui_chat_update(UI_CHAT_Handle *handle, MESSENGER_Application *app, @@ -2061,6 +2142,7 @@ ui_chat_update(UI_CHAT_Handle *handle, _chat_update_contacts(handle, app, group); _chat_update_files(handle, app, context); + _chat_update_media(handle, app, context); g_object_set_qdata( G_OBJECT(handle->reveal_identity_button), diff --git a/src/ui/file_entry.h b/src/ui/file_entry.h @@ -29,8 +29,6 @@ #include "messenger.h" -typedef struct UI_CHAT_Handle UI_CHAT_Handle; - typedef struct UI_FILE_ENTRY_Handle { GtkBuilder *builder; diff --git a/src/ui/media_preview.c b/src/ui/media_preview.c @@ -0,0 +1,247 @@ +/* + This file is part of GNUnet. + Copyright (C) 2024 GNUnet e.V. + + GNUnet is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + GNUnet is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + + SPDX-License-Identifier: AGPL3.0-or-later + */ +/* + * @author Tobias Frisch + * @file ui/media_preview.c + */ + +#include "media_preview.h" + +#include "../application.h" + +static int +handle_media_preview_redraw_animation(gpointer user_data) +{ + g_assert(user_data); + + UI_MEDIA_PREVIEW_Handle *handle = (UI_MEDIA_PREVIEW_Handle*) user_data; + + handle->redraw_animation = 0; + + if ((handle->preview_drawing_area) && + ((handle->preview_image) || + (handle->preview_animation) || + (handle->preview_animation_iter))) + gtk_widget_queue_draw(GTK_WIDGET(handle->preview_drawing_area)); + + return FALSE; +} + +static gboolean +handle_preview_drawing_area_draw(GtkWidget* drawing_area, + cairo_t* cairo, + gpointer user_data) +{ + g_assert((drawing_area) && (cairo) && (user_data)); + + UI_MEDIA_PREVIEW_Handle *handle = (UI_MEDIA_PREVIEW_Handle*) user_data; + + GtkStyleContext* context = gtk_widget_get_style_context(drawing_area); + + const guint width = gtk_widget_get_allocated_width(drawing_area); + const guint height = gtk_widget_get_allocated_height(drawing_area); + + gtk_render_background(context, cairo, 0, 0, width, height); + + GdkPixbuf *image = handle->preview_image; + + if (!(handle->preview_animation)) + goto render_image; + + if (handle->preview_animation_iter) + gdk_pixbuf_animation_iter_advance(handle->preview_animation_iter, NULL); + else + handle->preview_animation_iter = gdk_pixbuf_animation_get_iter( + handle->preview_animation, NULL + ); + + image = gdk_pixbuf_animation_iter_get_pixbuf(handle->preview_animation_iter); + + const int delay = gdk_pixbuf_animation_iter_get_delay_time( + handle->preview_animation_iter + ); + + handle->redraw_animation = g_timeout_add( + delay, handle_media_preview_redraw_animation, handle + ); + +render_image: + if (!image) + return FALSE; + + int swidth = gdk_pixbuf_get_width(image); + int sheight = gdk_pixbuf_get_height(image); + + int sx = 0; + int sy = 0; + + if (swidth > sheight) + { + sx = swidth - sheight; + swidth -= sx; + sx /= 2; + } + else + { + sy = sheight - swidth; + sheight -= sy; + sy /= 2; + } + + double ratio_width = 1.0 * width / swidth; + double ratio_height = 1.0 * height / sheight; + + const double ratio = ratio_width < ratio_height? ratio_width : ratio_height; + + const int interp_type = (ratio >= 1.0? + GDK_INTERP_NEAREST : + GDK_INTERP_BILINEAR + ); + + GdkPixbuf* subimage = gdk_pixbuf_new_subpixbuf( + image, + sx, + sy, + swidth, + sheight + ); + + GdkPixbuf* scaled = gdk_pixbuf_scale_simple( + subimage, + width, + height, + interp_type + ); + + g_object_unref(subimage); + gtk_render_icon(context, cairo, scaled, 0, 0); + + cairo_fill(cairo); + g_object_unref(scaled); + + if (handle->preview_image) + { + g_object_unref(handle->preview_image); + handle->preview_image = NULL; + } + + return FALSE; +} + +static void +_clear_message_preview_data(UI_MEDIA_PREVIEW_Handle *handle) +{ + g_assert(handle); + + if (handle->preview_image) + { + g_object_unref(handle->preview_image); + handle->preview_image = NULL; + } + + if (handle->redraw_animation) + { + g_source_remove(handle->redraw_animation); + handle->redraw_animation = 0; + } + + if (handle->preview_animation_iter) + { + g_object_unref(handle->preview_animation_iter); + handle->preview_animation_iter = NULL; + } + + if (handle->preview_animation) + { + g_object_unref(handle->preview_animation); + handle->preview_animation = NULL; + } +} + +UI_MEDIA_PREVIEW_Handle* +ui_media_preview_new(MESSENGER_Application *app) +{ + g_assert(app); + + UI_MEDIA_PREVIEW_Handle* handle = g_malloc(sizeof(UI_MEDIA_PREVIEW_Handle)); + + handle->builder = gtk_builder_new_from_resource( + application_get_resource_path(app, "ui/media_preview.ui") + ); + + handle->media_box = GTK_WIDGET( + gtk_builder_get_object(handle->builder, "media_box") + ); + + handle->preview_drawing_area = GTK_DRAWING_AREA( + gtk_builder_get_object(handle->builder, "preview_drawing_area") + ); + + g_signal_connect( + handle->preview_drawing_area, + "draw", + G_CALLBACK(handle_preview_drawing_area_draw), + handle + ); + + handle->preview_image = NULL; + handle->preview_animation = NULL; + handle->preview_animation_iter = NULL; + + handle->redraw_animation = 0; + + return handle; +} + +void +ui_media_preview_update(UI_MEDIA_PREVIEW_Handle *handle, + struct GNUNET_CHAT_File *file) +{ + g_assert((handle) && (file)); + + const char *preview = GNUNET_CHAT_file_open_preview(file); + + if (!preview) + return; + + handle->preview_animation = gdk_pixbuf_animation_new_from_file( + preview, NULL + ); + + if (!(handle->preview_animation)) + handle->preview_image = gdk_pixbuf_new_from_file(preview, NULL); + + GNUNET_CHAT_file_close_preview(file); + + if ((handle->preview_animation) || (handle->preview_image)) + gtk_widget_queue_draw(GTK_WIDGET(handle->preview_drawing_area)); +} + +void +ui_media_preview_delete(UI_MEDIA_PREVIEW_Handle *handle) +{ + g_assert(handle); + + _clear_message_preview_data(handle); + + g_object_unref(handle->builder); + + g_free(handle); +} diff --git a/src/ui/media_preview.h b/src/ui/media_preview.h @@ -0,0 +1,78 @@ +/* + This file is part of GNUnet. + Copyright (C) 2024 GNUnet e.V. + + GNUnet is free software: you can redistribute it and/or modify it + under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + GNUnet is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + + SPDX-License-Identifier: AGPL3.0-or-later + */ +/* + * @author Tobias Frisch + * @file ui/media_preview.h + */ + +#ifndef UI_MEDIA_PREVIEW_H_ +#define UI_MEDIA_PREVIEW_H_ + +#include <gnunet/gnunet_chat_lib.h> + +#include "messenger.h" + +typedef struct UI_MEDIA_PREVIEW_Handle +{ + GtkBuilder *builder; + + GtkWidget *media_box; + + GtkDrawingArea *preview_drawing_area; + + GdkPixbuf *preview_image; + GdkPixbufAnimation *preview_animation; + GdkPixbufAnimationIter *preview_animation_iter; + + guint redraw_animation; +} UI_MEDIA_PREVIEW_Handle; + +/** + * Allocates and creates a new media preview handle + * to manage loading files for a given messenger + * application. + * + * @param app Messenger application + * @return New media preview handle + */ +UI_MEDIA_PREVIEW_Handle* +ui_media_preview_new(MESSENGER_Application *app); + +/** + * Updates a media preview handle with a selected + * file to represent it visually. + * + * @param handle Media preview handle + * @param file Chat file + */ +void +ui_media_preview_update(UI_MEDIA_PREVIEW_Handle *handle, + struct GNUNET_CHAT_File *file); + +/** + * Frees its resources and destroys a given media + * preview handle. + * + * @param handle Media preview handle + */ +void +ui_media_preview_delete(UI_MEDIA_PREVIEW_Handle *handle); + +#endif /* UI_MEDIA_PREVIEW_H_ */ diff --git a/src/ui/meson.build b/src/ui/meson.build @@ -31,6 +31,7 @@ messenger_gtk_ui_sources = files([ 'file_entry.c', 'file_entry.h', 'file_load_entry.c', 'file_load_entry.h', 'invite_contact.c', 'invite_contact.h', + 'media_preview.c', 'media_preview.h', 'message.c', 'message.h', 'messenger.c', 'messenger.h', 'new_account.c', 'new_account.h', diff --git a/src/ui/message.c b/src/ui/message.c @@ -634,6 +634,8 @@ _update_file_message(UI_MESSAGE_Handle *handle, if (!(handle->preview_animation)) handle->preview_image = gdk_pixbuf_new_from_file(preview, NULL); + GNUNET_CHAT_file_close_preview(file); + if ((handle->preview_animation) || (handle->preview_image)) { gtk_widget_set_size_request( @@ -651,8 +653,6 @@ _update_file_message(UI_MESSAGE_Handle *handle, return; } - GNUNET_CHAT_file_close_preview(file); - if (_message_media_supports_file_extension(filename)) { gtk_image_set_from_icon_name(