ring_client.cpp 17.4 KB
Newer Older
1
/*
Sébastien Blin's avatar
Sébastien Blin committed
2
 *  Copyright (C) 2015-2019 Savoir-faire Linux Inc.
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
 *  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 "ring_client.h"

22 23
// system
#include <memory>
24
#include <regex>
25 26

// GTK+ related
27 28
#include <gtk/gtk.h>
#include <glib/gi18n.h>
29 30 31
#include <clutter-gtk/clutter-gtk.h>

// Qt
32
#include <QtCore/QTranslator>
Stepan Salenikovich's avatar
Stepan Salenikovich committed
33 34 35 36
#include <QtCore/QCoreApplication>
#include <QtCore/QString>
#include <QtCore/QByteArray>
#include <QtCore/QItemSelectionModel>
37 38 39
#include <QtCore/QStandardPaths>

// LRC
Olivier Gregoire's avatar
Olivier Gregoire committed
40
#include <smartinfohub.h>
41
#include <globalinstances.h>
42

43
// Ring client
44
#include "ring_client_options.h"
45
#include "ringmainwindow.h"
Stepan Salenikovich's avatar
Stepan Salenikovich committed
46
#include "dialogs.h"
47
#include "native/pixbufmanipulator.h"
48
#include "native/dbuserrorhandler.h"
49
#include "ringnotify.h"
50
#include "config.h"
51
#include "utils/files.h"
52
#include "revision.h"
53

aviau's avatar
aviau committed
54 55 56
#if HAVE_AYATANAAPPINDICATOR
#include <libayatana-appindicator/app-indicator.h>
#elif HAVE_APPINDICATOR
Stepan Salenikovich's avatar
Stepan Salenikovich committed
57 58 59
#include <libappindicator/app-indicator.h>
#endif

60 61
struct _RingClientClass
{
62
    GtkApplicationClass parent_class;
63 64 65 66 67 68 69 70
};

struct _RingClient
{
    GtkApplication parent;
};

typedef struct _RingClientPrivate RingClientPrivate;
71 72

struct _RingClientPrivate {
73 74 75
    /* args */
    int    argc;
    char **argv;
76 77 78

    GSettings *settings;

79 80 81 82
    /* main window */
    GtkWidget        *win;
    /* for libRingclient */
    QCoreApplication *qtapp;
83 84
    /* UAM */
    QMetaObject::Connection uam_updated;
85

86 87
    std::unique_ptr<QTranslator> translator_lang;
    std::unique_ptr<QTranslator> translator_full;
88

89
    gboolean restore_window_state;
Stepan Salenikovich's avatar
Stepan Salenikovich committed
90 91 92

    gpointer systray_icon;
    GtkWidget *icon_menu;
93 94
};

95 96 97 98 99 100 101 102
/* this union is used to pass ints as pointers and vice versa for GAction parameters*/
typedef union _int_ptr_t
{
    int value;
    gint64 value64;
    gpointer ptr;
} int_ptr_t;

103
G_DEFINE_TYPE_WITH_PRIVATE(RingClient, ring_client, GTK_TYPE_APPLICATION);
104 105 106 107

#define RING_CLIENT_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), RING_CLIENT_TYPE, RingClientPrivate))

static void
108
exception_dialog(const char* msg)
109
{
110
    g_critical("%s", msg);
111 112 113
    GtkWidget *dialog = gtk_message_dialog_new(NULL,
                            (GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT),
                            GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE,
Philippe Gorley's avatar
Philippe Gorley committed
114
                            _("Unable to initialize.\nMake sure the Jami daemon (dring) is running.\nError: %s"),
115 116
                            msg);

Philippe Gorley's avatar
Philippe Gorley committed
117
    gtk_window_set_title(GTK_WINDOW(dialog), _("Jami Error"));
118 119 120 121
    gtk_dialog_run(GTK_DIALOG(dialog));
    gtk_widget_destroy(dialog);
}

