1
0
Fork 0
mirror of https://gitlab.gnome.org/GNOME/calls.git synced 2024-05-14 17:19:27 +00:00

Introduce CallsAccountOverview: A UI to allow managing VoIP accounts

CallsAccountOverview allows user to view, add and edit VoIP accounts
(currently SIP only).

Each CallsAccount is represented by a CallsAccountRow.

account-row: Staying alive
This commit is contained in:
Evangelos Ribeiro Tzaras 2021-05-11 18:18:35 +02:00 committed by Evangelos Ribeiro Tzaras
parent fadeaeb6b5
commit fc7156fe75
12 changed files with 733 additions and 16 deletions

View file

@ -0,0 +1,191 @@
/*
* Copyright (C) 2021 Purism SPC
*
* This file is part of Calls.
*
* Calls 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.
*
* Calls 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 Calls. If not, see <http://www.gnu.org/licenses/>.
*
* Author: Evangelos Ribeiro Tzaras <evangelos.tzaras@puri.sm>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
#define G_LOG_DOMAIN "CallsAccountOverview"
#include "calls-account.h"
#include "calls-account-overview.h"
#include "calls-account-row.h"
#include "calls-account-provider.h"
/**
* Section:calls-account-overview
* short_description: A #HdyWindow to manage VoIP accounts
* @Title: CallsAccountOverview
*
* This is a #HdyWindow derived window to display and manage the
* VoIP accounts. Each available #CallsAccount from any #CallsAccountProvider
* will be listed as a #CallsAccountRow.
*/
typedef enum {
SHOW_INTRO = 0,
SHOW_OVERVIEW,
} CallsAccountOverviewState;
struct _CallsAccountOverview {
HdyWindow parent;
/* UI widgets */
GtkStack *stack;
GtkWidget *intro;
GtkWidget *overview;
GtkWidget *add_btn;
GtkWidget *add_row;
/* The window where we add the account providers widget */
GtkWindow *account_window;
GtkWidget *current_account_widget;
/* misc */
CallsAccountOverviewState state;
GList *providers;
};
G_DEFINE_TYPE (CallsAccountOverview, calls_account_overview, HDY_TYPE_WINDOW)
static void
update_visibility (CallsAccountOverview *self)
{
g_assert (CALLS_IS_ACCOUNT_OVERVIEW (self));
switch (self->state) {
case SHOW_INTRO:
gtk_stack_set_visible_child (self->stack, self->intro);
break;
case SHOW_OVERVIEW:
gtk_stack_set_visible_child (self->stack, self->overview);
break;
default:
g_warn_if_reached ();
}
}
static void
update_state (CallsAccountOverview *self)
{
guint n_origins = 0;
g_assert (CALLS_IS_ACCOUNT_OVERVIEW (self));
for (GList *node = self->providers; node != NULL; node = node->next) {
CallsProvider *provider = CALLS_PROVIDER (node->data);
GListModel *model = calls_provider_get_origins (provider);
n_origins += g_list_model_get_n_items (model);
}
if (n_origins > 0)
self->state = SHOW_OVERVIEW;
else
self->state = SHOW_INTRO;
update_visibility (self);
}
static void
attach_account_widget (CallsAccountOverview *self,
GtkWidget *widget)
{
g_assert (CALLS_IS_ACCOUNT_OVERVIEW (self));
g_assert (!widget || GTK_IS_WIDGET (widget));
if (widget == self->current_account_widget)
return;
if (self->current_account_widget)
gtk_container_remove (GTK_CONTAINER (self->account_window),
self->current_account_widget);
self->current_account_widget = widget;
if (widget)
gtk_container_add (GTK_CONTAINER (self->account_window), widget);
}
static void
on_add_account_clicked (CallsAccountOverview *self)
{
CallsAccountProvider *provider;
GtkWidget *widget;
/* For now we only have a single AccountProvider */
provider = CALLS_ACCOUNT_PROVIDER (self->providers->data);
widget = calls_account_provider_get_account_widget (provider);
attach_account_widget (self, widget);
calls_account_provider_add_new_account (provider);
gtk_window_present (self->account_window);
}
static void
calls_account_overview_class_init (CallsAccountOverviewClass *klass)
{
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Calls/ui/account-overview.ui");
gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, add_btn);
gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, add_row);
gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, stack);
gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, intro);
gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, overview);
gtk_widget_class_bind_template_child (widget_class, CallsAccountOverview, account_window);
gtk_widget_class_bind_template_callback (widget_class, on_add_account_clicked);
}
static void
calls_account_overview_init (CallsAccountOverview *self)
{
gtk_widget_init_template (GTK_WIDGET (self));
gtk_list_box_insert (GTK_LIST_BOX (self->overview),
GTK_WIDGET (self->add_row),
-1);
gtk_window_set_transient_for (self->account_window, GTK_WINDOW (self));
update_state (self);
}
CallsAccountOverview *
calls_account_overview_new (void)
{
return g_object_new (CALLS_TYPE_ACCOUNT_OVERVIEW,
NULL);
}

