avatarmanipulation.cpp 19.8 KB
Newer Older
1
/*
2
 *  Copyright (C) 2016-2019 Savoir-faire Linux Inc.
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
 *  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 */
24
#include <api/newaccountmodel.h>
25
#include <api/avmodel.h>
26
#include <globalinstances.h>
27
#include <QSize>
28 29 30 31

/* client */
#include "native/pixbufmanipulator.h"
#include "video/video_widget.h"
32
#include "cc-crop-area.h"
33 34 35 36 37

/* system */
#include <glib/gi18n.h>

/* size of avatar */
38 39
static constexpr int AVATAR_WIDTH  = 150; /* px */
static constexpr int AVATAR_HEIGHT = 150; /* px */
40 41

/* size of video widget */
42 43
static constexpr int VIDEO_WIDTH = 150; /* px */
static constexpr int VIDEO_HEIGHT = 150; /* px */
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58

struct _AvatarManipulation
{
    GtkBox parent;
};

struct _AvatarManipulationClass
{
    GtkBoxClass parent_class;
};

typedef struct _AvatarManipulationPrivate AvatarManipulationPrivate;

struct _AvatarManipulationPrivate
{
59 60 61
    AccountInfoPointer const *accountInfo_ = nullptr;
    gchar* temporaryAvatar = nullptr;

62 63 64 65
    GtkWidget *stack_avatar_manipulation;
    GtkWidget *video_widget;
    GtkWidget *box_views_and_controls;
    GtkWidget *box_controls;
66 67 68 69 70 71

    GtkWidget *button_box_current;
    GtkWidget *button_box_photo;
    GtkWidget *button_box_edit;

    GtkWidget *button_start_camera;
72
    GtkWidget *button_choose_picture;
73 74
    GtkWidget *button_take_photo;
    GtkWidget *button_return_photo;
75
    GtkWidget *button_set_avatar;
76 77
    GtkWidget *button_return_edit;

78
    // GtkWidget *selector_widget;
79 80
    GtkWidget *stack_views;
    GtkWidget *image_avatar;
81
    GtkWidget *vbox_crop_area;
82
    GtkWidget *frame_video;
83 84

    AvatarManipulationState state;
85 86 87 88 89 90 91
    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;
92 93

    GtkWidget *crop_area;
94 95

    lrc::api::AVModel* avModel_;
96 97 98 99 100 101 102 103 104
};

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);

105
static void start_camera(AvatarManipulation *self);
106 107
static void take_a_photo(AvatarManipulation *self);
static void choose_picture(AvatarManipulation *self);
108
static void return_to_previous(AvatarManipulation *self);
109 110 111 112 113 114 115 116 117 118
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 */
119
    if (priv->video_started_by_avatar_manipulation)
120
        priv->avModel_->stopPreview();
121
    if (priv->video_widget) {
122
        gtk_container_remove(GTK_CONTAINER(priv->frame_video), priv->video_widget);
123 124 125 126 127 128 129 130 131 132 133 134 135
        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*
136
avatar_manipulation_new(AccountInfoPointer const & accountInfo, lrc::api::AVModel* avModel)
137 138
{
    // a profile must exist
139 140 141 142
    gpointer view = g_object_new(AVATAR_MANIPULATION_TYPE, NULL);

    auto* priv = AVATAR_MANIPULATION_GET_PRIVATE(view);
    priv->accountInfo_ = &accountInfo;
143
    priv->avModel_ = avModel;
144

145 146 147
    set_state(AVATAR_MANIPULATION(view), AVATAR_MANIPULATION_STATE_CURRENT);

    return reinterpret_cast<GtkWidget*>(view);
148 149 150
}

GtkWidget*
151
avatar_manipulation_new_from_wizard(lrc::api::AVModel* avModel)
152
{
153 154
    // a profile must exist
    gpointer view = g_object_new(AVATAR_MANIPULATION_TYPE, NULL);
155

156 157
    auto* priv = AVATAR_MANIPULATION_GET_PRIVATE(view);
    priv->accountInfo_ = nullptr;
158
    priv->avModel_ = avModel;
159 160

    set_state(AVATAR_MANIPULATION(view), AVATAR_MANIPULATION_STATE_CURRENT);
161

162 163 164 165 166 167 168 169 170
    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;
171 172 173 174 175 176 177 178
}

static void
avatar_manipulation_class_init(AvatarManipulationClass *klass)
{
    G_OBJECT_CLASS(klass)->finalize = avatar_manipulation_finalize;
    G_OBJECT_CLASS(klass)->dispose = avatar_manipulation_dispose;

179
    gtk_widget_class_set_template_from_resource(GTK_WIDGET_CLASS (klass), "/net/jami/JamiGnome/avatarmanipulation.ui");
180 181 182

    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);
183 184
    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);