122 123 124
static void
ring_accelerators(RingClient *client)
{
Stepan Salenikovich's avatar
Stepan Salenikovich committed
125
#if GTK_CHECK_VERSION(3,12,0)
126 127 128
    const gchar *quit_accels[2] = { "<Ctrl>Q", NULL };
    gtk_application_set_accels_for_action(GTK_APPLICATION(client), "app.quit", quit_accels);
#else
129
    gtk_application_add_accelerator(GTK_APPLICATION(client), "<Control>Q", "app.quit", NULL);
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
#endif
}

static void
action_quit(G_GNUC_UNUSED GSimpleAction *simple,
            G_GNUC_UNUSED GVariant      *parameter,
            gpointer user_data)
{
    g_return_if_fail(G_IS_APPLICATION(user_data));

#if GLIB_CHECK_VERSION(2,32,0)
    g_application_quit(G_APPLICATION(user_data));
#else
    RingClientPrivate *priv = RING_CLIENT_GET_PRIVATE(user_data);
    gtk_widget_destroy(priv->win);
#endif
}

Stepan Salenikovich's avatar
Stepan Salenikovich committed
148 149
static void
action_about(G_GNUC_UNUSED GSimpleAction *simple,
Stepan Salenikovich's avatar
Stepan Salenikovich committed
150 151
             G_GNUC_UNUSED GVariant      *parameter,
             gpointer user_data)
Stepan Salenikovich's avatar
Stepan Salenikovich committed
152 153 154 155 156 157 158
{
    g_return_if_fail(G_IS_APPLICATION(user_data));
    RingClientPrivate *priv = RING_CLIENT_GET_PRIVATE(user_data);

    ring_about_dialog(priv->win);
}

Olivier Gregoire's avatar
Olivier Gregoire committed
159 160 161 162 163 164 165 166 167 168 169
static void
toggle_smartinfo(GSimpleAction *action, GVariant *parameter, gpointer)
{
    g_simple_action_set_state(action, parameter);
    if (g_variant_get_boolean(parameter)) {
        SmartInfoHub::instance().start();
    } else {
        SmartInfoHub::instance().stop();
    }
}

170 171
static const GActionEntry ring_actions[] =
{
Olivier Gregoire's avatar
Olivier Gregoire committed
172 173 174 175 176 177 178 179 180
    { "accept",             NULL,         NULL, NULL,    NULL, {0} },
    { "hangup",             NULL,         NULL, NULL,    NULL, {0} },
    { "hold",               NULL,         NULL, "false", NULL, {0} },
    { "quit",               action_quit,  NULL, NULL,    NULL, {0} },
    { "about",              action_about, NULL, NULL,    NULL, {0} },
    { "mute_audio",         NULL,         NULL, "false", NULL, {0} },
    { "mute_video",         NULL,         NULL, "false", NULL, {0} },
    { "record",             NULL,         NULL, "false", NULL, {0} },
    { "display-smartinfo",  NULL,         NULL, "false", toggle_smartinfo, {0} },
181 182 183 184
    /* TODO implement the other actions */
    // { "transfer",   NULL,        NULL, "flase", NULL, {0} },
};

185 186 187 188 189 190
static void
autostart_toggled(GSettings *settings, G_GNUC_UNUSED gchar *key, G_GNUC_UNUSED gpointer user_data)
{
    autostart_symlink(g_settings_get_boolean(settings, "start-on-login"));
}

191
static void
Stepan Salenikovich's avatar
Stepan Salenikovich committed
192
show_main_window_toggled(RingClient *client)
193 194 195
{
    RingClientPrivate *priv = RING_CLIENT_GET_PRIVATE(client);

Stepan Salenikovich's avatar
Stepan Salenikovich committed
196 197 198 199 200 201
    if (g_settings_get_boolean(priv->settings, "show-main-window")) {
        gtk_window_present(GTK_WINDOW(priv->win));
    } else {
        gtk_widget_hide(priv->win);
    }
}
202

