Editor: use listbox layout to edit contact and secondary menu

GNOME uses now listboxes as the standart design pattern instead of a
grid. This replaces the grid and makes use of listboxes to allow the
user to edit a contact.
Some key features are:
- Hide less important properties when not used
- Dynamically fill the editor with properties so that the user has always
  one empty row to fill for each visible property
- use a dialog for the birthday picker
- Group properties by persona

ContactSheet:
Replace the edit button with a secondary menu.
The secondary menu contains share (hidden for now), edit, unlink and delete.
The reason for this change is that it doesn't make a lot of sense to have
delete and unlink inside the edit mode, since they don't require to commit changed.

Folks doesn't provied a staging features. So changes are commited
directly to the backend. The FakePersona and FakeIndividual are used
exactly for this. They work as a intermidiate layer so the editor can
change the persona directly and then when the user presses "done" the
changes can be copied to the real contact.
parent aa243330
......@@ -5,10 +5,9 @@
<file compressed="true" preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/contacts-accounts-list.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/contacts-avatar-selector.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/contacts-contact-editor.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/contacts-contact-form.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/contacts-contact-pane.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/contacts-crop-cheese-dialog.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/contacts-editor-menu.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/contacts-in-app-notification.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/contacts-link-suggestion-grid.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/contacts-linked-personas-dialog.ui</file>
......
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<!-- interface-requires gtk+ 3.22 -->
<template class="ContactsContactForm" parent="GtkGrid">
<property name="visible">True</property>
<child>
<object class="GtkScrolledWindow" id="main_sw">
<property name="visible">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="shadow_type">none</property>
<property name="hscrollbar_policy">never</property>
<property name="vscrollbar_policy">automatic</property>
<child>
<object class="ContactsMaxWidthBin">
<property name="visible">True</property>
<property name="max_width">600</property>
<property name="halign">center</property>
<child>
<object class="GtkGrid" id="container_grid">
<property name="visible">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<property name="row_spacing">12</property>
<property name="column_spacing">12</property>
<property name="margin">12</property>
<property name="margin_top">36</property>
<property name="margin_bottom">36</property>
</object>
</child>
</object>
</child>
</object>
</child>
</template>
</interface>
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.20"/>
<template class="ContactsContactPane" parent="GtkStack">
<template class="ContactsContactPane" parent="GtkScrolledWindow">
<property name="visible">True</property>
<property name="visible-child">none_selected_page</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="shadow_type">none</property>
<property name="hscrollbar_policy">never</property>
<property name="vscrollbar_policy">automatic</property>
<child>
<object class="GtkGrid" id="none_selected_page">
<object class="HdyColumn">
<property name="visible">True</property>
<property name="width_request">300</property>
<property name="orientation">vertical</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="row_spacing">6</property>
<property name="maximum-width">600</property>
<property name="linear-growth-width">400</property>
<property name="margin-top">32</property>
<property name="margin-bottom">32</property>
<property name="margin-left">24</property>
<property name="margin-right">24</property>
<child>
<object class="GtkImage">
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="icon_name">avatar-default-symbolic</property>
<property name="vexpand">True</property>
<property name="valign">end</property>
<property name="pixel_size">144</property>
<style>
<class name="contacts-watermark"/>
</style>
<property name="visible-child">none_selected_page</property>
<child>
<object class="GtkGrid" id="none_selected_page">
<property name="visible">True</property>
<property name="width_request">300</property>
<property name="orientation">vertical</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="row_spacing">6</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="icon_name">avatar-default-symbolic</property>
<property name="vexpand">True</property>
<property name="valign">end</property>
<property name="pixel_size">144</property>
<style>
<class name="contacts-watermark"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="label" translatable="yes">Select a contact</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="valign">start</property>
<property name="margin_bottom">70</property>
<style>
<class name="contacts-watermark"/>
</style>
</object>
</child>
</object>
<packing>
<property name="name">none-selected-page</property>
</packing>
</child>
<child>
<object class="GtkBox" id="contact_sheet_page">
<property name="visible">True</property>
</object>
<packing>
<property name="name">contact-sheet-page</property>
</packing>
</child>
<child>
<object class="GtkBox" id="contact_editor_page">
<property name="visible">True</property>
</object>
<packing>
<property name="name">contact-editor-page</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="label" translatable="yes">Select a contact</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="valign">start</property>
<property name="margin_bottom">70</property>
<style>
<class name="contacts-watermark"/>
</style>
</object>
</child>
</object>
<packing>
<property name="name">none-selected-page</property>
</packing>
</child>
<child>
<object class="GtkBox" id="contact_sheet_page">
<property name="visible">True</property>
</object>
<packing>
<property name="name">contact-sheet-page</property>
</packing>
</child>
<child>
<object class="GtkBox" id="contact_editor_page">
<property name="visible">True</property>
</object>
<packing>
<property name="name">contact-editor-page</property>
</packing>
</child>
</template>
</interface>
<?xml version="1.0" encoding="utf-8"?>
<interface>
<object class="GtkPopoverMenu" id="editor_menu">
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<property name="margin">10</property>
<child>
<object class="GtkModelButton">
<property name="visible">True</property>
<property name="action-name">persona.change-addressbook</property>
<property name="text" translatable="yes">Change Addressbook</property>
</object>
</child>
</object>
</child>
</object>
</interface>
......@@ -94,6 +94,48 @@
</object>
</child>
</object>
<object class="GtkPopoverMenu" id="contact_sheet_menu">
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<property name="margin">10</property>
<child>
<object class="GtkModelButton">
<property name="visible">False</property>
<property name="action-name">window.share-contact</property>
<property name="text" translatable="yes">Share</property>
</object>
</child>
<child>
<object class="GtkModelButton">
<property name="visible">True</property>
<property name="action-name">window.edit-contact</property>
<property name="text" translatable="yes">Edit</property>
</object>
</child>
<child>
<object class="GtkModelButton" id="unlink_button">
<property name="visible">True</property>
<property name="action-name">window.unlink-contact</property>
<property name="text" translatable="yes">Unlink</property>
</object>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
</object>
</child>
<child>
<object class="GtkModelButton">
<property name="visible">True</property>
<property name="action-name">window.delete-contact</property>
<property name="text" translatable="yes">Delete</property>
</object>
</child>
</object>
</child>
</object>
<template class="ContactsWindow" parent="GtkApplicationWindow">
<property name="can_focus">False</property>
<property name="default_width">800</property>
......@@ -245,44 +287,43 @@
</packing>
</child>
<child>
<object class="GtkButton" id="edit_button">
<object class="GtkToggleButton" id="favorite_button">
<property name="visible">False</property>
<property name="can_focus">True</property>
<property name="focus_on_click">False</property>
<property name="valign">center</property>
<property name="tooltip_text" translatable="yes">Edit details</property>
<signal name="clicked" handler="on_edit_button_clicked"/>
<signal name="toggled" handler="on_favorite_button_toggled"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-edit-symbolic</property>
<property name="icon_name">starred-symbolic</property>
<property name="icon_size">1</property>
</object>
</child>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkToggleButton" id="favorite_button">
<object class="GtkMenuButton" id="contact_menu_button">
<property name="visible">False</property>
<property name="can_focus">True</property>
<property name="focus_on_click">False</property>
<property name="valign">center</property>
<signal name="toggled" handler="on_favorite_button_toggled"/>
<property name="popover">contact_sheet_menu</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">starred-symbolic</property>
<property name="icon_size">1</property>
<property name="icon_name">view-more-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
......
......@@ -2,24 +2,11 @@
* GNOME Contacts
*/
.contacts-map {
background-color: @theme_bg_color;
}
/* The contacts in the left pane */
.contacts-contact-list {
background-color: transparent;
}
/* A single row in the contact list pane */
row.contact-data-row {
}
/* Styles for a ContactsContactForm */
.contacts-contact-form {
background-color: mix(@theme_bg_color, @theme_base_color, 0.4);
}
.contacts-suggestion {
border-top: 1px solid @borders;
background-color: shade(@theme_bg_color, 0.9);
......@@ -69,3 +56,15 @@ row.contact-data-row {
text-shadow: none; -gtk-icon-shadow: none;
border: 1px solid rgba(205, 199, 194, 0.5);
}
/* remove padding from ListBoxRow so that the revealer doesn't jump */
row.editor-property-row {
padding: 0px;
}
popover list {
background-color: @theme_bg_color;
}
popover list row:hover {
background-color: @theme_selected_fg_color
}
/*
* Copyright (C) 2019 Purism SPC
*
* Author: Julian Sparber
*
* 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 2 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, see <http://www.gnu.org/licenses/>.
*/
using Hdy;
using Gtk;
using Folks;
public class Contacts.AddressbookList : ListBox {
private BackendStore store;
private Widget? checkmark;
private AddressbookRow? marked_row;
private bool show_icon;
public signal void addressbook_selected ();
public AddressbookList (BackendStore store, bool icon = true) {
this.store = store;
this.show_icon = icon;
this.set_header_func (list_box_update_header_func);
this.update ();
}
void list_box_update_header_func (ListBoxRow row, ListBoxRow? before) {
if (before == null) {
row.set_header (null);
} else if (row.get_header () == null) {
var header = new Separator (Orientation.HORIZONTAL);
header.show ();
row.set_header (header);
}
}
public override void row_activated (ListBoxRow row) {
var addressbook = row as AddressbookRow;
if (addressbook == null)
return;
if (marked_row != null &&
marked_row == addressbook) {
return;
}
if (marked_row != null) {
marked_row.unselect ();
}
addressbook.select ();
marked_row = addressbook;
addressbook_selected ();
}
public void update () {
foreach (var child in get_children ()) {
child.destroy ();
}
// Fill the list with address book
PersonaStore[] eds_stores = Utils.get_eds_address_books_from_backend (this.store);
debug ("Found %d EDS stores", eds_stores.length);
PersonaStore? local_store = null;
foreach (var persona_store in eds_stores) {
if (persona_store.id == "system-address-book") {
local_store = persona_store;
continue;
}
var source = (persona_store as Edsf.PersonaStore).source;
var parent_source = eds_source_registry.ref_source (source.parent);
var provider_name = Utils.format_persona_store_name (persona_store);
debug ("Contact store \"%s\"", provider_name);
var source_account_id = "";
if (parent_source.has_extension (E.SOURCE_EXTENSION_GOA)) {
var goa_source_ext = parent_source.get_extension (E.SOURCE_EXTENSION_GOA) as E.SourceGoa;
source_account_id = goa_source_ext.account_id;
}
Gtk.Image provider_image = null;
if (this.show_icon) {
if (source_account_id != "")
provider_image = Contacts.get_icon_for_goa_account (source_account_id);
else
provider_image = new Image.from_icon_name (Config.APP_ID, IconSize.DIALOG);
}
var row = new AddressbookRow (provider_name, parent_source.display_name, provider_image);
add (row);
}
if (local_store != null) {
var provider_image = (this.show_icon) ? new Image.from_icon_name (Config.APP_ID, IconSize.DIALOG) : null;
var local_row = new AddressbookRow (_("Local Address Book"), null, provider_image);
add (local_row);
}
/*
if (select_active &&
local_store == this.contacts_store.aggregator.primary_store) {
row_activated (local_row);
}
*/
show_all ();
}
}
public class Contacts.AddressbookRow : Hdy.ActionRow {
Widget checkmark;
public AddressbookRow (string title, string? subtitle, Widget? image = null) {
this.set_selectable (false);
if (image != null) {
this.add_prefix (image);
}
this.title = title;
if (subtitle != null) {
this.subtitle = subtitle;
}
this.show_all ();
this.no_show_all = true;
this.checkmark = new Image.from_icon_name ("object-select-symbolic", IconSize.MENU);
this.checkmark.set ("margin-end", 6,
"valign", Align.CENTER,
"halign", Align.END,
"vexpand", true,
"hexpand", true);
this.add_action (this.checkmark);
}
public void unselect () {
this.checkmark.hide ();
}
public void select () {
this.checkmark.show ();
}
}
......@@ -30,13 +30,13 @@ public class Contacts.App : Gtk.Application {
private bool is_quiescent_scheduled = false;
private const GLib.ActionEntry[] action_entries = {
{ "quit", quit },
{ "help", show_help },
{ "about", show_about },
{ "change-book", change_address_book },
{ "online-accounts", online_accounts },
{ "new-contact", new_contact },
{ "show-contact", on_show_contact, "s" }
{ "quit", quit },
{ "help", show_help },
{ "about", show_about },
{ "change-book", change_address_book },
{ "online-accounts", online_accounts },
{ "new-contact", new_contact },
{ "show-contact", on_show_contact, "s"}
};
private const OptionEntry[] options = {
......@@ -55,7 +55,7 @@ public class Contacts.App : Gtk.Application {
this.settings = new Settings (this);
add_main_option_entries (options);
create_actions ();
create_actions ();
}
public override int command_line (ApplicationCommandLine command_line) {
......
......@@ -48,11 +48,6 @@ public class Contacts.AvatarSelector : Popover {
private Cheese.CameraDeviceMonitor camera_monitor;
#endif
/**
* Fired after the user has definitely chosen a new avatar.
*/
public signal void set_avatar (GLib.Icon avatar_icon);
public AvatarSelector (Gtk.Widget relative, Individual? individual) {
this.set_relative_to(relative);
this.thumbnail_factory = new Gnome.DesktopThumbnailFactory (Gnome.ThumbnailSize.NORMAL);
......@@ -105,7 +100,8 @@ public class Contacts.AvatarSelector : Popover {
uint8[] buffer;
pixbuf.save_to_buffer (out buffer, "png", null);
var icon = new BytesIcon (new Bytes (buffer));
set_avatar (icon);
// Set the new avatar
this.individual.change_avatar(icon as LoadableIcon);
} catch (GLib.Error e) {
warning ("Failed to set avatar: %s", e.message);
Utils.show_error_dialog (_("Failed to set avatar."),
......
This diff is collapsed.
......@@ -27,13 +27,16 @@ const int PROFILE_SIZE = 128;
* and a ContactEditor to edit contact information.
*/
[GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-contact-pane.ui")]
public class Contacts.ContactPane : Stack {
public class Contacts.ContactPane : ScrolledWindow {
private Window parent_window;
private Store store;
public Individual? individual = null;
public Individual? individual { get; set; default = null; }
[GtkChild]
private Stack stack;
[GtkChild]
private Grid none_selected_page;
......@@ -46,27 +49,11 @@ public class Contacts.ContactPane : Stack {
private Box contact_editor_page;
private ContactEditor? editor = null;
private SimpleActionGroup edit_contact_actions = new SimpleActionGroup ();
private const GLib.ActionEntry[] action_entries = {
{ "add.email-addresses.home", on_add_detail },
{ "add.email-addresses.work", on_add_detail },
{ "add.phone-numbers.cell", on_add_detail },
{ "add.phone-numbers.home", on_add_detail },
{ "add.phone-numbers.work", on_add_detail },
{ "add.urls", on_add_detail },
{ "add.nickname", on_add_detail },
{ "add.birthday", on_add_detail },
{ "add.postal-addresses.home", on_add_detail },
{ "add.postal-addresses.work", on_add_detail },
{ "add.notes", on_add_detail },
};
public bool on_edit_mode = false;
private LinkSuggestionGrid? suggestion_grid = null;
/* Signals */
public signal void contacts_linked (string? main_contact, string linked_contact, LinkOperation operation);
public signal void will_delete (Individual individual);
/**
* Passes the changed display name to all listeners after edit mode has been completed.
*/
......@@ -76,8 +63,6 @@ public class Contacts.ContactPane : Stack {
publi