185
    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, button_take_photo);
186
    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, button_return_photo);
187
    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, button_set_avatar);
188
    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, button_return_edit);
189 190
    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);
191
    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, frame_video);
192
    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AvatarManipulation, vbox_crop_area);
193 194 195
    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);
196 197 198 199 200 201 202 203 204 205 206 207
}

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 */
208
    g_signal_connect_swapped(priv->button_start_camera, "clicked", G_CALLBACK(start_camera), self);
209 210
    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);
211
    g_signal_connect_swapped(priv->button_return_photo, "clicked", G_CALLBACK(return_to_previous), self);
212
    g_signal_connect_swapped(priv->button_set_avatar, "clicked", G_CALLBACK(set_avatar), self);
213
    g_signal_connect_swapped(priv->button_return_edit, "clicked", G_CALLBACK(return_to_previous), self);
214 215 216 217 218 219 220 221 222

    set_state(self, AVATAR_MANIPULATION_STATE_CURRENT);

    gtk_widget_show_all(priv->stack_views);
}

static void
set_state(AvatarManipulation *self, AvatarManipulationState state)
{
Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
223
    // note: this function does not check if the state transition is valid, this is assumed to have
224 225 226
    // been done by the caller
    AvatarManipulationPrivate *priv = AVATAR_MANIPULATION_GET_PRIVATE(self);

227 228 229
    // save prev state
    priv->last_state = priv->state;

230 231 232 233
    switch (state) {
        case AVATAR_MANIPULATION_STATE_CURRENT:
        {
            /* get the current or default profile avatar */
234 235 236 237 238 239 240 241
            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()) {
242 243
                    auto size = QSize(AVATAR_WIDTH, AVATAR_HEIGHT);
                    photo = Interfaces::PixbufManipulator().scaleAndFrame(avatar.value<std::shared_ptr<GdkPixbuf>>().get(), size);
244
                }
245
            }
246
            gtk_image_set_from_pixbuf(GTK_IMAGE(priv->image_avatar), photo.get());
247 248 249

            gtk_stack_set_visible_child_name(GTK_STACK(priv->stack_views), "page_avatar");

250
            /* available actions: start camera (if available) or choose image */
251
            if (priv->avModel_->getDevices().size() > 0) {
252 253 254 255 256 257
                // 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);
258

259 260
            /* make sure video widget and camera is not running */
            if (priv->video_started_by_avatar_manipulation)
261
                priv->avModel_->stopPreview();
262
            if (priv->video_widget) {
263
                gtk_container_remove(GTK_CONTAINER(priv->frame_video), priv->video_widget);
264 265 266 267
                priv->video_widget = NULL;
            }
        }
        break;
268
        case AVATAR_MANIPULATION_STATE_PHOTO:
269
        {
270 271 272 273 274
            // 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);
275
            gtk_container_add(GTK_CONTAINER(priv->frame_video), priv->video_widget);
276 277 278 279
            gtk_widget_set_visible(priv->video_widget, true);
            gtk_stack_set_visible_child_name(GTK_STACK(priv->stack_views), "page_photobooth");


280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
            // local renderer, but set as "remote" so that it takes up the whole screen
            try {
                const lrc::api::video::Renderer* previewRenderer =
                    &priv->avModel_->getRenderer(
                    lrc::api::video::PREVIEW_RENDERER_ID);
                video_widget_add_new_renderer(VIDEO_WIDGET(priv->video_widget),
                    priv->avModel_,
                    previewRenderer, VIDEO_RENDERER_REMOTE);

                if (!previewRenderer->isRendering()) {
                    priv->video_started_by_avatar_manipulation = TRUE;
                    priv->avModel_->startPreview();
                } else {
                    priv->video_started_by_avatar_manipulation = FALSE;
                }
            } catch (const std::out_of_range& e) {
296
                g_warning("Cannot start preview");
297 298
            }

299 300 301 302
            /* 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);
303 304 305 306
        }
        break;
        case AVATAR_MANIPULATION_STATE_EDIT:
        {
307 308
            /* make sure video widget and camera is not running */
            if (priv->video_started_by_avatar_manipulation)
309
                priv->avModel_->stopPreview();
310
            if (priv->video_widget) {
311
                gtk_container_remove(GTK_CONTAINER(priv->frame_video), priv->video_widget);
312 313 314
                priv->video_widget = NULL;
            }

315 316 317 318
            /* 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);
319 320 321 322 323 324 325 326 327 328

            gtk_stack_set_visible_child_name(GTK_STACK(priv->stack_views), "page_edit_view");
        }
        break;
    }

    priv->state = state;
}

static void
329
start_camera(AvatarManipulation *self)
330
{
331
    set_state(self, AVATAR_MANIPULATION_STATE_PHOTO);
332 333 334
}

static void
335
take_a_photo(AvatarManipulation *self)
336
{
337 338
    AvatarManipulationPrivate *priv = AVATAR_MANIPULATION_GET_PRIVATE(self);
    video_widget_take_snapshot(VIDEO_WIDGET(priv->video_widget));
339 340 341 342 343 344 345 346 347 348 349
}

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;

350 351
    /* get the cropped area */
    GdkPixbuf *selector_pixbuf = cc_crop_area_get_picture(CC_CROP_AREA(priv->crop_area));
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368

    /* 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 */
369 370 371 372 373 374 375 376 377
    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());
    }
