| /* |
| * Copyright (C) 2016-2018 Savoir-faire Linux Inc. |
| * Author: Nicolas Jager <nicolas.jager@savoirfairelinux.com> |
| * Author: Stepan Salenikovich <stepan.salenikovich@savoirfairelinux.com> |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 3 of the License, or |
| * (at your option) any later version. |
| * |
| * This program 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 General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| */ |
| |
| #include "avatarmanipulation.h" |
| |
| /* LRC */ |
| #include <api/newaccountmodel.h> |
| #include <globalinstances.h> |
| #include <person.h> |
| #include <profile.h> |
| #include <video/configurationproxy.h> |
| #include <video/previewmanager.h> |
| #include <video/devicemodel.h> |
| |
| /* client */ |
| #include "native/pixbufmanipulator.h" |
| #include "video/video_widget.h" |
| #include "cc-crop-area.h" |
| |
| /* system */ |
| #include <glib/gi18n.h> |
| |
| /* size of avatar */ |
| static constexpr int AVATAR_WIDTH = 150; /* px */ |
| static constexpr int AVATAR_HEIGHT = 150; /* px */ |
| |
| /* size of video widget */ |
| static constexpr int VIDEO_WIDTH = 150; /* px */ |
| static constexpr int VIDEO_HEIGHT = 150; /* px */ |
| |
| struct _AvatarManipulation |
| { |
| GtkBox parent; |
| }; |
| |
| struct _AvatarManipulationClass |
| { |
| GtkBoxClass parent_class; |
| }; |
| |
| typedef struct _AvatarManipulationPrivate AvatarManipulationPrivate; |
| |
| struct _AvatarManipulationPrivate |
| { |
| AccountInfoPointer const *accountInfo_ = nullptr; |
| gchar* temporaryAvatar = nullptr; |
| |
| GtkWidget *stack_avatar_manipulation; |
| GtkWidget *video_widget; |
| GtkWidget *box_views_and_controls; |
| GtkWidget *box_controls; |
| |
| GtkWidget *button_box_current; |
| GtkWidget *button_box_photo; |
| GtkWidget *button_box_edit; |
| |
| GtkWidget *button_start_camera; |
| GtkWidget *button_choose_picture; |
| GtkWidget *button_take_photo; |
| GtkWidget *button_return_photo; |
| GtkWidget *button_set_avatar; |
| GtkWidget *button_return_edit; |
| |
| // GtkWidget *selector_widget; |
| GtkWidget *stack_views; |
| GtkWidget *image_avatar; |
| GtkWidget *vbox_crop_area; |
| GtkWidget *frame_video; |
| |
| AvatarManipulationState state; |
| AvatarManipulationState last_state; |
| |
| /* this is used to keep track of the state of the preview when the camera is used to take a |
| * photo; if a call is in progress, then the preview should already be started and we don't want |
| * to stop it when the settings are closed, in this case |
| */ |
| gboolean video_started_by_avatar_manipulation; |
| |
| GtkWidget *crop_area; |
| }; |
| |
| G_DEFINE_TYPE_WITH_PRIVATE(AvatarManipulation, avatar_manipulation, GTK_TYPE_BOX); |
| |
| #define AVATAR_MANIPULATION_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), AVATAR_MANIPULATION_TYPE, \ |
| AvatarManipulationPrivate)) |
| |
| static void set_state(AvatarManipulation *self, AvatarManipulationState state); |
| |
| static void start_camera(AvatarManipulation *self); |
| static void take_a_photo(AvatarManipulation *self); |
| static void choose_picture(AvatarManipulation *self); |
| static void return_to_previous(AvatarManipulation *self); |
| static void update_preview_cb(GtkFileChooser *file_chooser, GtkWidget *preview); |
| static void set_avatar(AvatarManipulation *self); |
| static void got_snapshot(AvatarManipulation *parent); |
| |
| static void |
| avatar_manipulation_dispose(GObject *object) |
| { |
| AvatarManipulationPrivate *priv = AVATAR_MANIPULATION_GET_PRIVATE(object); |
| |
| /* make sure we stop the preview and the video widget */ |
| if (priv->video_started_by_avatar_manipulation) |
| Video::PreviewManager::instance().stopPreview(); |
| if (priv->video_widget) { |
| gtk_container_remove(GTK_CONTAINER(priv->frame_video), priv->video_widget); |
| priv->video_widget = NULL; |
| } |
| |
| G_OBJECT_CLASS(avatar_manipulation_parent_class)->dispose(object); |
| } |
| |
| static void |
| avatar_manipulation_finalize(GObject *object) |
| { |
| G_OBJECT_CLASS(avatar_manipulation_parent_class)->finalize(object); |
| } |
| |
| GtkWidget* |
| avatar_manipulation_new(AccountInfoPointer const & accountInfo) |
| { |
| // a profile must exist |
| gpointer view = g_object_new(AVATAR_MANIPULATION_TYPE, NULL); |
| |
| auto* priv = AVATAR_MANIPULATION_GET_PRIVATE(view); |
| priv->accountInfo_ = &accountInfo; |
| |
| set_state(AVATAR_MANIPULATION(view), AVATAR_MANIPULATION_STATE_CURRENT); |
| |
| return reinterpret_cast<GtkWidget*>(view); |
| } |
| |
| GtkWidget* |
| avatar_manipulation_new_from_wizard(void) |
| { |
| // a profile must exist |
| gpointer view = g_object_new(AVATAR_MANIPULATION_TYPE, NULL); |
| |
| auto* priv = AVATAR_MANIPULATION_GET_PRIVATE(view); |
| priv->accountInfo_ = nullptr; |
| |
| set_state(AVATAR_MANIPULATION(view), AVATAR_MANIPULATION_STATE_CURRENT); |
| |
| return reinterpret_cast<GtkWidget*>(view); |
| } |
| |
| gchar* |
| avatar_manipulation_get_temporary(AvatarManipulation *view) |
| { |
| g_return_val_if_fail(IS_AVATAR_MANIPULATION(view), nullptr); |
| AvatarManipulationPrivate *priv = AVATAR_MANIPULATION_GET_PRIVATE(view); |
| return priv->temporaryAvatar; |
| } |
| |
| static void |
| avatar_manipulation_class_init(AvatarManipulationClass *klass) |
| { |
| G_OBJECT_CLASS(klass)->finalize = avatar_manipulation_finalize; |
| G_OBJECT_CLASS(klass)->dispose = avatar_manipulation_dispose; |
| |
| gtk_widget_class_set_template_from_resource(GTK_WIDGET_CLASS (klass), "/cx/ring/RingGnome/avatarmanipulation.ui"); |
| |
| gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, box_views_and_controls); |
| gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, box_controls); |
| gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, button_start_camera); |
| gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, button_choose_picture); |
| gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, button_take_photo); |
| gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, button_return_photo); |
| gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, button_set_avatar); |
| gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, button_return_edit); |
| gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, stack_views); |
| gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, image_avatar); |
| gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, frame_video); |
| gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, vbox_crop_area); |
| gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, button_box_current); |
| gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, button_box_photo); |
| gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, button_box_edit); |
| } |
| |
| static void |
| avatar_manipulation_init(AvatarManipulation *self) |
| { |
| AvatarManipulationPrivate *priv = AVATAR_MANIPULATION_GET_PRIVATE(self); |
| gtk_widget_init_template(GTK_WIDGET(self)); |
| |
| /* our desired size for the image area */ |
| gtk_widget_set_size_request(priv->stack_views, VIDEO_WIDTH, VIDEO_HEIGHT); |
| |
| /* signals */ |
| g_signal_connect_swapped(priv->button_start_camera, "clicked", G_CALLBACK(start_camera), self); |
| g_signal_connect_swapped(priv->button_choose_picture, "clicked", G_CALLBACK(choose_picture), self); |
| g_signal_connect_swapped(priv->button_take_photo, "clicked", G_CALLBACK(take_a_photo), self); |
| g_signal_connect_swapped(priv->button_return_photo, "clicked", G_CALLBACK(return_to_previous), self); |
| g_signal_connect_swapped(priv->button_set_avatar, "clicked", G_CALLBACK(set_avatar), self); |
| g_signal_connect_swapped(priv->button_return_edit, "clicked", G_CALLBACK(return_to_previous), self); |
| |
| set_state(self, AVATAR_MANIPULATION_STATE_CURRENT); |
| |
| gtk_widget_show_all(priv->stack_views); |
| } |
| |
| static void |
| set_state(AvatarManipulation *self, AvatarManipulationState state) |
| { |
| // note: this function does not check if the state transition is valid, this is assumed to have |
| // been done by the caller |
| AvatarManipulationPrivate *priv = AVATAR_MANIPULATION_GET_PRIVATE(self); |
| |
| // save prev state |
| priv->last_state = priv->state; |
| |
| switch (state) { |
| case AVATAR_MANIPULATION_STATE_CURRENT: |
| { |
| /* get the current or default profile avatar */ |
| auto default_avatar = Interfaces::PixbufManipulator().generateAvatar("", ""); |
| auto default_scaled = Interfaces::PixbufManipulator().scaleAndFrame(default_avatar.get(), QSize(AVATAR_WIDTH, AVATAR_HEIGHT)); |
| auto photo = default_scaled; |
| if ((priv->accountInfo_ && (*priv->accountInfo_)) || priv->temporaryAvatar) { |
| auto photostr = priv->temporaryAvatar? priv->temporaryAvatar : (*priv->accountInfo_)->profileInfo.avatar; |
| QByteArray byteArray(photostr.c_str(), photostr.length()); |
| QVariant avatar = Interfaces::PixbufManipulator().personPhoto(byteArray); |
| if (avatar.isValid()) { |
| auto size = QSize(AVATAR_WIDTH, AVATAR_HEIGHT); |
| photo = Interfaces::PixbufManipulator().scaleAndFrame(avatar.value<std::shared_ptr<GdkPixbuf>>().get(), size); |
| } |
| } |
| gtk_image_set_from_pixbuf(GTK_IMAGE(priv->image_avatar), photo.get()); |
| |
| gtk_stack_set_visible_child_name(GTK_STACK(priv->stack_views), "page_avatar"); |
| |
| /* available actions: start camera (if available) or choose image */ |
| if (Video::DeviceModel::instance().rowCount() > 0) { |
| // TODO: update if a video device gets inserted while in this state |
| gtk_widget_set_visible(priv->button_start_camera, true); |
| } |
| gtk_widget_set_visible(priv->button_box_current, true); |
| gtk_widget_set_visible(priv->button_box_photo, false); |
| gtk_widget_set_visible(priv->button_box_edit, false); |
| |
| /* make sure video widget and camera is not running */ |
| if (priv->video_started_by_avatar_manipulation) |
| Video::PreviewManager::instance().stopPreview(); |
| if (priv->video_widget) { |
| gtk_container_remove(GTK_CONTAINER(priv->frame_video), priv->video_widget); |
| priv->video_widget = NULL; |
| } |
| } |
| break; |
| case AVATAR_MANIPULATION_STATE_PHOTO: |
| { |
| // start the video; if its not available we should not be in this state |
| priv->video_widget = video_widget_new(); |
| g_signal_connect_swapped(priv->video_widget, "snapshot-taken", G_CALLBACK (got_snapshot), self); |
| gtk_widget_set_vexpand_set(priv->video_widget, FALSE); |
| gtk_widget_set_hexpand_set(priv->video_widget, FALSE); |
| gtk_container_add(GTK_CONTAINER(priv->frame_video), priv->video_widget); |
| gtk_widget_set_visible(priv->video_widget, true); |
| gtk_stack_set_visible_child_name(GTK_STACK(priv->stack_views), "page_photobooth"); |
| |
| |
| /* local renderer, but set as "remote" so that it takes up the whole screen */ |
| video_widget_push_new_renderer(VIDEO_WIDGET(priv->video_widget), |
| Video::PreviewManager::instance().previewRenderer(), |
| VIDEO_RENDERER_REMOTE); |
| |
| if (!Video::PreviewManager::instance().isPreviewing()) { |
| priv->video_started_by_avatar_manipulation = TRUE; |
| Video::PreviewManager::instance().startPreview(); |
| } else { |
| priv->video_started_by_avatar_manipulation = FALSE; |
| } |
| |
| /* available actions: take snapshot, return*/ |
| gtk_widget_set_visible(priv->button_box_current, false); |
| gtk_widget_set_visible(priv->button_box_photo, true); |
| gtk_widget_set_visible(priv->button_box_edit, false); |
| } |
| break; |
| case AVATAR_MANIPULATION_STATE_EDIT: |
| { |
| /* make sure video widget and camera is not running */ |
| if (priv->video_started_by_avatar_manipulation) |
| Video::PreviewManager::instance().stopPreview(); |
| if (priv->video_widget) { |
| gtk_container_remove(GTK_CONTAINER(priv->frame_video), priv->video_widget); |
| priv->video_widget = NULL; |
| } |
| |
| /* available actions: set avatar, return */ |
| gtk_widget_set_visible(priv->button_box_current, false); |
| gtk_widget_set_visible(priv->button_box_photo, false); |
| gtk_widget_set_visible(priv->button_box_edit, true); |
| |
| gtk_stack_set_visible_child_name(GTK_STACK(priv->stack_views), "page_edit_view"); |
| } |
| break; |
| } |
| |
| priv->state = state; |
| } |
| |
| static void |
| start_camera(AvatarManipulation *self) |
| { |
| set_state(self, AVATAR_MANIPULATION_STATE_PHOTO); |
| } |
| |
| static void |
| take_a_photo(AvatarManipulation *self) |
| { |
| AvatarManipulationPrivate *priv = AVATAR_MANIPULATION_GET_PRIVATE(self); |
| video_widget_take_snapshot(VIDEO_WIDGET(priv->video_widget)); |
| } |
| |
| static void |
| set_avatar(AvatarManipulation *self) |
| { |
| AvatarManipulationPrivate *priv = AVATAR_MANIPULATION_GET_PRIVATE(self); |
| |
| gchar* png_buffer_signed = nullptr; |
| gsize png_buffer_size; |
| GError* error = nullptr; |
| |
| /* get the cropped area */ |
| GdkPixbuf *selector_pixbuf = cc_crop_area_get_picture(CC_CROP_AREA(priv->crop_area)); |
| |
| /* scale it */ |
| GdkPixbuf* pixbuf_frame_resized = gdk_pixbuf_scale_simple(selector_pixbuf, AVATAR_WIDTH, AVATAR_HEIGHT, |
| GDK_INTERP_HYPER); |
| |
| /* save the png in memory */ |
| gdk_pixbuf_save_to_buffer(pixbuf_frame_resized, &png_buffer_signed, &png_buffer_size, "png", &error, NULL); |
| if (!png_buffer_signed) { |
| g_warning("(set_avatar) failed to save pixbuffer to png: %s\n", error->message); |
| g_error_free(error); |
| return; |
| } |
| |
| /* convert buffer to QByteArray in base 64*/ |
| QByteArray png_q_byte_array = QByteArray::fromRawData(png_buffer_signed, png_buffer_size).toBase64(); |
| |
| /* save in profile */ |
| if (priv->accountInfo_ && (*priv->accountInfo_)) { |
| try { |
| (*priv->accountInfo_)->accountModel->setAvatar((*priv->accountInfo_)->id, png_q_byte_array.toStdString()); |
| } catch (std::out_of_range&) { |
| g_warning("Can't set avatar for unknown account"); |
| } |
| } else { |
| priv->temporaryAvatar = g_strdup(png_q_byte_array.toStdString().c_str()); |
| } |
| |
| g_free(png_buffer_signed); |
| g_object_unref(selector_pixbuf); |
| g_object_unref(pixbuf_frame_resized); |
| |
| set_state(self, AVATAR_MANIPULATION_STATE_CURRENT); |
| } |
| |
| static void |
| return_to_previous(AvatarManipulation *self) |
| { |
| AvatarManipulationPrivate *priv = AVATAR_MANIPULATION_GET_PRIVATE(self); |
| |
| if (priv->state == AVATAR_MANIPULATION_STATE_PHOTO) { |
| // from photo we alway go back to current |
| set_state(self, AVATAR_MANIPULATION_STATE_CURRENT); |
| } else { |
| // otherwise, if we were in edit state, we may have come from photo or current state |
| set_state(self, priv->last_state); |
| } |
| } |
| |
| static void |
| choose_picture(AvatarManipulation *self) |
| { |
| AvatarManipulationPrivate *priv = AVATAR_MANIPULATION_GET_PRIVATE(self); |
| GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN; |
| gint res; |
| |
| auto preview = gtk_image_new(); |
| |
| GtkWidget *ring_main_window = gtk_widget_get_toplevel(GTK_WIDGET(self)); |
| |
| auto dialog = gtk_file_chooser_dialog_new (_("Open Avatar Image"), |
| GTK_WINDOW(ring_main_window), |
| action, |
| _("_Cancel"), |
| GTK_RESPONSE_CANCEL, |
| _("_Open"), |
| GTK_RESPONSE_ACCEPT, |
| NULL); |
| |
| /* add an image preview inside the file choose */ |
| gtk_file_chooser_set_preview_widget (GTK_FILE_CHOOSER(dialog), preview); |
| g_signal_connect (GTK_FILE_CHOOSER(dialog), "update-preview", G_CALLBACK (update_preview_cb), preview); |
| |
| /* start the file chooser */ |
| res = gtk_dialog_run (GTK_DIALOG(dialog)); /* blocks until the dialog is closed */ |
| |
| if (res == GTK_RESPONSE_ACCEPT) { |
| if(auto filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER (dialog))) { |
| GError* error = nullptr; /* initialising to null avoid trouble... */ |
| |
| auto picture = gdk_pixbuf_new_from_file_at_size (filename, VIDEO_WIDTH, VIDEO_HEIGHT, &error); |
| |
| if (!error) { |
| /* reset crop area */ |
| if (priv->crop_area) |
| gtk_container_remove(GTK_CONTAINER(priv->vbox_crop_area), priv->crop_area); |
| priv->crop_area = cc_crop_area_new(); |
| gtk_widget_show(priv->crop_area); |
| gtk_box_pack_start(GTK_BOX(priv->vbox_crop_area), priv->crop_area, TRUE, TRUE, 0); |
| cc_crop_area_set_picture(CC_CROP_AREA(priv->crop_area), picture); |
| g_object_unref(picture); |
| |
| set_state(self, AVATAR_MANIPULATION_STATE_EDIT); |
| } else { |
| g_warning("(choose_picture) failed to load pixbuf from file: %s", error->message); |
| g_error_free(error); |
| } |
| |
| g_free(filename); |
| } else { |
| g_warning("(choose_picture) filename empty"); |
| } |
| } |
| |
| gtk_widget_destroy(dialog); |
| } |
| |
| static void |
| update_preview_cb(GtkFileChooser *file_chooser, GtkWidget *preview) |
| { |
| gboolean have_preview = FALSE; |
| if (auto filename = gtk_file_chooser_get_preview_filename(file_chooser)) { |
| GError* error = nullptr; |
| auto pixbuf = gdk_pixbuf_new_from_file_at_size (filename, 128, 128, &error); |
| if (!error) { |
| gtk_image_set_from_pixbuf(GTK_IMAGE(preview), pixbuf); |
| g_object_unref(pixbuf); |
| have_preview = TRUE; |
| } else { |
| // nothing to do, the file is probably not a picture |
| } |
| g_free (filename); |
| } |
| gtk_file_chooser_set_preview_widget_active(file_chooser, have_preview); |
| } |
| |
| static void |
| got_snapshot(AvatarManipulation *self) |
| { |
| AvatarManipulationPrivate *priv = AVATAR_MANIPULATION_GET_PRIVATE(self); |
| GdkPixbuf* pix = video_widget_get_snapshot(VIDEO_WIDGET(priv->video_widget)); |
| |
| if (priv->crop_area) |
| gtk_container_remove(GTK_CONTAINER(priv->vbox_crop_area), priv->crop_area); |
| priv->crop_area = cc_crop_area_new(); |
| gtk_widget_show(priv->crop_area); |
| gtk_box_pack_start(GTK_BOX(priv->vbox_crop_area), priv->crop_area, TRUE, TRUE, 0); |
| cc_crop_area_set_picture(CC_CROP_AREA(priv->crop_area), pix); |
| |
| set_state(self, AVATAR_MANIPULATION_STATE_EDIT); |
| } |
| |
| void |
| avatar_manipulation_wizard_completed(AvatarManipulation *self) |
| { |
| auto priv = AVATAR_MANIPULATION_GET_PRIVATE(self); |
| |
| /* Tuleap: #1441 |
| * if the user did not validate the avatar area selection, we still take that as the image |
| * for their avatar; otherwise many users end up with no avatar by default |
| * TODO: improve avatar creation process to not need this fix |
| */ |
| if (priv->state == AVATAR_MANIPULATION_STATE_EDIT) |
| set_avatar(self); |
| } |