View file

@ -0,0 +1,37 @@
/*
* Copyright (C) 2021 Purism SPC
*
* This file is part of Calls.
*
* Calls 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.
*
* Calls 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 Calls. If not, see <http://www.gnu.org/licenses/>.
*
* Author: Evangelos Ribeiro Tzaras <evangelos.tzaras@puri.sm>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
#pragma once
#include <handy.h>
G_BEGIN_DECLS
#define CALLS_TYPE_ACCOUNT_OVERVIEW (calls_account_overview_get_type ())
G_DECLARE_FINAL_TYPE (CallsAccountOverview, calls_account_overview, CALLS, ACCOUNT_OVERVIEW, HdyWindow)
CallsAccountOverview *calls_account_overview_new (void);
G_END_DECLS

269
src/calls-account-row.c Normal file
View file

@ -0,0 +1,269 @@
/*
* Copyright (C) 2021 Purism SPC
*
* This file is part of Calls.
*
* Calls 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.
*
* Calls 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 Calls. If not, see <http://www.gnu.org/licenses/>.
*
* Author: Evangelos Ribeiro Tzaras <evangelos.tzaras@puri.sm>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
#define G_LOG_DOMAIN "CallsAccountRow"
#include "calls-account-row.h"
#include "calls-account-provider.h"
/**
* Section:calls-account-row
* short_description: A #HdyActionRow for use in #CallsAccountOverview
* @Title: CallsAccountRow
*
* This is a #HdyActionRow derived widget representing a #CallsAccount
* for VoIP accounts (currently only SIP).
*/
enum {
PROP_0,
PROP_PROVIDER,
PROP_ACCOUNT,
PROP_ONLINE,
PROP_LAST_PROP
};
static GParamSpec *props[PROP_LAST_PROP];
enum {
EDIT_CLICKED,
N_SIGNALS
};
static guint signals[N_SIGNALS];
struct _CallsAccountRow {
HdyActionRow parent;
CallsAccountProvider *provider;
CallsAccount *account;
gboolean online;
/* UI elements */
HdyAvatar *avatar;
GtkSwitch *online_switch;
GtkButton *edit_btn;
};
G_DEFINE_TYPE (CallsAccountRow, calls_account_row, HDY_TYPE_ACTION_ROW)
static void
on_account_state_changed (CallsAccountRow *self)
{
CallsAccountState state = calls_account_get_state (self->account);
gtk_switch_set_active (self->online_switch, state == CALLS_ACCOUNT_ONLINE);
}
static void
on_edit_clicked (CallsAccountRow *self)
{
/** CallsAccountOverview connects to this signal to show
* the window containing the account providers widget.
* See calls_account_provider_get_account_widget()
*/
g_signal_emit (self, signals[EDIT_CLICKED], 0, self->provider, self->account);
}
static void
on_online_switched (CallsAccountRow *self)
{
gboolean online;
g_assert (CALLS_IS_ACCOUNT_ROW (self));
online = gtk_switch_get_active (self->online_switch);
calls_account_go_online (self->account, online);
}
static void
calls_account_row_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec *pspec)
{
CallsAccountRow *self = CALLS_ACCOUNT_ROW (object);
switch (property_id) {
case PROP_PROVIDER:
self->provider = g_value_get_object (value);
break;
case PROP_ACCOUNT:
self->account = g_value_get_object (value);
g_object_bind_property (self->account, "name",
self, "title",
G_BINDING_SYNC_CREATE);
g_object_bind_property (self->account, "address",
self, "subtitle",
G_BINDING_SYNC_CREATE);
g_signal_connect_object (self->account, "notify::account-state",
G_CALLBACK (on_account_state_changed), self,
G_CONNECT_SWAPPED);
on_account_state_changed (self);
break;
case PROP_ONLINE:
calls_account_row_set_online (self, g_value_get_boolean (value));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
calls_account_row_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
CallsAccountRow *self = CALLS_ACCOUNT_ROW (object);
switch (property_id) {
case PROP_ACCOUNT:
g_value_set_object (value, calls_account_row_get_account (self));
break;
case PROP_ONLINE:
g_value_set_boolean (value, calls_account_row_get_online (self));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
calls_account_row_class_init (CallsAccountRowClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
object_class->set_property = calls_account_row_set_property;
object_class->get_property = calls_account_row_get_property;
signals[EDIT_CLICKED] =
g_signal_new ("edit-clicked",
CALLS_TYPE_ACCOUNT_ROW,
G_SIGNAL_RUN_FIRST,
0,
NULL, NULL, NULL,
G_TYPE_NONE,
2,
CALLS_TYPE_ACCOUNT_PROVIDER,
CALLS_TYPE_ACCOUNT);
props[PROP_PROVIDER] =
g_param_spec_object ("provider",
"Provider",
"The provider of the account this row represents",
CALLS_TYPE_ACCOUNT_PROVIDER,
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS);
props[PROP_ACCOUNT] =
g_param_spec_object ("account",
"Account",
"The account this row represents",
CALLS_TYPE_ACCOUNT,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
props[PROP_ONLINE] =
g_param_spec_boolean ("online",
"online",
"The state of the online switch",
FALSE,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Calls/ui/account-row.ui");
gtk_widget_class_bind_template_child (widget_class, CallsAccountRow, avatar);
gtk_widget_class_bind_template_child (widget_class, CallsAccountRow, online_switch);
gtk_widget_class_bind_template_child (widget_class, CallsAccountRow, edit_btn);
gtk_widget_class_bind_template_callback (widget_class, on_edit_clicked);
gtk_widget_class_bind_template_callback (widget_class, on_online_switched);
}
static void
calls_account_row_init (CallsAccountRow *self)
{
gtk_widget_init_template (GTK_WIDGET (self));
}
CallsAccountRow *
calls_account_row_new (CallsAccountProvider *provider,
CallsAccount *account)
{
g_return_val_if_fail (CALLS_IS_ACCOUNT (account), NULL);
return g_object_new (CALLS_TYPE_ACCOUNT_ROW,
"provider", provider,
"account", account,
NULL);
}
gboolean
calls_account_row_get_online (CallsAccountRow *self)
{
g_return_val_if_fail (CALLS_IS_ACCOUNT_ROW (self), FALSE);
return gtk_switch_get_active (self->online_switch);
}
void
calls_account_row_set_online (CallsAccountRow *self,
gboolean online)
{
g_return_if_fail (CALLS_IS_ACCOUNT_ROW (self));
if (online == gtk_switch_get_active (self->online_switch))
return;
gtk_switch_set_active (self->online_switch, online);
}
CallsAccount *
calls_account_row_get_account (CallsAccountRow *self)
{
g_return_val_if_fail (CALLS_IS_ACCOUNT_ROW (self), NULL);
return self->account;
}

46
src/calls-account-row.h Normal file
View file

@ -0,0 +1,46 @@
/*
* Copyright (C) 2021 Purism SPC
*
* This file is part of Calls.
*
* Calls 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.
*
* Calls 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 Calls. If not, see <http://www.gnu.org/licenses/>.
*
* Author: Evangelos Ribeiro Tzaras <evangelos.tzaras@puri.sm>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
#pragma once
#include "calls-account.h"
#include "calls-account-provider.h"
#include <handy.h>
G_BEGIN_DECLS
#define CALLS_TYPE_ACCOUNT_ROW (calls_account_row_get_type ())
G_DECLARE_FINAL_TYPE (CallsAccountRow, calls_account_row, CALLS, ACCOUNT_ROW, HdyActionRow)
CallsAccountRow *calls_account_row_new (CallsAccountProvider *provider,
CallsAccount *account);
gboolean calls_account_row_get_online (CallsAccountRow *self);
void calls_account_row_set_online (CallsAccountRow *self,
gboolean online);
CallsAccount *calls_account_row_get_account (CallsAccountRow *self);
G_END_DECLS

View file

@ -350,6 +350,15 @@ copy_number (GSimpleAction *action,
g_debug ("Copied `%s' to clipboard", number);
}
static void
show_accounts (GSimpleAction *action,
GVariant *parameter,
gpointer user_data)
{
CallsApplication *app = CALLS_APPLICATION (g_application_get_default ());
calls_main_window_show_accounts_overview (app->main_window);
}
static void
manager_state_changed_cb (GApplication *application)
{
@ -366,6 +375,8 @@ static const GActionEntry actions[] =
{ "set-daemon", set_daemon_action, NULL },
{ "dial", dial_action, "s" },
{ "copy-number", copy_number, "s"},
/* TODO About dialog { "about", show_about, NULL}, */
{ "accounts", show_accounts, NULL},
};

View file

@ -23,6 +23,7 @@
*/
#include "calls-main-window.h"
#include "calls-account-overview.h"
#include "calls-origin.h"
#include "calls-ussd.h"
#include "calls-call-selector-item.h"
@ -54,6 +55,7 @@ struct _CallsMainWindow
GtkRevealer *permanent_error_revealer;
GtkLabel *permanent_error_label;
CallsAccountOverview *account_overview;
CallsNewCallBox *new_call;
GtkDialog *ussd_dialog;
@ -426,6 +428,7 @@ dispose (GObject *object)
CallsMainWindow *self = CALLS_MAIN_WINDOW (object);
g_clear_object (&self->record_store);
g_clear_object (&self->account_overview);
G_OBJECT_CLASS (calls_main_window_parent_class)->dispose (object);
}
@ -531,3 +534,17 @@ calls_main_window_dial (CallsMainWindow *self,
calls_new_call_box_dial (self->new_call, target);
}
}
void
calls_main_window_show_accounts_overview (CallsMainWindow *self)
{
g_return_if_fail (CALLS_IS_MAIN_WINDOW (self));
if (self->account_overview == NULL) {
self->account_overview = calls_account_overview_new ();
gtk_window_set_transient_for (GTK_WINDOW (self->account_overview),
GTK_WINDOW (self));
}
gtk_window_present (GTK_WINDOW (self->account_overview));
}

View file

@ -33,10 +33,11 @@ G_BEGIN_DECLS
G_DECLARE_FINAL_TYPE (CallsMainWindow, calls_main_window, CALLS, MAIN_WINDOW, HdyApplicationWindow);
CallsMainWindow *calls_main_window_new (GtkApplication *application,
GListModel *record_store);
void calls_main_window_dial (CallsMainWindow *self,
const gchar *target);
CallsMainWindow *calls_main_window_new (GtkApplication *application,
GListModel *record_store);
void calls_main_window_dial (CallsMainWindow *self,
const gchar *target);
void calls_main_window_show_accounts_overview (CallsMainWindow *self);
G_END_DECLS

View file

@ -14,6 +14,8 @@
<file preprocess="xml-stripblanks">in-app-notification.ui</file>
<file preprocess="xml-stripblanks">contacts-row.ui</file>
<file preprocess="xml-stripblanks">contacts-box.ui</file>
<file preprocess="xml-stripblanks">account-overview.ui</file>
<file preprocess="xml-stripblanks">account-row.ui</file>
</gresource>
<gresource prefix="/org/gnome/Calls/">
<file>style.css</file>

View file

@ -108,6 +108,8 @@ calls_sources = files(['calls-message-source.c', 'calls-message-source.h',
'calls-contacts-row.c', 'calls-contacts-row.h',
'calls-account.c', 'calls-account.h',
'calls-account-provider.c', 'calls-account-provider.h',
'calls-account-overview.c', 'calls-account-overview.h',
'calls-account-row.c', 'calls-account-row.h',
'calls-settings.c', 'calls-settings.h',
]) + calls_generated_sources

View file

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.24"/>
<requires lib="libhandy" version="1.0"/>
<template class="CallsAccountOverview" parent="HdyWindow">
<property name="visible">True</property>
<property name="default-width">380</property>
<property name="default-height">660</property>
<signal name="delete-event" handler="gtk_widget_hide_on_delete"/>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
<object class="HdyHeaderBar">
<property name="title" translatable="yes">VoIP Accounts</property>
<property name="show-close-button">True</property>
<property name="visible">True</property>
<child>
<object class="GtkSpinner" id="spinner">
<property name="visible">True</property>
</object>
<packing>
<property name="pack_type">end</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="vexpand">True</property>
<property name="transition-type">crossfade</property>
<!-- First child type: No accounts present: Show a blurb and a Add button -->
<child>
<object class="HdyStatusPage" id="intro">
<property name="visible">True</property>
<property name="title" translatable="yes">Add VoIP Accounts</property>
<property name="icon-name">system-users-symbolic</property>
<property name="description" translatable="yes">You can add VoIP account here. It will allow you to place and receive VoIP calls using the SIP protocol. This feature is still relatively new and not yet feature complete (i.e. no encrypted media).</property>
<child>
<object class="GtkButton" id="add_btn">
<property name="visible">True</property>
<property name="margin">6</property>
<property name="halign">center</property>
<property name="use-underline">True</property>
<property name="label" translatable="yes">_Add Account</property>
<signal name="clicked" handler="on_add_account_clicked" swapped="yes"/>
<style>
<class name="suggested-action"/>
</style>
</object>
</child>
</object>
<packing>
<property name="name">intro-page</property>
</packing>
</child>
<!-- Second child type: Some accounts present: Show a Listbox to manage accounts -->
<child>
<object class="GtkListBox" id="overview">
<property name="visible">True</property>
<property name="selection-mode">none</property>
<!-- placeholder -->
<child type="placeholder"/>
</object>
<packing>
<property name="name">overview-page</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</template>
<object class="HdyActionRow" id="add_row">
<property name="visible">True</property>
<property name="use-underline">True</property>
<property name="activatable">True</property>
<property name="title" translatable="yes">_Add Account</property>
<signal name="activated" handler="on_add_account_clicked" swapped="yes"/>
</object>
<object class="HdyWindow" id="account_window">
<property name="visible">False</property>
<property name="default-width">380</property>
<property name="default-height">660</property>
<signal name="delete-event" handler="gtk_widget_hide_on_delete"/>
</object>
</interface>

41
src/ui/account-row.ui Normal file
View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.20"/>
<requires lib="libhandy" version="1.0"/>
<template class="CallsAccountRow" parent="HdyActionRow">
<property name="visible">True</property>
<property name="title">Title</property>
<property name="subtitle">Subtitle</property>
<property name="activatable">True</property>
<child type="prefix">
<object class="HdyAvatar" id="avatar">
<property name="visible">True</property>
<property name="show-initials">True</property>
<property name="size">48</property>
</object>
</child>
<child>
<object class="GtkSwitch" id="online_switch">
<property name="valign">center</property>
<property name="visible">True</property>
<property name="active">False</property>
<signal name="notify::active" handler="on_online_switched" swapped="yes"/>
</object>
</child>
<child>
<object class="GtkButton" id="edit_btn">
<property name="visible">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon_name">go-next-symbolic</property>
</object>
</child>
<signal name="clicked" handler="on_edit_clicked" swapped="yes"/>
</object>
</child>
</template>
</interface>

View file

@ -30,7 +30,7 @@
</child>
<child>
<object class="GtkMenuButton">
<property name="visible">False</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="popover">menu_popover</property>
<child>
@ -225,8 +225,8 @@
<child>
<object class="GtkModelButton">
<property name="visible">True</property>
<property name="text" translatable="yes">VoIP Accounts</property>
<!--<property name="action-name">app.voip-accounts</property>-->
<property name="text" translatable="yes">_VoIP Accounts</property>
<property name="action-name">app.accounts</property>
</object>
</child>
<child>
@ -237,20 +237,21 @@
</child>
<child>
<object class="GtkModelButton">
<property name="visible">True</property>
<property name="text" translatable="yes">Keyboard shortcuts</property>
<property name="visible">False</property>
<property name="text" translatable="yes">_Keyboard shortcuts</property>
</object>
</child>
<child>
<object class="GtkModelButton">
<property name="visible">False</property>
<property name="text" translatable="yes">_Help</property>
</object>
</child>
<child>
<object class="GtkModelButton">
<property name="visible">True</property>
<property name="text" translatable="yes">Help</property>
</object>
</child>
<child>
<object class="GtkModelButton">
<property name="visible">True</property>
<property name="text" translatable="yes">About Calls</property>
<property name="text" translatable="yes">_About Calls</property>
<property name="action-name">app.about</property>
</object>
</child>