378 379 380 381 382 383 384 385

    g_free(png_buffer_signed);
    g_object_unref(selector_pixbuf);
    g_object_unref(pixbuf_frame_resized);

    set_state(self, AVATAR_MANIPULATION_STATE_CURRENT);
}

386
static void
387
return_to_previous(AvatarManipulation *self)
388 389
{
    AvatarManipulationPrivate *priv = AVATAR_MANIPULATION_GET_PRIVATE(self);
390 391 392 393 394 395 396 397

    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);
    }
398 399
}

400 401 402 403 404 405 406 407 408
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();

409
    GtkWidget *main_window = gtk_widget_get_toplevel(GTK_WIDGET(self));
410 411

    auto dialog = gtk_file_chooser_dialog_new (_("Open Avatar Image"),
412
                                          GTK_WINDOW(main_window),
413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
                                          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... */

431
            auto picture = gdk_pixbuf_new_from_file_at_size (filename, VIDEO_WIDTH, VIDEO_HEIGHT, &error);
432 433

            if (!error) {
434 435 436 437 438 439 440 441 442
                /* 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);

443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482
                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));

483 484 485 486 487
    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);
488
    cc_crop_area_set_picture(CC_CROP_AREA(priv->crop_area), pix);
489 490 491

    set_state(self, AVATAR_MANIPULATION_STATE_EDIT);
}
492 493 494 495 496 497 498 499 500 501 502 503 504 505

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);
}