Stepan Salenikovich's avatar
Stepan Salenikovich committed
203 204 205 206 207
static void
ring_window_show(RingClient *client)
{
    RingClientPrivate *priv = RING_CLIENT_GET_PRIVATE(client);
    g_settings_set_boolean(priv->settings, "show-main-window", TRUE);
208 209 210 211 212 213
}

static void
ring_window_hide(RingClient *client)
{
    RingClientPrivate *priv = RING_CLIENT_GET_PRIVATE(client);
Stepan Salenikovich's avatar
Stepan Salenikovich committed
214
    g_settings_set_boolean(priv->settings, "show-main-window", FALSE);
215 216
}

217 218 219 220 221 222
static gboolean
on_close_window(GtkWidget *window, G_GNUC_UNUSED GdkEvent *event, RingClient *client)
{
    g_return_val_if_fail(GTK_IS_WINDOW(window) && IS_RING_CLIENT(client), FALSE);
    RingClientPrivate *priv = RING_CLIENT_GET_PRIVATE(client);

Stepan Salenikovich's avatar
Stepan Salenikovich committed
223
    if (g_settings_get_boolean(priv->settings, "show-status-icon")) {
224
        /* we want to simply hide the window and keep the client running */
225 226 227 228 229
        auto closeWindow = ring_main_window_can_close(RING_MAIN_WINDOW(window));
        if (closeWindow) {
            ring_window_hide(client);
            ring_main_window_reset(RING_MAIN_WINDOW(window));
        }
Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
230
        return TRUE; /* do not propagate event */
231
    } else {
Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
232
        /* we want to quit the application, so just propagate the event */
233 234 235 236
        return FALSE;
    }
}

237 238
#if !HAVE_APPINDICATOR

Stepan Salenikovich's avatar
Stepan Salenikovich committed
239 240 241 242 243 244 245 246 247 248 249 250
static void
popup_menu(GtkStatusIcon *self,
           guint          button,
           guint          when,
           RingClient    *client)
{
    RingClientPrivate *priv = RING_CLIENT_GET_PRIVATE(client);
    G_GNUC_BEGIN_IGNORE_DEPRECATIONS // GtkStatusIcon is deprecated since 3.14, but we fallback on it
    gtk_menu_popup(GTK_MENU(priv->icon_menu), NULL, NULL, gtk_status_icon_position_menu, self, button, when);
    G_GNUC_END_IGNORE_DEPRECATIONS
}

251 252
#endif

