blob: fe7c5fec23696120f0750461538f47a6d04796be [file] [log] [blame]
/*
* Copyright (C) 2016-2017 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 <globalinstances.h>
#include <person.h>
#include <profile.h>
#include <profilemodel.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 = 100; /* px */
static constexpr int AVATAR_HEIGHT = 100; /* px */
/* size of video widget */
static constexpr int VIDEO_WIDTH = 300; /* px */
static constexpr int VIDEO_HEIGHT = 200; /* px */
struct _AvatarManipulation
{
GtkBox parent;
};
struct _AvatarManipulationClass
{
GtkBoxClass parent_class;
};
typedef struct _AvatarManipulationPrivate AvatarManipulationPrivate;
struct _AvatarManipulationPrivate
{
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(void)
{
// a profile must exist
g_return_val_if_fail(ProfileModel::instance().selectedProfile(), NULL);
return (GtkWidget *)g_object_new(AVATAR_MANIPULATION_TYPE, NULL);
}
GtkWidget*
avatar_manipulation_new_from_wizard(void)
{
auto self = avatar_manipulation_new();
/* in this mode, we want to automatically go to the PHOTO avatar state, unless one already exists */
if (!ProfileModel::instance().selectedProfile()->person()->photo().isValid()) {
// check if there is a camera
if (Video::DeviceModel::instance().rowCount() > 0)
set_state(AVATAR_MANIPULATION(self), AVATAR_MANIPULATION_STATE_PHOTO);
}
return self;
}
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 funciton 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 photo = GlobalInstances::pixmapManipulator().contactPhoto(
ProfileModel::instance().selectedProfile()->person(),
QSize(AVATAR_WIDTH, AVATAR_HEIGHT),
false);
std::shared_ptr<GdkPixbuf> pixbuf_photo = photo.value<std::shared_ptr<GdkPixbuf>>();
if (photo.isValid()) {
gtk_image_set_from_pixbuf (GTK_IMAGE(priv->image_avatar), pixbuf_photo.get());
} else {
g_warning("invlid pixbuf");
}
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 */
QVariant photo = GlobalInstances::pixmapManipulator().personPhoto(png_q_byte_array);
ProfileModel::instance().selectedProfile()->person()->setPhoto(photo);
ProfileModel::instance().selectedProfile()->save();
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);
}