accountview.cpp 21.5 KB
Newer Older
1
/*
Stepan Salenikovich's avatar
Stepan Salenikovich committed
2
 *  Copyright (C) 2015-2016 Savoir-faire Linux Inc.
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
 *  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 "accountview.h"

#include <gtk/gtk.h>
23
#include <glib/gi18n.h>
24 25
#include <accountmodel.h>
#include <audio/codecmodel.h>
26 27
#include <protocolmodel.h>
#include <QtCore/QItemSelectionModel>
28
#include "models/gtkqtreemodel.h"
29 30
#include "models/gtkqsortfiltertreemodel.h"
#include "models/activeitemproxymodel.h"
31 32 33
#include "accountgeneraltab.h"
#include "accountaudiotab.h"
#include "accountvideotab.h"
Stepan Salenikovich's avatar
Stepan Salenikovich committed
34
#include "accountadvancedtab.h"
35
#include "accountsecuritytab.h"
36
#include "dialogs.h"
37
#include <glib/gprintf.h>
38
#include "utils/models.h"
39 40 41

struct _AccountView
{
42
    GtkPaned parent;
43 44 45 46
};

struct _AccountViewClass
{
47
    GtkPanedClass parent_class;
48 49 50 51 52 53 54 55 56
};

typedef struct _AccountViewPrivate AccountViewPrivate;

struct _AccountViewPrivate
{
    GtkWidget *treeview_account_list;
    GtkWidget *stack_account;
    GtkWidget *current_account_notebook;
57 58 59
    GtkWidget *button_remove_account;
    GtkWidget *button_add_account;
    GtkWidget *combobox_account_type;
60

61
    gint current_page; /* keeps track of current notebook page displayed */
62 63

    ActiveItemProxyModel *active_protocols;
64
    QMetaObject::Connection protocol_selection_changed;
65 66
};

67
G_DEFINE_TYPE_WITH_PRIVATE(AccountView, account_view, GTK_TYPE_PANED);
68 69 70 71 72 73 74 75 76

#define ACCOUNT_VIEW_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), ACCOUNT_VIEW_TYPE, AccountViewPrivate))

static void
account_view_dispose(GObject *object)
{
    AccountView *view = ACCOUNT_VIEW(object);
    AccountViewPrivate *priv = ACCOUNT_VIEW_GET_PRIVATE(view);

77
    QObject::disconnect(priv->protocol_selection_changed);
78 79 80 81

    G_OBJECT_CLASS(account_view_parent_class)->dispose(object);
}

82 83 84 85 86 87 88 89 90 91 92
static void
account_view_finalize(GObject *object)
{
    AccountView *view = ACCOUNT_VIEW(object);
    AccountViewPrivate *priv = ACCOUNT_VIEW_GET_PRIVATE(view);

    delete priv->active_protocols;

    G_OBJECT_CLASS(account_view_parent_class)->finalize(object);
}

93 94 95 96 97
static void
update_account_model_selection(GtkTreeSelection *selection, G_GNUC_UNUSED gpointer user_data)
{
    QModelIndex current = get_index_from_selection(selection);
    if (current.isValid())
98
        AccountModel::instance().selectionModel()->setCurrentIndex(current, QItemSelectionModel::ClearAndSelect);
99
    else
100
        AccountModel::instance().selectionModel()->clearCurrentIndex();
101 102
}

103 104 105 106 107 108 109 110
static GtkWidget *
create_scrolled_account_view(GtkWidget *account_view)
{
    auto scrolled = gtk_scrolled_window_new(NULL, NULL);
    gtk_container_add(GTK_CONTAINER(scrolled), account_view);
    return scrolled;
}