Stepan Salenikovich's avatar
Stepan Salenikovich committed
253 254 255 256 257 258 259 260
static void
init_systray(RingClient *client)
{
    RingClientPrivate *priv = RING_CLIENT_GET_PRIVATE(client);

    // init menu
    if (!priv->icon_menu) {

261 262 263 264
        /* for some reason AppIndicator doesn't like the menu being built from a GMenuModel and/or
         * the GMenuModel being built from an xml resource. So we build the menu in code.
         */
        priv->icon_menu = gtk_menu_new();
Stepan Salenikovich's avatar
Stepan Salenikovich committed
265
        g_object_ref_sink(priv->icon_menu);
266

Philippe Gorley's avatar
Philippe Gorley committed
267
        auto item = gtk_check_menu_item_new_with_label(C_("In the status icon menu, toggle action to show or hide the Jami main window", "Show Jami"));
268 269 270 271 272 273 274 275 276
        gtk_actionable_set_action_name(GTK_ACTIONABLE(item), "app.show-main-window");
        gtk_menu_shell_append(GTK_MENU_SHELL(priv->icon_menu), item);

        item = gtk_menu_item_new_with_label(_("Quit"));
        gtk_actionable_set_action_name(GTK_ACTIONABLE(item), "app.quit");
        gtk_menu_shell_append(GTK_MENU_SHELL(priv->icon_menu), item);

        gtk_widget_insert_action_group(priv->icon_menu, "app", G_ACTION_GROUP(client));
        gtk_widget_show_all(priv->icon_menu);
Stepan Salenikovich's avatar
Stepan Salenikovich committed
277 278
    }

279
#if HAVE_APPINDICATOR
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
    auto indicator = app_indicator_new("jami", "jami", APP_INDICATOR_CATEGORY_COMMUNICATIONS);
    app_indicator_set_status(indicator, APP_INDICATOR_STATUS_ACTIVE);
    app_indicator_set_title(indicator, "Jami");
    /* app indicator requires a menu */
    app_indicator_set_menu(indicator, GTK_MENU(priv->icon_menu));
    priv->systray_icon = indicator;
#else
    GError *error = NULL;
    GdkPixbuf* icon = gdk_pixbuf_new_from_resource("/net/jami/JamiGnome/jami-symbol-blue", &error);
    if (icon == nullptr) {
        g_debug("Could not load icon: %s", error->message);
        g_clear_error(&error);
    } else {
        G_GNUC_BEGIN_IGNORE_DEPRECATIONS // GtkStatusIcon is deprecated since 3.14, but we fallback on it
        auto status_icon = gtk_status_icon_new_from_pixbuf(icon);
        gtk_status_icon_set_title(status_icon, "jami");
        G_GNUC_END_IGNORE_DEPRECATIONS
        g_signal_connect_swapped(status_icon, "activate", G_CALLBACK(ring_window_show), client);
        g_signal_connect(status_icon, "popup-menu", G_CALLBACK(popup_menu), client);
        priv->systray_icon = status_icon;
300 301
    }
#endif
Stepan Salenikovich's avatar
Stepan Salenikovich committed
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
}

static void
systray_toggled(GSettings *settings, const gchar *key, RingClient *client)
{
    RingClientPrivate *priv = RING_CLIENT_GET_PRIVATE(client);

    if (g_settings_get_boolean(settings, key)) {
        if (!priv->systray_icon)
            init_systray(client);
    } else {
        if (priv->systray_icon)
            g_clear_object(&priv->systray_icon);
    }
}

318 319 320 321 322 323 324
static void
ring_client_activate(GApplication *app)
{
    RingClient *client = RING_CLIENT(app);
    RingClientPrivate *priv = RING_CLIENT_GET_PRIVATE(client);

    if (priv->win == NULL) {
Stepan Salenikovich's avatar
Stepan Salenikovich committed
325
        // activate being called for the first time
326 327 328 329 330 331 332
        priv->win = ring_main_window_new(GTK_APPLICATION(app));

        /* make sure win is set to NULL when the window is destroyed */
        g_object_add_weak_pointer(G_OBJECT(priv->win), (gpointer *)&priv->win);

        /* check if the window should be destoryed or not on close */
        g_signal_connect(priv->win, "delete-event", G_CALLBACK(on_close_window), client);
333

Stepan Salenikovich's avatar
Stepan Salenikovich committed
334 335 336 337
        /* if we didn't launch with the '-r' (--restore-last-window-state) option then force the
         * show-main-window to true */
        if (!priv->restore_window_state)
            ring_window_show(client);
338 339
        show_main_window_toggled(client);
        g_signal_connect_swapped(priv->settings, "changed::show-main-window", G_CALLBACK(show_main_window_toggled), client);
340

Stepan Salenikovich's avatar
Stepan Salenikovich committed
341 342 343 344 345
        // track sys icon state
        g_signal_connect(priv->settings, "changed::show-status-icon", G_CALLBACK(systray_toggled), client);
        systray_toggled(priv->settings, "show-status-icon", client);
    } else {
        // activate not being called for the first time, force showing of main window
346
        ring_window_show(client);
Stepan Salenikovich's avatar
Stepan Salenikovich committed
347
    }
348 349
}