111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
static void
account_selection_changed(GtkTreeSelection *selection, AccountView *view)
{
    g_return_if_fail(IS_ACCOUNT_VIEW(view));
    AccountViewPrivate *priv = ACCOUNT_VIEW_GET_PRIVATE(view);

    GtkWidget *old_account_view = gtk_stack_get_visible_child(GTK_STACK(priv->stack_account));

    /* keep track of the last tab displayed */
    if (priv->current_account_notebook)
        priv->current_page = gtk_notebook_get_current_page(GTK_NOTEBOOK(priv->current_account_notebook));

    if (priv->current_page < 0)
        priv->current_page = 0;

    QModelIndex account_idx = get_index_from_selection(selection);
    if (!account_idx.isValid()) {
        /* it nothing is slected, simply display something empty */
        GtkWidget *empty_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
        gtk_widget_show(empty_box);
        gtk_stack_add_named(GTK_STACK(priv->stack_account), empty_box, "placeholder");
        gtk_stack_set_visible_child(GTK_STACK(priv->stack_account), empty_box);
        priv->current_account_notebook = NULL;
    } else {
135
        Account *account = AccountModel::instance().getAccountByModelIndex(account_idx);
136 137 138 139 140 141

        /* build new account view */
        GtkWidget *hbox_account = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);

        /* create account notebook */
        priv->current_account_notebook = gtk_notebook_new();
142 143
        gtk_notebook_set_scrollable(GTK_NOTEBOOK(priv->current_account_notebook), TRUE);
        gtk_notebook_set_show_border(GTK_NOTEBOOK(priv->current_account_notebook), FALSE);
144 145 146
        gtk_box_pack_start(GTK_BOX(hbox_account), priv->current_account_notebook, TRUE, TRUE, 0);

        /* customize account view based on account */
147
        auto general_tab = create_scrolled_account_view(account_general_tab_new(account));
148 149
        gtk_notebook_append_page(GTK_NOTEBOOK(priv->current_account_notebook),
                                 general_tab,
150
                                 gtk_label_new(C_("Account settings", "General")));
151
        auto audio_tab = create_scrolled_account_view(account_audio_tab_new(account));
152 153
        gtk_notebook_append_page(GTK_NOTEBOOK(priv->current_account_notebook),
                                 audio_tab,
154
                                 gtk_label_new(C_("Account settings", "Audio")));
155
        auto video_tab = create_scrolled_account_view(account_video_tab_new(account));
156 157
        gtk_notebook_append_page(GTK_NOTEBOOK(priv->current_account_notebook),
                                 video_tab,
158
                                 gtk_label_new(C_("Account settings", "Video")));
159
        auto advanced_tab = create_scrolled_account_view(account_advanced_tab_new(account));
Stepan Salenikovich's avatar
Stepan Salenikovich committed
160 161
        gtk_notebook_append_page(GTK_NOTEBOOK(priv->current_account_notebook),
                                 advanced_tab,
162
                                 gtk_label_new(C_("Account settings", "Advanced")));
163
        auto security_tab = create_scrolled_account_view(account_security_tab_new(account));
164 165
        gtk_notebook_append_page(GTK_NOTEBOOK(priv->current_account_notebook),
                                 security_tab,
166
                                 gtk_label_new(C_("Account settings", "Security")));
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204

        /* set the tab displayed to the same as the prev account selected */
        gtk_notebook_set_current_page(GTK_NOTEBOOK(priv->current_account_notebook), priv->current_page);

        gtk_widget_show_all(hbox_account);

        /* set the new account view as visible */
        char *account_view_name = g_strdup_printf("%p_account", account);
        gtk_stack_add_named(GTK_STACK(priv->stack_account), hbox_account, account_view_name);
        gtk_stack_set_visible_child(GTK_STACK(priv->stack_account), hbox_account);
        g_free(account_view_name);
    }

    /* remove the old account view */
    if (old_account_view)
        gtk_container_remove(GTK_CONTAINER(priv->stack_account), old_account_view);
}