350
// TODO add some args!
351
static void
352
ring_client_open(GApplication *app, GFile ** /*file*/, gint /*arg3*/, const gchar* /*arg4*/)
353 354 355
{
    ring_client_activate(app);

356
    // TODO migrate place call at begining
357 358
}

359 360
static void
ring_client_startup(GApplication *app)
361 362 363 364
{
    RingClient *client = RING_CLIENT(app);
    RingClientPrivate *priv = RING_CLIENT_GET_PRIVATE(client);

365
    g_message("Jami GNOME client version: %s", RING_VERSION);
366 367
    g_message("git ref: %s", RING_CLIENT_REVISION);

368 369 370 371
    /* make sure that the system corresponds to the autostart setting */
    autostart_symlink(g_settings_get_boolean(priv->settings, "start-on-login"));
    g_signal_connect(priv->settings, "changed::start-on-login", G_CALLBACK(autostart_toggled), NULL);

372 373
    /* init clutter */
    int clutter_error;
374 375
    if ((clutter_error = gtk_clutter_init(&priv->argc, &priv->argv)) != CLUTTER_INIT_SUCCESS) {
        g_error("Could not init clutter : %d\n", clutter_error);
Hugo Lefeuvre's avatar
Hugo Lefeuvre committed
376
        exit(1); /* the g_error above should normally cause the application to exit */
377
    }
378 379 380

    /* init libRingClient and make sure its connected to the dbus */
    try {
381
        priv->qtapp = new QCoreApplication(priv->argc, priv->argv);
382
        /* the call model will try to connect to dring via dbus */
383 384 385
    } catch(const char * msg) {
        exception_dialog(msg);
        exit(1);
386
    } catch(QString& msg) {
387 388
        exception_dialog(msg.toLocal8Bit().constData());
        exit(1);
389 390
    }

391
    /* load translations from LRC */
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
    const auto locale_name = QLocale::system().name();
    const auto locale_lang = locale_name.split('_')[0];

    if (locale_name != locale_lang) {
        /* Install language first to have lowest priority */
        priv->translator_lang.reset(new QTranslator);
        if (priv->translator_lang->load(RING_CLIENT_INSTALL "/share/libringclient/translations/lrc_" + locale_lang)) {
            g_debug("installed translations for %s", locale_lang.toUtf8().constData());
            priv->qtapp->installTranslator(priv->translator_lang.get());
        }
    }

    priv->translator_full.reset(new QTranslator);
    if (priv->translator_full->load(RING_CLIENT_INSTALL "/share/libringclient/translations/lrc_" + locale_name)) {
        g_debug("installed translations for %s", locale_name.toUtf8().constData());
    }

    if (not priv->translator_lang and not priv->translator_full) {
410
        g_debug("could not load LRC translations for %s, %s",
411 412
                QLocale::languageToString(QLocale::system().language()).toUtf8().constData(),
                QLocale::countryToString(QLocale::system().country()).toUtf8().constData()
413 414 415
        );
    }

416
    /* init delegates */
417
    GlobalInstances::setPixmapManipulator(std::unique_ptr<Interfaces::PixbufManipulator>(new Interfaces::PixbufManipulator()));
418
    GlobalInstances::setDBusErrorHandler(std::unique_ptr<Interfaces::DBusErrorHandler>(new Interfaces::DBusErrorHandler()));
419

420 421 422 423 424 425 426 427
    /* Override theme since we don't have appropriate icons for a dark them (yet) */
    GtkSettings *gtk_settings = gtk_settings_get_default();
    g_object_set(G_OBJECT(gtk_settings), "gtk-application-prefer-dark-theme",
                 FALSE, NULL);
    /* enable button icons */
    g_object_set(G_OBJECT(gtk_settings), "gtk-button-images",
                 TRUE, NULL);

428 429 430 431
    /* enable sound (for notification) */
    g_object_set(G_OBJECT(gtk_settings), "gtk-enable-event-sounds",
                 TRUE, NULL);

432
    /* add GActions */
433
    g_action_map_add_action_entries(
434 435
        G_ACTION_MAP(app), ring_actions, G_N_ELEMENTS(ring_actions), app);

Stepan Salenikovich's avatar
Stepan Salenikovich committed
436 437 438 439
    /* GActions for settings */
    auto action_window_visible = g_settings_create_action(priv->settings, "show-main-window");
    g_action_map_add_action(G_ACTION_MAP(app), action_window_visible);

440 441
    /* add accelerators */
    ring_accelerators(RING_CLIENT(app));
Stepan Salenikovich's avatar
Stepan Salenikovich committed
442

443
    G_APPLICATION_CLASS(ring_client_parent_class)->startup(app);
444 445
}