static void
account_active_toggled(GtkCellRendererToggle *renderer, gchar *path, AccountView *view)
{
    g_return_if_fail(IS_ACCOUNT_VIEW(view));
    AccountViewPrivate *priv = ACCOUNT_VIEW_GET_PRIVATE(view);

    /* we want to set it to the opposite of the current value */
    gboolean toggle = !gtk_cell_renderer_toggle_get_active(renderer);

    /* get iter which was clicked */
    GtkTreePath *tree_path = gtk_tree_path_new_from_string(path);
    GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(priv->treeview_account_list));
    GtkTreeIter iter;
    gtk_tree_model_get_iter(model, &iter, tree_path);

    /* get qmodelindex from iter and set the model data */
    QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(model), &iter);
    if (idx.isValid()) {
        /* check if it is the IP2IP account, as we don't want to be able to disable it */
        QVariant alias = idx.data(static_cast<int>(Account::Role::Alias));
205
        if (strcmp(alias.value<QString>().toLocal8Bit().constData(), "IP2IP") != 0) {
206
            AccountModel::instance().setData(idx, QVariant(toggle), Qt::CheckStateRole);
207
            /* save the account to apply the changed state right away */
208
            AccountModel::instance().getAccountByModelIndex(idx)->performAction(Account::EditAction::SAVE);
209
        }
210 211 212
    }
}

213
static gboolean
214
remove_account_dialog(AccountView *view, Account *account)
215 216 217 218 219
{
    gboolean response = FALSE;
    GtkWidget *dialog = gtk_message_dialog_new(NULL,
                            (GtkDialogFlags)(GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT),
                            GTK_MESSAGE_QUESTION, GTK_BUTTONS_OK_CANCEL,
220
                            _("Are you sure you want to delete account \"%s\"?"),
221 222
                            account->alias().toLocal8Bit().constData());

223 224 225 226 227 228 229 230 231
    gtk_window_set_destroy_with_parent(GTK_WINDOW(dialog), TRUE);

    /* get parent window so we can center on it */
    GtkWidget *parent = gtk_widget_get_toplevel(GTK_WIDGET(view));
    if (gtk_widget_is_toplevel(parent)) {
        gtk_window_set_transient_for(GTK_WINDOW(dialog), GTK_WINDOW(parent));
        gtk_window_set_position(GTK_WINDOW(dialog), GTK_WIN_POS_CENTER_ON_PARENT);
    }

232 233 234 235 236 237 238 239 240 241 242 243 244 245
    switch (gtk_dialog_run(GTK_DIALOG(dialog))) {
        case GTK_RESPONSE_OK:
            response = TRUE;
            break;
        default:
            response = FALSE;
            break;
    }

    gtk_widget_destroy(dialog);

    return response;
}

246 247 248
static gboolean
save_account(GtkWidget *working_dialog)
{
249
    AccountModel::instance().save();
250 251 252 253 254 255
    if (working_dialog)
        gtk_widget_destroy(working_dialog);

    return G_SOURCE_REMOVE;
}

256 257 258 259 260 261 262 263 264 265 266
static void
remove_account(G_GNUC_UNUSED GtkWidget *entry, AccountView *view)
{
    g_return_if_fail(IS_ACCOUNT_VIEW(view));
    AccountViewPrivate *priv = ACCOUNT_VIEW_GET_PRIVATE(view);

    GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(priv->treeview_account_list));
    QModelIndex idx = get_index_from_selection(selection);

    if (idx.isValid()) {
        /* this is a destructive operation, ask the user if they are sure */
267
        Account *account = AccountModel::instance().getAccountByModelIndex(idx);
268 269 270 271 272
        if (remove_account_dialog(view, account)) {
            /* show working dialog in case save operation takes time */
            GtkWidget *working = ring_dialog_working(GTK_WIDGET(view), NULL);
            gtk_window_present(GTK_WINDOW(working));

273
            AccountModel::instance().remove(idx);
274 275 276 277 278

            /* now save the time it takes to transition the account view to the new account (300ms)
             * the save doesn't happen before the "working" dialog is presented
             * the timeout function should destroy the "working" dialog when done saving
             */
279
            g_timeout_add_full(G_PRIORITY_DEFAULT, 300, (GSourceFunc)save_account, working, NULL);
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
        }
    }
}

static void
add_account(G_GNUC_UNUSED GtkWidget *entry, AccountView *view)
{
    g_return_if_fail(IS_ACCOUNT_VIEW(view));
    AccountViewPrivate *priv = ACCOUNT_VIEW_GET_PRIVATE(view);

    GtkTreeIter protocol_iter;
    if (gtk_combo_box_get_active_iter(GTK_COMBO_BOX(priv->combobox_account_type), &protocol_iter)) {
        /* get the qmodelindex of the protocol */
        GtkTreeModel *protocol_model = gtk_combo_box_get_model(GTK_COMBO_BOX(priv->combobox_account_type));
        QModelIndex protocol_idx = gtk_q_sort_filter_tree_model_get_source_idx(
                                    GTK_Q_SORT_FILTER_TREE_MODEL(protocol_model),
                                    &protocol_iter);
        if (protocol_idx.isValid()) {
            protocol_idx = priv->active_protocols->mapToSource(protocol_idx);
299 300 301 302 303

            /* show working dialog in case save operation takes time */
            GtkWidget *working = ring_dialog_working(GTK_WIDGET(view), NULL);
            gtk_window_present(GTK_WINDOW(working));

304
            auto account = AccountModel::instance().add(QString(_("New Account")), protocol_idx);
305
            if (account->protocol() == Account::Protocol::RING)
306
                account->setDisplayName(_("New Account"));
307 308 309 310 311

            /* now save after a short timeout to make sure that
             * the save doesn't happen before the "working" dialog is presented
             * the timeout function should destroy the "working" dialog when done saving
             */
312
            g_timeout_add_full(G_PRIORITY_DEFAULT, 300, (GSourceFunc)save_account, working, NULL);
313 314 315 316
        }
    }
}

317 318 319 320 321
static void
state_to_string(G_GNUC_UNUSED GtkTreeViewColumn *tree_column,
                GtkCellRenderer *cell,
                GtkTreeModel *tree_model,
                GtkTreeIter *iter,
322
                GtkTreeView *treeview)
323
{
324 325 326 327 328 329 330
    // check if this iter is selected
    gboolean is_selected = FALSE;
    if (GTK_IS_TREE_VIEW(treeview)) {
        auto selection = gtk_tree_view_get_selection(treeview);
        is_selected = gtk_tree_selection_iter_is_selected(selection, iter);
    }

331
    gchar *display_state = NULL;
332 333 334 335 336

    /* get account */
    QModelIndex idx = gtk_q_tree_model_get_source_idx(GTK_Q_TREE_MODEL(tree_model), iter);
    if (idx.isValid()) {

337
        auto account = AccountModel::instance().getAccountByModelIndex(idx);
338 339
        auto humanState = account->toHumanStateName();

340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362
        /* we want the color of the status text to be the default color if this iter is
         * selected so that the treeview is able to invert it against the selection color */
        if (is_selected) {
            display_state = g_strdup_printf("%s", humanState.toUtf8().constData());
        } else {
            switch (account->registrationState()) {
                case Account::RegistrationState::READY:
                    display_state = g_strdup_printf("<span fgcolor=\"green\">%s</span>", humanState.toUtf8().constData());
                break;
                case Account::RegistrationState::UNREGISTERED:
                    display_state = g_strdup_printf("<span fgcolor=\"gray\">%s</span>", humanState.toUtf8().constData());
                break;
                case Account::RegistrationState::TRYING:
                    display_state = g_strdup_printf("<span fgcolor=\"orange\">%s</span>", humanState.toUtf8().constData());
                break;
                case Account::RegistrationState::ERROR:
                    display_state = g_strdup_printf("<span fgcolor=\"red\">%s</span>", humanState.toUtf8().constData());
                break;
                case Account::RegistrationState::COUNT__:
                    g_warning("registration state should never be \"count\"");
                    display_state = g_strdup_printf("<span fgcolor=\"red\">%s</span>", humanState.toUtf8().constData());
                break;
            }
363
        }
364
    }
365

366 367 368 369
    g_object_set(G_OBJECT(cell), "markup", display_state, NULL);
    g_free(display_state);
}