446 447 448 449 450 451
static void
ring_client_shutdown(GApplication *app)
{
    RingClient *self = RING_CLIENT(app);
    RingClientPrivate *priv = RING_CLIENT_GET_PRIVATE(self);

452
    gtk_widget_destroy(priv->win);
453

454 455
    QObject::disconnect(priv->uam_updated);

456 457
    /* free the QCoreApplication, which will destroy all libRingClient models
     * and thus send the Unregister signal over dbus to dring */
Nicolas Jager's avatar
Nicolas Jager committed
458 459 460 461
    if (priv->qtapp) {
        delete priv->qtapp;
        priv->qtapp = nullptr;
    }
462

463 464 465
    /* free the copied cmd line args */
    g_strfreev(priv->argv);

466 467
    g_clear_object(&priv->settings);

468 469 470 471 472 473 474 475 476 477 478
    /* Chain up to the parent class */
    G_APPLICATION_CLASS(ring_client_parent_class)->shutdown(app);
}

static void
ring_client_init(RingClient *self)
{
    RingClientPrivate *priv = RING_CLIENT_GET_PRIVATE(self);

    priv->win = NULL;
    priv->qtapp = NULL;
479
    priv->settings = g_settings_new_full(get_ring_schema(), NULL, NULL);
480 481 482

    /* add custom cmd line options */
    ring_client_add_options(G_APPLICATION(self));
483 484 485 486 487
}

static void
ring_client_class_init(RingClientClass *klass)
{
488 489
    G_APPLICATION_CLASS(klass)->startup = ring_client_startup;
    G_APPLICATION_CLASS(klass)->activate = ring_client_activate;
490
    G_APPLICATION_CLASS(klass)->open = ring_client_open;
491 492 493
    G_APPLICATION_CLASS(klass)->shutdown = ring_client_shutdown;
}

494
RingClient*
495
ring_client_new(int argc, char *argv[])
496
{
497
    RingClient *client = (RingClient *)g_object_new(ring_client_get_type(),
498
                                                    "application-id", RING_CLIENT_APP_ID,
499
                                                    "flags", G_APPLICATION_HANDLES_OPEN ,
500 501 502 503 504 505 506 507
                                                    NULL);

    /* copy the cmd line args before they get processed by the GApplication*/
    RingClientPrivate *priv = RING_CLIENT_GET_PRIVATE(client);
    priv->argc = argc;
    priv->argv = g_strdupv((gchar **)argv);

    return client;
508
}
509

510
GtkWindow*
511
ring_client_get_main_window(RingClient *client)
512 513 514 515 516 517
{
    g_return_val_if_fail(IS_RING_CLIENT(client), NULL);
    RingClientPrivate *priv = RING_CLIENT_GET_PRIVATE(client);

    return (GtkWindow *)priv->win;
}
518 519 520 521 522 523 524 525 526

void
ring_client_set_restore_main_window_state(RingClient *client, gboolean restore)
{
    g_return_if_fail(IS_RING_CLIENT(client));
    RingClientPrivate *priv = RING_CLIENT_GET_PRIVATE(client);

    priv->restore_window_state = restore;
}