370 371 372 373 374 375 376 377 378 379 380 381
static void
account_view_init(AccountView *view)
{
    gtk_widget_init_template(GTK_WIDGET(view));

    AccountViewPrivate *priv = ACCOUNT_VIEW_GET_PRIVATE(view);

    /* account model */
    GtkQTreeModel *account_model;
    GtkCellRenderer *renderer;
    GtkTreeViewColumn *column;

382
    account_model = gtk_q_tree_model_new(&AccountModel::instance(), 4,
383 384
        Account::Role::Enabled, G_TYPE_BOOLEAN,
        Account::Role::Alias, G_TYPE_STRING,
385 386
        Account::Role::Proto, G_TYPE_STRING,
        Account::Role::RegistrationState, G_TYPE_UINT);
387 388 389
    gtk_tree_view_set_model(GTK_TREE_VIEW(priv->treeview_account_list), GTK_TREE_MODEL(account_model));

    renderer = gtk_cell_renderer_toggle_new();
390
    column = gtk_tree_view_column_new_with_attributes(C_("Account state column", "Enabled"), renderer, "active", 0, NULL);
391 392 393 394 395
    gtk_tree_view_append_column(GTK_TREE_VIEW(priv->treeview_account_list), column);

    g_signal_connect(renderer, "toggled", G_CALLBACK(account_active_toggled), view);

    renderer = gtk_cell_renderer_text_new();
396
    column = gtk_tree_view_column_new_with_attributes(C_("Account alias (name) column", "Alias"), renderer, "text", 1, NULL);
397
    gtk_tree_view_append_column(GTK_TREE_VIEW(priv->treeview_account_list), column);
398 399 400 401
    g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
    gtk_tree_view_column_set_expand(column, TRUE);
    // set a min width so most of the account name is visible
    g_object_set(G_OBJECT(renderer), "width", 75, NULL);
402 403

    renderer = gtk_cell_renderer_text_new();
404
    column = gtk_tree_view_column_new_with_attributes(C_("Account status column", "Status"), renderer, "text", 3, NULL);
405
    gtk_tree_view_append_column(GTK_TREE_VIEW(priv->treeview_account_list), column);
406 407
    g_object_set(G_OBJECT(renderer), "ellipsize", PANGO_ELLIPSIZE_END, NULL);
    gtk_tree_view_column_set_expand(column, TRUE);
408

409 410 411 412 413
    /* the registration state is an enum, we want to display it as a string */
    gtk_tree_view_column_set_cell_data_func(
        column,
        renderer,
        (GtkTreeCellDataFunc)state_to_string,
414
        priv->treeview_account_list,
415 416
        NULL);

417 418 419 420 421 422
    /* add an empty box to the account stack initially, otherwise there will
     * be no cool animation when the first account is selected */
    GtkWidget *empty_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_widget_show(empty_box);
    gtk_stack_add_named(GTK_STACK(priv->stack_account), empty_box, "placeholder");
    gtk_stack_set_visible_child(GTK_STACK(priv->stack_account), empty_box);
423 424 425

    /* populate account type combo box */
    /* TODO: when to delete this model? */
426
    priv->active_protocols = new ActiveItemProxyModel((QAbstractItemModel *)AccountModel::instance().protocolModel());
427 428 429 430 431 432 433 434 435 436 437 438 439 440

    GtkQSortFilterTreeModel *protocol_model = gtk_q_sort_filter_tree_model_new(
                                                (QSortFilterProxyModel *)priv->active_protocols,
                                                1,
                                                Qt::DisplayRole, G_TYPE_STRING);

    gtk_combo_box_set_model(GTK_COMBO_BOX(priv->combobox_account_type), GTK_TREE_MODEL(protocol_model));

    renderer = gtk_cell_renderer_text_new();
    gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(priv->combobox_account_type), renderer, FALSE);
    gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(priv->combobox_account_type), renderer,
                                   "text", 0, NULL);

    /* connect signals to and from the selection model of the account model */
441
    priv->protocol_selection_changed = QObject::connect(
442
        AccountModel::instance().selectionModel(),
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462
        &QItemSelectionModel::currentChanged,
        [=](const QModelIndex & current, const QModelIndex & previous) {
            GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(priv->treeview_account_list));

            /* first unselect the previous */
            if (previous.isValid()) {
                GtkTreeIter old_iter;
                if (gtk_q_tree_model_source_index_to_iter(account_model, previous, &old_iter)) {
                    gtk_tree_selection_unselect_iter(selection, &old_iter);
                } else {
                    g_warning("Trying to unselect invalid GtkTreeIter");
                }
            }

            /* select the current */
            if (current.isValid()) {
                GtkTreeIter new_iter;
                if (gtk_q_tree_model_source_index_to_iter(account_model, current, &new_iter)) {
                    gtk_tree_selection_select_iter(selection, &new_iter);
                } else {
463
                    g_warning("SelectionModel of AccountModel changed to invalid QModelIndex?");
464 465 466 467 468 469 470 471 472 473
                }
            }
        }
    );

    GtkTreeSelection *account_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(priv->treeview_account_list));
    g_signal_connect(account_selection, "changed", G_CALLBACK(update_account_model_selection), NULL);
    g_signal_connect(account_selection, "changed", G_CALLBACK(account_selection_changed), view);

    /* select the default protocol */
474
    QModelIndex protocol_idx = AccountModel::instance().protocolModel()->selectionModel()->currentIndex();
475 476 477 478 479 480 481 482 483 484 485 486 487 488
    if (protocol_idx.isValid()) {
        protocol_idx = priv->active_protocols->mapFromSource(protocol_idx);
        GtkTreeIter protocol_iter;
        if (gtk_q_sort_filter_tree_model_source_index_to_iter(
                (GtkQSortFilterTreeModel *)protocol_model,
                protocol_idx,
                &protocol_iter)) {
            gtk_combo_box_set_active_iter(GTK_COMBO_BOX(priv->combobox_account_type), &protocol_iter);
        }
    }

    /* connect signals to add/remove accounts */
    g_signal_connect(priv->button_remove_account, "clicked", G_CALLBACK(remove_account), view);
    g_signal_connect(priv->button_add_account, "clicked", G_CALLBACK(add_account), view);
489 490 491 492 493 494
}

static void
account_view_class_init(AccountViewClass *klass)
{
    G_OBJECT_CLASS(klass)->dispose = account_view_dispose;
495
    G_OBJECT_CLASS(klass)->finalize = account_view_finalize;
496 497 498 499 500 501

    gtk_widget_class_set_template_from_resource(GTK_WIDGET_CLASS (klass),
                                                "/cx/ring/RingGnome/accountview.ui");

    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AccountView, treeview_account_list);
    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AccountView, stack_account);
502 503 504
    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AccountView, button_remove_account);
    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AccountView, button_add_account);
    gtk_widget_class_bind_template_child_private(GTK_WIDGET_CLASS (klass), AccountView, combobox_account_type);
505 506 507 508 509 510 511
}

GtkWidget *
account_view_new(void)
{
    return (GtkWidget *)g_object_new(ACCOUNT_VIEW_TYPE, NULL);
}