[gnome-contacts] Editor: use listbox layout to edit contact and secondary menu
- From: Niels De Graef <nielsdg src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-contacts] Editor: use listbox layout to edit contact and secondary menu
- Date: Mon, 3 Feb 2020 12:34:22 +0000 (UTC)
commit 368d11f94f411a63ab5792ebed55047ef7bcf0fd
Author: Julian Sparber <julian sparber net>
Date: Fri Jan 10 15:46:17 2020 +0100
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.
data/contacts.gresource.xml | 3 +-
data/ui/contacts-contact-form.ui | 37 --
data/ui/contacts-contact-pane.ui | 118 +++--
data/ui/contacts-editor-menu.ui | 19 +
data/ui/contacts-window.ui | 59 ++-
data/ui/style.css | 25 +-
src/contacts-addressbook-list.vala | 156 ++++++
src/contacts-app.vala | 16 +-
src/contacts-avatar-selector.vala | 8 +-
src/contacts-contact-editor.vala | 941 ++---------------------------------
src/contacts-contact-pane.vala | 204 +++-----
src/contacts-contact-sheet.vala | 75 ++-
src/contacts-editor-persona.vala | 190 +++++++
src/contacts-editor-property.vala | 629 +++++++++++++++++++++++
src/contacts-fake-persona-store.vala | 516 +++++++++++++++----
src/contacts-linking.vala | 54 +-
src/contacts-max-width-bin.vala | 66 ---
src/contacts-store.vala | 14 +-
src/contacts-utils.vala | 21 +
src/contacts-window.vala | 93 +++-
src/meson.build | 5 +-
21 files changed, 1830 insertions(+), 1419 deletions(-)
---
diff --git a/data/contacts.gresource.xml b/data/contacts.gresource.xml
index 6618f43..4105fc5 100644
--- a/data/contacts.gresource.xml
+++ b/data/contacts.gresource.xml
@@ -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>
diff --git a/data/ui/contacts-contact-pane.ui b/data/ui/contacts-contact-pane.ui
index 1d2362b..1958d97 100644
--- a/data/ui/contacts-contact-pane.ui
+++ b/data/ui/contacts-contact-pane.ui
@@ -1,62 +1,84 @@
<?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>
diff --git a/data/ui/contacts-editor-menu.ui b/data/ui/contacts-editor-menu.ui
new file mode 100644
index 0000000..21ac6bb
--- /dev/null
+++ b/data/ui/contacts-editor-menu.ui
@@ -0,0 +1,19 @@
+<?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>
diff --git a/data/ui/contacts-window.ui b/data/ui/contacts-window.ui
index e203f73..616ac42 100644
--- a/data/ui/contacts-window.ui
+++ b/data/ui/contacts-window.ui
@@ -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>
diff --git a/data/ui/style.css b/data/ui/style.css
index 763703c..de2f0f6 100644
--- a/data/ui/style.css
+++ b/data/ui/style.css
@@ -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
+}
diff --git a/src/contacts-addressbook-list.vala b/src/contacts-addressbook-list.vala
new file mode 100644
index 0000000..c59a30a
--- /dev/null
+++ b/src/contacts-addressbook-list.vala
@@ -0,0 +1,156 @@
+/*
+ * 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 ();
+ }
+}
diff --git a/src/contacts-app.vala b/src/contacts-app.vala
index 668c90e..af4bfa5 100644
--- a/src/contacts-app.vala
+++ b/src/contacts-app.vala
@@ -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) {
diff --git a/src/contacts-avatar-selector.vala b/src/contacts-avatar-selector.vala
index 126d06d..51a5f3a 100644
--- a/src/contacts-avatar-selector.vala
+++ b/src/contacts-avatar-selector.vala
@@ -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."),
diff --git a/src/contacts-contact-editor.vala b/src/contacts-contact-editor.vala
index 396a681..c73da5d 100644
--- a/src/contacts-contact-editor.vala
+++ b/src/contacts-contact-editor.vala
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2011 Alexander Larsson <alexl redhat com>
+ * Copyright (C) 2019 Purism SPC
*
* 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
@@ -19,885 +20,32 @@ using Gtk;
using Folks;
using Gee;
-public class Contacts.AddressEditor : Box {
- public Entry? entries[7]; /* must be the number of elements in postal_element_props */
- public PostalAddressFieldDetails details;
-
- public const string[] postal_element_props = {"street", "extension", "locality", "region", "postal_code",
"po_box", "country"};
- public static string[] postal_element_names = {_("Street"), _("Extension"), _("City"),
_("State/Province"), _("Zip/Postal Code"), _("PO box"), _("Country")};
-
- public signal void changed ();
-
- public AddressEditor (PostalAddressFieldDetails _details) {
- set_hexpand (true);
- set_orientation (Orientation.VERTICAL);
-
- details = _details;
-
- for (int i = 0; i < entries.length; i++) {
- string postal_part;
- details.value.get (AddressEditor.postal_element_props[i], out postal_part);
-
- entries[i] = new Entry ();
- entries[i].set_hexpand (true);
- entries[i].set ("placeholder-text", AddressEditor.postal_element_names[i]);
-
- if (postal_part != null)
- entries[i].set_text (postal_part);
-
- entries[i].get_style_context ().add_class ("contacts-postal-entry");
- add (entries[i]);
-
- entries[i].changed.connect (() => {
- changed ();
- });
- }
- }
-
- public override void grab_focus () {
- entries[0].grab_focus ();
- }
-}
-
/**
* A widget that allows the user to edit a given {@link Contact}.
*/
-[GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-contact-editor.ui")]
-public class Contacts.ContactEditor : ContactForm {
-
- private const string[] DEFAULT_PROPS_NEW_CONTACT = {
- "email-addresses.personal",
- "phone-numbers.cell",
- "postal-addresses.home"
- };
-
- private weak Widget focus_widget;
-
+public class Contacts.ContactEditor : Box {
+ private Individual individual;
private Entry name_entry;
-
+ private AvatarSelector avatar_selector = null;
private Avatar avatar;
- [GtkChild]
- private MenuButton add_detail_button;
-
- [GtkChild]
- public Button linked_button;
-
- [GtkChild]
- public Button remove_button;
-
- public struct PropertyData {
- Persona? persona;
- Value value;
- }
-
- struct RowData {
- AbstractFieldDetails details;
- }
-
- struct Field {
- bool changed;
- HashMap<int, RowData?> rows;
- }
-
- private HashSet<Persona> unlink_personas;
- /* the key of the hash_map is the uid of the persona */
- private HashMap<string, HashMap<string, Field?>> writable_personas;
-
- public bool has_birthday_row {
- get; private set; default = false;
- }
-
- public bool has_nickname_row {
- get; private set; default = false;
- }
-
- public bool has_notes_row {
- get; private set; default = false;
- }
-
- construct {
- this.unlink_personas = new HashSet<Persona> ();
- this.writable_personas = new HashMap<string, HashMap<string, Field?>> ();
- this.container_grid.size_allocate.connect(on_container_grid_size_allocate);
- }
-
- public ContactEditor (Individual? individual, Store store, GLib.ActionGroup editor_actions) {
- this.store = store;
+ public ContactEditor (Individual individual, IndividualAggregator aggregator) {
+ Object (orientation: Orientation.VERTICAL, spacing: 24);
this.individual = individual;
- this.add_detail_button.get_popover ().insert_action_group ("edit", editor_actions);
-
- if (individual != null) {
- this.remove_button.sensitive = Contacts.Utils.can_remove_personas (individual);
- this.linked_button.sensitive = individual.personas.size > 1;
- } else {
- this.remove_button.hide ();
- this.linked_button.hide ();
- }
-
- create_avatar_button ();
- create_name_entry ();
-
- if (individual != null)
- fill_in_contact ();
- else
- fill_in_empty ();
-
- this.container_grid.show_all ();
- }
-
- private void fill_in_contact () {
- int i = 3;
- int last_store_position = 0;
- bool is_first_persona = true;
-
- var personas = Contacts.Utils.get_personas_for_display (individual);
- foreach (var p in personas) {
- if (!is_first_persona) {
- this.container_grid.attach (create_persona_store_label (p), 0, i, 2);
- last_store_position = ++i;
- }
+ Box header = new Box (Orientation.HORIZONTAL, 6);
+ header.add (create_avatar_button ());
+ header.add (create_name_entry ());
+ add (header);
- var rw_props = sort_persona_properties (p.writeable_properties);
- if (rw_props.length != 0) {
- this.writable_personas[p.uid] = new HashMap<string, Field?> ();
- foreach (var prop in rw_props)
- add_edit_row (p, prop, ref i);
- }
-
- if (is_first_persona)
- this.last_row = i - 1;
-
- if (i != 3)
- is_first_persona = false;
-
- if (i == last_store_position) {
- i--;
- this.container_grid.get_child_at (0, i).destroy ();
- }
+ foreach (var p in individual.personas) {
+ add (new EditorPersona (p, aggregator));
}
- }
-
- private void fill_in_empty () {
- this.last_row = 2;
-
- this.writable_personas["null-persona.hack"] = new HashMap<string, Field?> ();
- foreach (var prop in DEFAULT_PROPS_NEW_CONTACT) {
- var tok = prop.split (".");
- add_new_row_for_property (null, tok[0], tok[1].up ());
- }
-
- this.focus_widget = this.name_entry;
- }
-
- Value get_value_from_emails (HashMap<int, RowData?> rows) {
- var new_details = new HashSet<EmailFieldDetails>();
-
- foreach (var row_entry in rows.entries) {
- var combo = container_grid.get_child_at (0, row_entry.key) as TypeCombo;
- var entry = container_grid.get_child_at (1, row_entry.key) as Entry;
-
- /* Ignore empty entries. */
- if (entry.get_text () == "")
- continue;
-
- combo.active_descriptor.save_to_field_details (row_entry.value.details);
- var details = new EmailFieldDetails (entry.get_text (), row_entry.value.details.parameters);
- new_details.add (details);
- }
- var new_value = Value (new_details.get_type ());
- new_value.set_object (new_details);
-
- return new_value;
- }
-
- Value get_value_from_phones (HashMap<int, RowData?> rows) {
- var new_details = new HashSet<PhoneFieldDetails>();
-
- foreach (var row_entry in rows.entries) {
- var combo = container_grid.get_child_at (0, row_entry.key) as TypeCombo;
- var entry = container_grid.get_child_at (1, row_entry.key) as Entry;
-
- /* Ignore empty entries. */
- if (entry.get_text () == "")
- continue;
-
- combo.active_descriptor.save_to_field_details (row_entry.value.details);
- var details = new PhoneFieldDetails (entry.get_text (), row_entry.value.details.parameters);
- new_details.add (details);
- }
- var new_value = Value (new_details.get_type ());
- new_value.set_object (new_details);
- return new_value;
- }
-
- Value get_value_from_urls (HashMap<int, RowData?> rows) {
- var new_details = new HashSet<UrlFieldDetails>();
-
- foreach (var row_entry in rows.entries) {
- var entry = container_grid.get_child_at (1, row_entry.key) as Entry;
-
- /* Ignore empty entries. */
- if (entry.get_text () == "")
- continue;
-
- var details = new UrlFieldDetails (entry.get_text (), row_entry.value.details.parameters);
- new_details.add (details);
- }
- var new_value = Value (new_details.get_type ());
- new_value.set_object (new_details);
- return new_value;
- }
-
- Value get_value_from_nickname (HashMap<int, RowData?> rows) {
- var new_value = Value (typeof (string));
- foreach (var row_entry in rows.entries) {
- var entry = container_grid.get_child_at (1, row_entry.key) as Entry;
-
- /* Ignore empty entries. */
- if (entry.get_text () == "")
- continue;
-
- new_value.set_string (entry.get_text ());
- }
- return new_value;
- }
-
- Value get_value_from_birthday (HashMap<int, RowData?> rows) {
- var new_value = Value (typeof (DateTime));
- foreach (var row_entry in rows.entries) {
- var box = container_grid.get_child_at (1, row_entry.key) as Grid;
- var day_spin = box.get_child_at (0, 0) as SpinButton;
- var combo = box.get_child_at (1, 0) as ComboBoxText;
- var year_spin = box.get_child_at (2, 0) as SpinButton;
-
- var bday = new DateTime.local (year_spin.get_value_as_int (),
- combo.get_active () + 1,
- day_spin.get_value_as_int (),
- 0, 0, 0);
- bday = bday.to_utc ();
-
- new_value.set_boxed (bday);
- }
- return new_value;
- }
-
- Value get_value_from_notes (HashMap<int, RowData?> rows) {
- var new_details = new HashSet<NoteFieldDetails>();
-
- foreach (var row_entry in rows.entries) {
- var text = (container_grid.get_child_at (1, row_entry.key) as Bin).get_child () as TextView;
- TextIter start, end;
- text.get_buffer ().get_start_iter (out start);
- text.get_buffer ().get_end_iter (out end);
- var value = text.get_buffer ().get_text (start, end, true);
- if (value != "") {
- var details = new NoteFieldDetails (value, row_entry.value.details.parameters);
- new_details.add (details);
- }
- }
- var new_value = Value (new_details.get_type ());
- new_value.set_object (new_details);
- return new_value;
- }
-
- Value get_value_from_addresses (HashMap<int, RowData?> rows) {
- var new_details = new HashSet<PostalAddressFieldDetails>();
-
- foreach (var row_entry in rows.entries) {
- var combo = container_grid.get_child_at (0, row_entry.key) as TypeCombo;
- var addr_editor = container_grid.get_child_at (1, row_entry.key) as AddressEditor;
- combo.active_descriptor.save_to_field_details (row_entry.value.details);
-
- var new_value = new PostalAddress (addr_editor.details.value.po_box,
- addr_editor.details.value.extension,
- addr_editor.details.value.street,
- addr_editor.details.value.locality,
- addr_editor.details.value.region,
- addr_editor.details.value.postal_code,
- addr_editor.details.value.country,
- addr_editor.details.value.address_format,
- addr_editor.details.id);
- for (int i = 0; i < addr_editor.entries.length; i++)
- new_value.set (AddressEditor.postal_element_props[i], addr_editor.entries[i].get_text ());
-
- var details = new PostalAddressFieldDetails(new_value, row_entry.value.details.parameters);
- new_details.add (details);
- }
- var new_value = Value (new_details.get_type ());
- new_value.set_object (new_details);
- return new_value;
- }
-
- void set_field_changed (int row) {
- foreach (var fields in writable_personas.values) {
- foreach (var entry in fields.entries) {
- if (row in entry.value.rows.keys) {
- if (entry.value.changed)
- return;
-
- entry.value.changed = true;
- return;
- }
- }
- }
- }
-
- new void remove_row (int row) {
- foreach (var fields in writable_personas.values) {
- foreach (var field_entry in fields.entries) {
- foreach (var idx in field_entry.value.rows.keys) {
- if (idx == row) {
- var child = container_grid.get_child_at (0, row);
- child.destroy ();
- child = container_grid.get_child_at (1, row);
- child.destroy ();
- child = container_grid.get_child_at (2, row);
- child.destroy ();
-
- field_entry.value.changed = true;
- field_entry.value.rows.unset (row);
- return;
- }
- }
- }
- }
- }
-
- void attach_row_with_entry (int row, TypeSet type_set, AbstractFieldDetails details, string value, string?
type = null) {
- var combo = new TypeCombo (type_set);
- combo.set_hexpand (false);
- combo.set_active_from_field_details (details);
- if (type != null)
- combo.set_active_from_vcard_type (type);
- combo.set_valign (Align.CENTER);
- container_grid.attach (combo, 0, row, 1, 1);
-
- var value_entry = new Entry ();
- value_entry.set_text (value);
- value_entry.set_hexpand (true);
- container_grid.attach (value_entry, 1, row, 1, 1);
-
- if (type_set == TypeSet.email) {
- value_entry.placeholder_text = _("Add email");
- } else if (type_set == TypeSet.phone) {
- value_entry.placeholder_text = _("Add number");
- }
-
- var delete_button = new Button.from_icon_name ("user-trash-symbolic", IconSize.MENU);
- delete_button.get_accessible ().set_name (_("Delete field"));
- container_grid.attach (delete_button, 2, row, 1, 1);
-
- /* Notify change to upper layer */
- combo.changed.connect ((c) => {
- set_field_changed (get_current_row (combo));
- });
- value_entry.changed.connect (() => {
- set_field_changed (get_current_row (value_entry));
- });
- delete_button.clicked.connect (() => {
- remove_row (get_current_row (delete_button));
- });
-
- if (value == "")
- focus_widget = value_entry;
- }
-
- void attach_row_with_entry_labeled (string title, AbstractFieldDetails? details, string value, int row) {
- var title_label = new Label (title);
- title_label.set_hexpand (false);
- title_label.set_halign (Align.START);
- title_label.margin_end = 6;
- container_grid.attach (title_label, 0, row, 1, 1);
-
- var value_entry = new Entry ();
- value_entry.set_text (value);
- value_entry.set_hexpand (true);
- container_grid.attach (value_entry, 1, row, 1, 1);
-
- var delete_button = new Button.from_icon_name ("user-trash-symbolic", IconSize.MENU);
- delete_button.get_accessible ().set_name (_("Delete field"));
- container_grid.attach (delete_button, 2, row, 1, 1);
-
- /* Notify change to upper layer */
- value_entry.changed.connect (() => {
- set_field_changed (get_current_row (value_entry));
- });
- delete_button.clicked.connect_after (() => {
- remove_row (get_current_row (delete_button));
- });
-
- if (value == "")
- focus_widget = value_entry;
- }
-
- void attach_row_with_text_labeled (string title, AbstractFieldDetails? details, string value, int row) {
- var title_label = new Label (title);
- title_label.set_hexpand (false);
- title_label.set_halign (Align.START);
- title_label.set_valign (Align.START);
- title_label.margin_top = 3;
- title_label.margin_end = 6;
- container_grid.attach (title_label, 0, row, 1, 1);
-
- var sw = new ScrolledWindow (null, null);
- sw.set_shadow_type (ShadowType.OUT);
- sw.set_size_request (-1, 100);
- var value_text = new TextView ();
- value_text.get_buffer ().set_text (value);
- value_text.set_hexpand (true);
- sw.add (value_text);
- container_grid.attach (sw, 1, row, 1, 1);
-
- var delete_button = new Button.from_icon_name ("user-trash-symbolic", IconSize.MENU);
- delete_button.get_accessible ().set_name (_("Delete field"));
- delete_button.set_valign (Align.START);
- container_grid.attach (delete_button, 2, row, 1, 1);
-
- /* Notify change to upper layer */
- value_text.get_buffer ().changed.connect (() => {
- set_field_changed (get_current_row (sw));
- });
- delete_button.clicked.connect (() => {
- remove_row (get_current_row (delete_button));
- /* eventually will need to check against the details type */
- has_notes_row = false;
- });
-
- if (value == "")
- focus_widget = value_text;
- }
-
- delegate void AdjustingDateFn();
-
- void attach_row_for_birthday (string title, AbstractFieldDetails? details, DateTime birthday, int row) {
- var title_label = new Label (title);
- title_label.set_hexpand (false);
- title_label.set_halign (Align.START);
- title_label.margin_end = 6;
- container_grid.attach (title_label, 0, row, 1, 1);
-
- var box = new Grid ();
- box.set_column_spacing (12);
- var day_spin = new SpinButton.with_range (1.0, 31.0, 1.0);
- day_spin.set_digits (0);
- day_spin.numeric = true;
- day_spin.set_value ((double)birthday.to_local ().get_day_of_month ());
-
- var month_combo = new ComboBoxText ();
- var january = new DateTime.local (1, 1, 1, 1, 1, 1);
- for (int i = 0; i < 12; i++) {
- var month = january.add_months (i);
- month_combo.append_text (month.format ("%B"));
- }
- month_combo.set_active (birthday.to_local ().get_month () - 1);
- month_combo.hexpand = true;
-
- var year_spin = new SpinButton.with_range (1800, 3000, 1);
- year_spin.set_digits (0);
- year_spin.numeric = true;
- year_spin.set_value ((double)birthday.to_local ().get_year ());
-
- box.add (day_spin);
- box.add (month_combo);
- box.add (year_spin);
-
- container_grid.attach (box, 1, row, 1, 1);
-
- var delete_button = new Button.from_icon_name ("user-trash-symbolic", IconSize.MENU);
- delete_button.get_accessible ().set_name (_("Delete field"));
- container_grid.attach (delete_button, 2, row, 1, 1);
-
- AdjustingDateFn fn = () => {
- int[] month_of_31 = {3, 5, 8, 10};
- if (month_combo.get_active () in month_of_31) {
- day_spin.set_range (1, 30);
- } else if (month_combo.get_active () == 1) {
- if (year_spin.get_value_as_int () % 4 == 0 &&
- year_spin.get_value_as_int () % 100 != 0) {
- day_spin.set_range (1, 29);
- } else {
- day_spin.set_range (1, 28);
- }
- }
- };
-
- /* Notify change to upper layer */
- day_spin.changed.connect (() => {
- set_field_changed (get_current_row (day_spin));
- });
- month_combo.changed.connect (() => {
- set_field_changed (get_current_row (month_combo));
-
- /* adjusting day_spin value using selected month constraints*/
- fn ();
- });
- year_spin.changed.connect (() => {
- set_field_changed (get_current_row (year_spin));
-
- fn ();
- });
- delete_button.clicked.connect (() => {
- remove_row (get_current_row (delete_button));
- has_birthday_row = false;
- });
- }
-
- void attach_row_for_address (int row, TypeSet type_set, PostalAddressFieldDetails details, string? type =
null) {
- var combo = new TypeCombo (type_set);
- combo.set_hexpand (false);
- combo.set_active_from_field_details (details);
- if (type != null)
- combo.set_active_from_vcard_type (type);
- container_grid.attach (combo, 0, row, 1, 1);
-
- var value_address = new AddressEditor (details);
- container_grid.attach (value_address, 1, row, 1, 1);
-
- var delete_button = new Button.from_icon_name ("user-trash-symbolic", IconSize.MENU);
- delete_button.get_accessible ().set_name (_("Delete field"));
- delete_button.set_valign (Align.START);
- container_grid.attach (delete_button, 2, row, 1, 1);
-
- /* Notify change to upper layer */
- combo.changed.connect (() => {
- set_field_changed (get_current_row (combo));
- });
- value_address.changed.connect (() => {
- set_field_changed (get_current_row (value_address));
- });
- delete_button.clicked.connect (() => {
- remove_row (get_current_row (delete_button));
- });
-
- focus_widget = value_address;
- }
-
- void add_edit_row (Persona? p, string prop_name, ref int row, bool add_empty = false, string? type = null)
{
- /* Here, we will need to add manually every type of field,
- * we're planning to allow editing on */
- string persona_uid = p != null ? p.uid : "null-persona.hack";
- switch (prop_name) {
- case "email-addresses":
- var rows = new HashMap<int, RowData?> ();
- if (add_empty) {
- var detail_field = new EmailFieldDetails ("");
- attach_row_with_entry (row, TypeSet.email, detail_field, "", type);
- rows.set (row, { detail_field });
- row++;
- } else {
- var details = p as EmailDetails;
- if (details != null) {
- var emails = Contacts.Utils.sort_fields<EmailFieldDetails>(details.email_addresses);
- foreach (var email in emails) {
- attach_row_with_entry (row, TypeSet.email, email, email.value);
- rows.set (row, { email });
- row++;
- }
- }
- }
- if (! rows.is_empty) {
- if (writable_personas[persona_uid].has_key (prop_name)) {
- foreach (var entry in rows.entries) {
- writable_personas[persona_uid][prop_name].rows.set (entry.key, entry.value);
- }
- } else {
- writable_personas[persona_uid].set (prop_name, { false, rows });
- }
- }
- break;
- case "phone-numbers":
- var rows = new HashMap<int, RowData?> ();
- if (add_empty) {
- var detail_field = new PhoneFieldDetails ("");
- attach_row_with_entry (row, TypeSet.phone, detail_field, "", type);
- rows.set (row, { detail_field });
- row++;
- } else {
- var details = p as PhoneDetails;
- if (details != null) {
- var phones = Contacts.Utils.sort_fields<PhoneFieldDetails>(details.phone_numbers);
- foreach (var phone in phones) {
- attach_row_with_entry (row, TypeSet.phone, phone, phone.value, type);
- rows.set (row, { phone });
- row++;
- }
- }
- }
- if (! rows.is_empty) {
- if (writable_personas[persona_uid].has_key (prop_name)) {
- foreach (var entry in rows.entries) {
- writable_personas[persona_uid][prop_name].rows.set (entry.key, entry.value);
- }
- } else {
- writable_personas[persona_uid].set (prop_name, { false, rows });
- }
- }
- break;
- case "urls":
- var rows = new HashMap<int, RowData?> ();
- if (add_empty) {
- var detail_field = new UrlFieldDetails ("");
- attach_row_with_entry_labeled (_("Website"), detail_field, "", row);
- rows.set (row, { detail_field });
- row++;
- } else {
- var url_details = p as UrlDetails;
- if (url_details != null) {
- foreach (var url in url_details.urls) {
- attach_row_with_entry_labeled (_("Website"), url, url.value, row);
- rows.set (row, { url });
- row++;
- }
- }
- }
- if (! rows.is_empty) {
- if (writable_personas[persona_uid].has_key (prop_name)) {
- foreach (var entry in rows.entries) {
- writable_personas[persona_uid][prop_name].rows.set (entry.key, entry.value);
- }
- } else {
- writable_personas[persona_uid].set (prop_name, { false, rows });
- }
- }
- break;
- case "nickname":
- var rows = new HashMap<int, RowData?> ();
- if (add_empty) {
- attach_row_with_entry_labeled (_("Nickname"), null, "", row);
- rows.set (row, { null });
- row++;
- } else {
- var name_details = p as NameDetails;
- if (name_details != null) {
- if (is_set (name_details.nickname)) {
- attach_row_with_entry_labeled (_("Nickname"), null, name_details.nickname, row);
- rows.set (row, { null });
- row++;
- }
- }
- }
- if (! rows.is_empty) {
- has_nickname_row = true;
- var delete_button = container_grid.get_child_at (2, row - 1) as Button;
- delete_button.clicked.connect (() => {
- has_nickname_row = false;
- });
-
- if (writable_personas[persona_uid].has_key (prop_name)) {
- foreach (var entry in rows.entries) {
- writable_personas[persona_uid][prop_name].rows.set (entry.key, entry.value);
- }
- } else {
- writable_personas[persona_uid].set (prop_name, { false, rows });
- }
- }
- break;
- case "birthday":
- var rows = new HashMap<int, RowData?> ();
- if (add_empty) {
- var today = new DateTime.now_local ();
- attach_row_for_birthday (_("Birthday"), null, today, row);
- rows.set (row, { null });
- row++;
- } else {
- var birthday_details = p as BirthdayDetails;
- if (birthday_details != null) {
- if (birthday_details.birthday != null) {
- attach_row_for_birthday (_("Birthday"), null, birthday_details.birthday, row);
- rows.set (row, { null });
- row++;
- }
- }
- }
- if (! rows.is_empty) {
- has_birthday_row = true;
- writable_personas[persona_uid].set (prop_name, { add_empty, rows });
- }
- break;
- case "notes":
- var rows = new HashMap<int, RowData?> ();
- if (add_empty) {
- var detail_field = new NoteFieldDetails ("");
- attach_row_with_text_labeled (_("Note"), detail_field, "", row);
- rows.set (row, { detail_field });
- row++;
- } else {
- var note_details = p as NoteDetails;
- if (note_details != null || add_empty) {
- foreach (var note in note_details.notes) {
- attach_row_with_text_labeled (_("Note"), note, note.value, row);
- rows.set (row, { note });
- row++;
- }
- }
- }
- if (! rows.is_empty) {
- has_notes_row = true;
- if (writable_personas[persona_uid].has_key (prop_name)) {
- foreach (var entry in rows.entries) {
- writable_personas[persona_uid][prop_name].rows.set (entry.key, entry.value);
- }
- } else {
- writable_personas[persona_uid].set (prop_name, { false, rows });
- }
- }
- break;
- case "postal-addresses":
- var rows = new HashMap<int, RowData?> ();
- if (add_empty) {
- var detail_field = new PostalAddressFieldDetails (
- new PostalAddress (null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null));
- attach_row_for_address (row, TypeSet.general, detail_field, type);
- rows.set (row, { detail_field });
- row++;
- } else {
- var address_details = p as PostalAddressDetails;
- if (address_details != null) {
- foreach (var addr in address_details.postal_addresses) {
- attach_row_for_address (row, TypeSet.general, addr, type);
- rows.set (row, { addr });
- row++;
- }
- }
- }
- if (! rows.is_empty) {
- if (writable_personas[persona_uid].has_key (prop_name)) {
- foreach (var entry in rows.entries) {
- writable_personas[persona_uid][prop_name].rows.set (entry.key, entry.value);
- }
- } else {
- writable_personas[persona_uid].set (prop_name, { false, rows });
- }
- }
- break;
- }
- }
-
- int get_current_row (Widget child) {
- int row;
-
- container_grid.child_get (child, "top-attach", out row);
- return row;
- }
-
- void insert_row_at (int idx) {
- foreach (var field_maps in writable_personas.values) {
- foreach (var field in field_maps.values) {
- foreach (var row in field.rows.keys) {
- if (row >= idx) {
- var new_rows = new HashMap <int, RowData?> ();
- foreach (var old_row in field.rows.keys) {
- /* move all rows +1 */
- new_rows.set (old_row + 1, field.rows[old_row]);
- }
- field.rows = new_rows;
- break;
- }
- }
- }
- }
- foreach (var entry in writable_personas.entries) {
- foreach (var field_entry in entry.value.entries) {
- foreach (var row in field_entry.value.rows.keys) {
- if (row >= idx) {
- var new_rows = new HashMap <int, RowData?> ();
- foreach (var old_row in field_entry.value.rows.keys) {
- new_rows.set (old_row + 1, field_entry.value.rows[old_row]);
- }
- field_entry.value.rows = new_rows;
- break;
- }
- }
- }
- }
- container_grid.insert_row (idx);
- }
-
- private void on_container_grid_size_allocate (Allocation alloc) {
- if (this.focus_widget != null && this.focus_widget is Widget) {
- this.focus_widget.grab_focus ();
- this.focus_widget = null;
- }
- }
-
- public HashMap<string, PropertyData?> properties_changed () {
- var props_set = new HashMap<string, PropertyData?> ();
-
- foreach (var entry in writable_personas.entries) {
- foreach (var field_entry in entry.value.entries) {
- if (field_entry.value.changed && !props_set.has_key (field_entry.key)) {
- PropertyData p = PropertyData ();
- p.persona = null;
- if (individual != null) {
- p.persona = Contacts.Utils.find_persona_from_uid (individual, entry.key);
- }
-
- switch (field_entry.key) {
- case "email-addresses":
- p.value = get_value_from_emails (field_entry.value.rows);
- break;
- case "phone-numbers":
- p.value = get_value_from_phones (field_entry.value.rows);
- break;
- case "urls":
- p.value = get_value_from_urls (field_entry.value.rows);
- break;
- case "nickname":
- p.value = get_value_from_nickname (field_entry.value.rows);
- break;
- case "birthday":
- p.value = get_value_from_birthday (field_entry.value.rows);
- break;
- case "notes":
- p.value = get_value_from_notes (field_entry.value.rows);
- break;
- case "postal-addresses":
- p.value = get_value_from_addresses (field_entry.value.rows);
- break;
- }
-
- props_set.set (field_entry.key, p);
- }
- }
- }
-
- return props_set;
- }
-
- public HashSet<Persona> get_unlink_personas () {
- return unlink_personas;
- }
-
- public void add_new_row_for_property (Persona? persona, string prop_name, string? type = null) {
- int next_idx = 0;
- foreach (var fields in writable_personas.values) {
- if (fields.has_key (prop_name)) {
- foreach (var idx in fields[prop_name].rows.keys) {
- if (idx < last_row)
- next_idx = idx > next_idx ? idx : next_idx;
- }
- break;
- }
- }
- next_idx = (next_idx == 0 ? last_row : next_idx) + 1;
- insert_row_at (next_idx);
- add_edit_row (persona, prop_name, ref next_idx, true, type);
- last_row++;
- container_grid.show_all ();
+ show_all ();
}
// Creates the contact's current avatar in a big button on top of the Editor
- private void create_avatar_button () {
+ private Widget create_avatar_button () {
this.avatar = new Avatar (PROFILE_SIZE, this.individual);
var button = new Button ();
@@ -905,65 +53,36 @@ public class Contacts.ContactEditor : ContactForm {
button.image = this.avatar;
button.clicked.connect (on_avatar_button_clicked);
- this.container_grid.attach (button, 0, 0, 1, 3);
+ return button;
}
// Show the avatar popover when the avatar is clicked
private void on_avatar_button_clicked (Button avatar_button) {
- var popover = new AvatarSelector (avatar_button, this.individual);
- popover.set_avatar.connect ( (icon) => {
- this.avatar.set_data ("value", icon);
- this.avatar.set_data ("changed", true);
-
- Gdk.Pixbuf? a_pixbuf = null;
- try {
- var stream = (icon as LoadableIcon).load (PROFILE_SIZE, null);
- a_pixbuf = new Gdk.Pixbuf.from_stream_at_scale (stream, PROFILE_SIZE, PROFILE_SIZE, true);
- } catch {
- }
-
- this.avatar.set_pixbuf (a_pixbuf);
- });
- popover.show();
- }
-
- public bool avatar_changed () {
- return this.avatar.get_data<bool> ("changed");
- }
-
- public Value get_avatar_value () {
- GLib.Icon icon = this.avatar.get_data<GLib.Icon> ("value");
- Value v = Value (icon.get_type ());
- v.set_object (icon);
- return v;
+ if (this.avatar_selector == null)
+ this.avatar_selector = new AvatarSelector (avatar_button, this.individual);
+ this.avatar_selector.show();
}
// Creates the big name entry on the top
- private void create_name_entry () {
+ private Widget create_name_entry () {
+ NameDetails name = this.individual as NameDetails;
this.name_entry = new Entry ();
this.name_entry.hexpand = true;
this.name_entry.valign = Align.CENTER;
this.name_entry.placeholder_text = _("Add name");
- this.name_entry.set_data ("changed", false);
- if (this.individual != null)
- this.name_entry.text = this.individual.display_name;
+ // Get primary persona from this.individual
+ this.name_entry.text = name.full_name;
- /* structured name change */
this.name_entry.changed.connect (() => {
- this.name_entry.set_data ("changed", true);
- });
-
- this.container_grid.attach (this.name_entry, 1, 0, 2, 3);
- }
-
- public bool name_changed () {
- return this.name_entry.get_data<bool> ("changed");
- }
+ foreach (var p in this.individual.personas) {
+ var name_p = p as NameDetails;
+ if (name_p != null) {
+ name_p.full_name = this.name_entry.get_text ();
+ }
+ }
+ });
- public Value get_full_name_value () {
- Value v = Value (typeof (string));
- v.set_string (this.name_entry.get_text ());
- return v;
+ return this.name_entry;
}
}
diff --git a/src/contacts-contact-pane.vala b/src/contacts-contact-pane.vala
index adaafeb..44db168 100644
--- a/src/contacts-contact-pane.vala
+++ b/src/contacts-contact-pane.vala
@@ -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 {
public ContactPane (Window parent_window, Store contacts_store) {
this.parent_window = parent_window;
this.store = contacts_store;
-
- this.edit_contact_actions.add_action_entries (action_entries, this);
}
public void add_suggestion (Individual i) {
@@ -115,7 +100,7 @@ public class Contacts.ContactPane : Stack {
show_contact_sheet ();
} else {
remove_contact_sheet ();
- set_visible_child (this.none_selected_page);
+ this.stack.set_visible_child (this.none_selected_page);
}
}
@@ -125,7 +110,7 @@ public class Contacts.ContactPane : Stack {
remove_contact_sheet();
this.sheet = new ContactSheet (this.individual, this.store);
this.contact_sheet_page.add (this.sheet);
- set_visible_child (this.contact_sheet_page);
+ this.stack.set_visible_child (this.contact_sheet_page);
var matches = this.store.aggregator.get_potential_matches (this.individual, MatchResult.HIGH);
foreach (var i in matches.keys) {
@@ -149,26 +134,9 @@ public class Contacts.ContactPane : Stack {
}
private void create_contact_editor () {
- if (this.editor != null)
- remove_contact_editor ();
-
- this.editor = new ContactEditor (this.individual, this.store, this.edit_contact_actions);
-
- this.editor.linked_button.clicked.connect (linked_accounts);
- this.editor.remove_button.clicked.connect (delete_contact);
-
- /* enable/disable actions*/
- var birthday_action = this.edit_contact_actions.lookup_action ("add.birthday") as SimpleAction;
- this.editor.bind_property ("has-birthday-row", birthday_action, "enabled",
- BindingFlags.SYNC_CREATE | BindingFlags.INVERT_BOOLEAN);
-
- var nickname_action = this.edit_contact_actions.lookup_action ("add.nickname") as SimpleAction;
- this.editor.bind_property ("has-nickname-row", nickname_action, "enabled",
- BindingFlags.SYNC_CREATE | BindingFlags.INVERT_BOOLEAN);
+ remove_contact_editor ();
- var notes_action = this.edit_contact_actions.lookup_action ("add.notes") as SimpleAction;
- this.editor.bind_property ("has-notes-row", notes_action, "enabled",
- BindingFlags.SYNC_CREATE | BindingFlags.INVERT_BOOLEAN);
+ this.editor = new ContactEditor (this.individual, store.aggregator);
this.contact_editor_page.add (this.editor);
}
@@ -181,137 +149,90 @@ public class Contacts.ContactPane : Stack {
this.editor = null;
}
- void on_add_detail (GLib.SimpleAction action, GLib.Variant? parameter) {
- var tok = action.name.split (".");
-
- if (tok[0] == "add") {
- editor.add_new_row_for_property (Contacts.Utils.find_primary_persona (individual),
- tok[1],
- tok.length > 2 ? tok[2].up () : null);
- }
- }
-
- private void linked_accounts () {
- var dialog = new LinkedPersonasDialog (this.parent_window, this.store, individual);
- if (dialog.run () == ResponseType.CLOSE && dialog.any_unlinked) {
- /* update edited contact if any_unlinked */
- stop_editing ();
- start_editing ();
- }
- dialog.destroy ();
- }
-
- void delete_contact () {
- if (individual != null) {
- will_delete (individual);
- }
- }
-
- public void start_editing() {
+ private void start_editing() {
if (this.on_edit_mode || this.individual == null)
return;
this.on_edit_mode = true;
- remove_contact_sheet ();
create_contact_editor ();
- set_visible_child (this.contact_editor_page);
+ this.stack.set_visible_child (this.contact_editor_page);
}
- public void stop_editing (bool drop_changes = false) {
+ public void stop_editing (bool cancel = false) {
if (!this.on_edit_mode)
return;
this.on_edit_mode = false;
- /* saving changes */
- if (!drop_changes)
- save_editor_changes.begin ();
-
remove_contact_editor ();
- if (this.individual != null)
- show_contact_sheet ();
- else
- set_visible_child (this.none_selected_page);
- }
-
- private async void save_editor_changes () {
- foreach (var prop in this.editor.properties_changed ().entries) {
- try {
- yield Contacts.Utils.set_persona_property (prop.value.persona, prop.key, prop.value.value);
- } catch (Error e) {
- show_message (e.message);
+ if (cancel) {
+ var fake_individual = individual as FakeIndividual;
+ if (fake_individual != null && fake_individual.real_individual != null) {
+ // Reset individual on to the real one
+ this.individual = fake_individual.real_individual;
+ this.stack.set_visible_child (this.contact_sheet_page);
+ } else {
+ this.stack.set_visible_child (this.none_selected_page);
}
+ return;
}
- if (this.editor.name_changed ()) {
- var v = this.editor.get_full_name_value ();
- try {
- yield Contacts.Utils.set_individual_property (individual, "full-name", v);
- display_name_changed (v.get_string ());
- } catch (Error e) {
- show_message (e.message);
- }
- }
+ /* Save changes if editing wasn't canceled */
+ apply_changes.begin ();
+ }
- if (this.editor.avatar_changed ()) {
- var v = this.editor.get_avatar_value ();
- try {
- yield Contacts.Utils.set_individual_property (individual, "avatar", v);
- } catch (Error e) {
- show_message (e.message);
- }
+ private async void apply_changes () {
+ /* Show fake contact to the user */
+ /* TODO: block changes to fake contact */
+ show_contact_sheet ();
+ var fake_individual = individual as FakeIndividual;
+ if (fake_individual != null && fake_individual.real_individual == null) {
+ // Create a new persona in the primary store based on the fake persona
+ yield create_contact (fake_individual.primary_persona);
+ } else {
+ yield fake_individual.apply_changes_to_real ();
+ /* Todo: we need to check if the changes where applied to the contact */
+ this.individual = fake_individual.real_individual;
}
- /* unlink personas */
- if (this.editor.get_unlink_personas ().size > 0) {
- var operation = new UnLinkOperation (this.store);
- operation.do.begin (this.individual, this.editor.get_unlink_personas ());
- }
+ /* Replace fake contact with real contact */
+ show_contact_sheet ();
}
- public void new_contact () {
- this.on_edit_mode = true;
- this.individual = null;
- remove_contact_sheet ();
- create_contact_editor ();
- set_visible_child (this.contact_editor_page);
+ public void edit_contact () {
+ this.individual = new FakeIndividual.from_real (this.individual);
+ start_editing ();
}
- // Creates a new contact from the details in the ContactEditor
- public async void create_contact () {
+ public void new_contact () {
var details = new HashTable<string, Value?> (str_hash, str_equal);
+ string[] writeable_properties;
+ // TODO: make sure we have a primary_store
+ if (this.store.aggregator.primary_store != null) {
+ // FIXME: We shouldn't use this list but there isn't an other way to find writeable_properties, and we
should expect that all properties are writeable
+ writeable_properties = this.store.aggregator.primary_store.always_writeable_properties;
+ } else {
+ writeable_properties = {};
+ }
- // Collect the details from the editor
- if (editor.name_changed ())
- details["full-name"] = this.editor.get_full_name_value ();
-
- if (editor.avatar_changed ())
- details["avatar"] = this.editor.get_avatar_value ();
-
- foreach (var prop in this.editor.properties_changed ().entries)
- details[prop.key] = prop.value.value;
+ var fake_persona = new FakePersona (FakePersonaStore.the_store(), writeable_properties, details);
+ var fake_personas = new HashSet<FakePersona> ();
+ fake_personas.add (fake_persona);
+ this.individual = new FakeIndividual(fake_personas);
- // Leave edit mode
- stop_editing (true);
+ start_editing ();
+ }
- if (details.size () == 0) {
- show_message_dialog (_("You need to enter some data"));
- return;
- }
+ // Create a new contact from the FakePersona
+ public async void create_contact (FakePersona fake_persona) {
+ var details = fake_persona.get_details ();
if (this.store.aggregator.primary_store == null) {
show_message_dialog (_("No primary addressbook configured"));
return;
}
- // Create a FakeContact temporary persona so we can show it already to the user
- var fake_persona = new FakePersona (FakePersonaStore.the_store(), details);
- var fake_personas = new HashSet<Persona> ();
- fake_personas.add (fake_persona);
- var fake_individual = new Individual(fake_personas);
- this.parent_window.set_shown_contact (fake_individual);
-
// Create the contact
var primary_store = this.store.aggregator.primary_store;
Persona? persona = null;
@@ -325,6 +246,7 @@ public class Contacts.ContactPane : Stack {
// Now show the real persona to the user
var individual = persona.individual;
+
if (individual != null) {
//FIXME: This causes a flicker, especially visibile when a avatar is set
this.parent_window.set_shown_contact (individual);
@@ -345,12 +267,6 @@ public class Contacts.ContactPane : Stack {
dialog.destroy ();
}
- private void show_message (string message) {
- var notification = new InAppNotification (message);
- notification.show ();
- this.parent_window.add_notification (notification);
- }
-
private void remove_suggestion_grid () {
if (this.suggestion_grid == null)
return;
diff --git a/src/contacts-contact-sheet.vala b/src/contacts-contact-sheet.vala
index 0f6ebe8..b6c2249 100644
--- a/src/contacts-contact-sheet.vala
+++ b/src/contacts-contact-sheet.vala
@@ -24,33 +24,61 @@ using Gee;
*
* (Note: to edit a contact, use the {@link ContactEditor} instead.
*/
-public class Contacts.ContactSheet : ContactForm {
+public class Contacts.ContactSheet : Grid {
+ private int last_row = 0;
+ private Individual individual;
+ public bool narrow { get; set; default = true; }
+
+ private const string[] SORTED_PROPERTIES = {
+ "email-addresses",
+ "phone-numbers",
+ "im-addresses",
+ "urls",
+ "nickname",
+ "birthday",
+ "postal-addresses",
+ "notes"
+ };
+
public ContactSheet (Individual individual, Store store) {
+ Object (row_spacing: 12, column_spacing: 12);
this.individual = individual;
- this.store = store;
this.individual.notify.connect (update);
this.individual.personas_changed.connect (update);
- this.store.quiescent.connect (update);
+ store.quiescent.connect (update);
update ();
}
+ private Label create_persona_store_label (Persona p) {
+ var store_name = new Label (Utils.format_persona_store_name_for_contact (p));
+ var attrList = new Pango.AttrList ();
+ attrList.insert (Pango.attr_weight_new (Pango.Weight.BOLD));
+ store_name.set_attributes (attrList);
+ store_name.set_halign (Align.START);
+ store_name.set_ellipsize (Pango.EllipsizeMode.MIDDLE);
+
+ return store_name;
+ }
+
private Button create_button (string icon) {
var button = new Button.from_icon_name (icon, IconSize.BUTTON);
button.set_halign (Align.END);
- button.get_style_context ().add_class ("contacts-flatten");
+ button.get_style_context ().add_class ("flatten");
return button;
}
- void add_row_with_label (string label_value, string value, Widget? buttons = null) {
+ void add_row_with_label (string label_value, string value, Widget? btn1 = null, Widget? btn2 =null) {
+ if (value == "" || value == null)
+ return;
var type_label = new Label (label_value);
type_label.xalign = 1.0f;
type_label.set_halign (Align.END);
type_label.set_valign (Align.CENTER);
type_label.get_style_context ().add_class ("dim-label");
- this.container_grid.attach (type_label, 0, this.last_row, 1, 1);
+ this.attach (type_label, 0, this.last_row, 1, 1);
var value_label = new Label (value);
value_label.set_line_wrap (true);
@@ -60,47 +88,52 @@ public class Contacts.ContactSheet : ContactForm {
value_label.wrap_mode = Pango.WrapMode.CHAR;
value_label.set_selectable (true);
- if (buttons != null) {
+ if (btn1 != null || btn2 !=null) {
var value_box = new Box(Orientation.HORIZONTAL, 12);
value_box.pack_start(value_label, false, false, 0);
- value_box.pack_end(buttons, false, false, 0);
- this.container_grid.attach (value_box, 1, this.last_row, 1, 1);
+
+ if (btn1 != null)
+ value_box.pack_end(btn1, false, false, 0);
+ if (btn2 != null)
+ value_box.pack_end(btn2, false, false, 0);
+ this.attach (value_box, 1, this.last_row, 1, 1);
} else {
- this.container_grid.attach (value_label, 1, this.last_row, 1, 1);
+ this.attach (value_label, 1, this.last_row, 1, 1);
}
this.last_row++;
}
private void update () {
this.last_row = 0;
- this.container_grid.foreach ((child) => this.container_grid.remove (child));
+ this.foreach ((child) => this.remove (child));
var image_frame = new Avatar (PROFILE_SIZE, this.individual);
image_frame.set_vexpand (false);
image_frame.set_valign (Align.START);
- this.container_grid.attach (image_frame, 0, 0, 1, 3);
+
+ this.attach (image_frame, 0, 0, 1, 3);
create_name_label ();
this.last_row += 3; // Name/Avatar takes up 3 rows
- var personas = Contacts.Utils.get_personas_for_display (this.individual);
+ var personas = Utils.get_personas_for_display (this.individual);
/* Cause personas are sorted properly I can do this */
foreach (var p in personas) {
bool is_first_persona = (this.last_row == 3);
int persona_store_pos = this.last_row;
if (!is_first_persona) {
- this.container_grid.attach (create_persona_store_label (p), 0, this.last_row, 3);
+ this.attach (create_persona_store_label (p), 0, this.last_row, 3);
this.last_row++;
}
- foreach (var prop in ContactForm.SORTED_PROPERTIES)
+ foreach (var prop in SORTED_PROPERTIES)
add_row_for_property (p, prop);
// Nothing to show in the persona: don't mention it
bool is_empty_persona = (this.last_row == persona_store_pos + 1);
if (!is_first_persona && is_empty_persona) {
- this.container_grid.remove_row (persona_store_pos);
+ this.remove_row (persona_store_pos);
this.last_row--;
}
}
@@ -119,7 +152,7 @@ public class Contacts.ContactSheet : ContactForm {
name_label.ellipsize = Pango.EllipsizeMode.END;
name_label.xalign = 0f;
name_label.selectable = true;
- this.container_grid.attach (name_label, 1, 0, 1, 3);
+ this.attach (name_label, 1, 0, 1, 3);
update_name_label (name_label);
this.individual.notify["display-name"].connect ((obj, spec) => {
update_name_label (name_label);
@@ -161,7 +194,7 @@ public class Contacts.ContactSheet : ContactForm {
private void add_emails (Persona persona) {
var details = persona as EmailDetails;
if (details != null) {
- var emails = Contacts.Utils.sort_fields<EmailFieldDetails>(details.email_addresses);
+ var emails = Utils.sort_fields<EmailFieldDetails>(details.email_addresses);
foreach (var email in emails) {
var button = create_button ("mail-unread-symbolic");
button.clicked.connect (() => {
@@ -175,7 +208,7 @@ public class Contacts.ContactSheet : ContactForm {
private void add_phone_nrs (Persona persona) {
var phone_details = persona as PhoneDetails;
if (phone_details != null) {
- var phones = Contacts.Utils.sort_fields<PhoneFieldDetails>(phone_details.phone_numbers);
+ var phones = Utils.sort_fields<PhoneFieldDetails>(phone_details.phone_numbers);
foreach (var phone in phones) {
#if HAVE_TELEPATHY
if (this.store.caller_account != null) {
@@ -204,7 +237,7 @@ public class Contacts.ContactSheet : ContactForm {
if (persona is Tpf.Persona) {
var button = create_button ("user-available-symbolic");
button.clicked.connect (() => {
- var im_persona = Contacts.Utils.find_im_persona (individual, protocol, id.value);
+ var im_persona = Utils.find_im_persona (individual, protocol, id.value);
if (im_persona != null) {
var type = im_persona.presence_type;
if (type != PresenceType.UNSET && type != PresenceType.ERROR &&
@@ -280,7 +313,7 @@ public class Contacts.ContactSheet : ContactForm {
var addr_details = persona as PostalAddressDetails;
if (addr_details != null) {
foreach (var addr in addr_details.postal_addresses) {
- var all_strs = string.joinv ("\n", Contacts.Utils.format_address (addr.value));
+ var all_strs = string.joinv ("\n", Utils.format_address (addr.value));
add_row_with_label (TypeSet.general.format_type (addr), all_strs);
}
}
diff --git a/src/contacts-editor-persona.vala b/src/contacts-editor-persona.vala
new file mode 100644
index 0000000..14deec0
--- /dev/null
+++ b/src/contacts-editor-persona.vala
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ * Author: Julian Sparber <julian sparber puri sm>
+ *
+ * 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 Gtk;
+using Folks;
+using Gee;
+
+/**
+ * A widget representing a persona in the {@link ContactEditor}.
+ */
+public class Contacts.EditorPersona : Box {
+ private const GLib.ActionEntry[] action_entries = {
+ { "change-addressbook", change_addressbook },
+ };
+
+ // List of important properties and a list of secoundary properties
+ private const string[] PROPERTIES = {
+ "email-addresses",
+ "phone-numbers"
+ };
+ private const string[] OTHER_PROPERTIES = {
+ "im-addresses",
+ "urls",
+ "nickname",
+ "birthday",
+ "postal-addresses",
+ "notes"
+ };
+
+ private Persona persona;
+ private Box header;
+ private ListBox content;
+
+ private IndividualAggregator aggregator;
+
+ construct {
+ this.header = new Box (Orientation.HORIZONTAL, 0);
+ add (this.header);
+
+ var frame = new Frame (null);
+ this.content = new ListBox ();
+ this.content.set_header_func (list_box_update_header_func);
+ frame.add (this.content);
+ add (frame);
+
+ SimpleActionGroup actions = new SimpleActionGroup ();
+ actions.add_action_entries (action_entries, this);
+ this.insert_action_group ("persona", actions);
+ }
+
+ private void list_box_update_header_func (ListBoxRow row, ListBoxRow? before) {
+ if (before == null) {
+ row.set_header (null);
+ return;
+ }
+
+ if (row.get_header () == null) {
+ var header = new Separator (Orientation.HORIZONTAL);
+ header.show ();
+ row.set_header (header);
+ }
+ }
+
+ public EditorPersona (Persona persona, IndividualAggregator aggregator) {
+ Object (orientation: Orientation.VERTICAL, spacing: 6);
+ this.persona = persona;
+ this.aggregator = aggregator;
+ create_label ();
+ /* TODO: implement the possibility of changing the addressbook of a persona
+ create_button (); */
+
+ // Add most important properites
+ foreach (var property in PROPERTIES) {
+ debug ("Create property entry for %s", property);
+ var rows = new EditorProperty (persona, property);
+ foreach (var row in rows) {
+ row.show_with_animation (false);
+ connect_row (row);
+ this.content.add (row);
+ }
+ }
+ // Add a row with a button to show all properties
+ ListBoxRow show_all_row = new ListBoxRow ();
+ show_all_row.set_selectable (false);
+ // Add less important property when the show_more button is clicked
+ this.content.row_activated.connect ((current_row) => {
+ if (current_row == show_all_row) {
+ foreach (var property in OTHER_PROPERTIES) {
+ debug ("Create property entry for %s", property);
+ var rows = new EditorProperty (persona, property);
+ foreach (var row in rows) {
+ connect_row (row);
+ this.content.add (row);
+ row.show_with_animation ();
+ }
+ }
+ show_all_row.destroy ();
+ }
+ });
+ Image show_all = new Image.from_icon_name ("view-more-symbolic", IconSize.BUTTON);
+ show_all.margin = 12;
+ show_all_row.add (show_all);
+ this.content.add (show_all_row);
+ }
+
+ private void connect_row (EditorPropertyRow row) {
+ row.notify["is-empty"].connect ( () => {
+ var empty_rows_count = this.count_empty_rows (row.ptype);
+ if (row.is_empty) {
+ // destroy all rows of our type which is not us
+ this.destroy_empty_rows (row, row.ptype);
+ }
+ if (!row.is_empty && empty_rows_count == 0) {
+ // We are sure that we only created one new row
+ var new_rows = new EditorProperty (persona, row.ptype, true);
+ if (new_rows.size > 0) {
+ this.content.insert (new_rows[0], row.get_index () + 1);
+ connect_row (new_rows[0]);
+ new_rows[0].show_with_animation ();
+ } else {
+ debug ("Couldn't add new row with type %s", row.ptype);
+ }
+ }
+ });
+ }
+
+ private uint count_empty_rows (string type) {
+ uint count = 0;
+ foreach (var row in this.content.get_children ()) {
+ var prop = (row as EditorPropertyRow);
+ if (prop != null && !prop.is_removed && prop.is_empty && prop.ptype == type) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private void destroy_empty_rows (ListBoxRow current_row, string type) {
+ foreach (var row in this.content.get_children ()) {
+ if (current_row != row) {
+ var prop = (row as EditorPropertyRow);
+ if (prop != null && !prop.is_removed && prop.is_empty && prop.ptype == type) {
+ prop.remove ();
+ }
+ }
+ }
+ }
+
+ private void change_addressbook () {
+ /* Not yet implemented */
+ }
+
+ private void create_label () {
+ string title = "";
+ FakePersona fake_persona = this.persona as FakePersona;
+ if (fake_persona != null && fake_persona.real_persona != null) {
+ title = fake_persona.real_persona.store.display_name;
+ } else {
+ title = this.aggregator.primary_store.display_name;
+ }
+
+ Label addressbook = new Label (title);
+ this.header.pack_start (addressbook, false, false, 0);
+ }
+
+ private void create_button () {
+ var image = new Image.from_icon_name ("emblem-system-symbolic", IconSize.BUTTON);
+ var button = new MenuButton ();
+ button.set_image (image);
+ var builder = new Builder.from_resource ("/org/gnome/Contacts/ui/contacts-editor-menu.ui");
+ var menu = builder.get_object ("editor_menu") as Widget;
+ button.set_popover (menu);
+ this.header.pack_end (button, false, false, 0);
+ }
+}
diff --git a/src/contacts-editor-property.vala b/src/contacts-editor-property.vala
new file mode 100644
index 0000000..00dfe4a
--- /dev/null
+++ b/src/contacts-editor-property.vala
@@ -0,0 +1,629 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ * Author: Julian Sparber <julian sparber puri sm>
+ *
+ * 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 Gtk;
+using Folks;
+using Gee;
+
+
+public class Contacts.BirthdayEditor : Hdy.Dialog {
+ private SpinButton day_spin;
+ private ComboBoxText month_combo;
+ private SpinButton year_spin;
+ public bool is_set { get; set; default = false; }
+
+ public signal void changed ();
+ delegate void AdjustingDateFn ();
+
+ public DateTime get_birthday () {
+ return new DateTime.local (year_spin.get_value_as_int (),
+ month_combo.get_active () + 1,
+ day_spin.get_value_as_int (),
+ 0, 0, 0).to_utc ();
+ }
+
+ public BirthdayEditor (Window window, DateTime birthday) {
+ Object (transient_for: window, use_header_bar: 1);
+ day_spin = new SpinButton.with_range (1.0, 31.0, 1.0);
+ day_spin.set_digits (0);
+ day_spin.numeric = true;
+ day_spin.set_value ((double)birthday.to_local ().get_day_of_month ());
+
+ month_combo = new ComboBoxText ();
+ var january = new DateTime.local (1, 1, 1, 1, 1, 1);
+ for (int i = 0; i < 12; i++) {
+ var month = january.add_months (i);
+ month_combo.append_text (month.format ("%B"));
+ }
+ month_combo.set_active (birthday.to_local ().get_month () - 1);
+ month_combo.hexpand = true;
+
+ year_spin = new SpinButton.with_range (1800, 3000, 1);
+ year_spin.set_digits (0);
+ year_spin.numeric = true;
+ year_spin.set_value ((double)birthday.to_local ().get_year ());
+
+ // Create grid and labels
+ Box box = new Box (Orientation.VERTICAL, 12);
+ Grid grid = new Grid ();
+ grid.set_column_spacing (12);
+ grid.set_row_spacing (12);
+ Label day = new Label(_("Day"));
+ day.set_halign (Align.END);
+ grid.attach (day, 0, 0);
+ grid.attach (day_spin, 1, 0);
+ Label month = new Label(_("Month"));
+ month.set_halign (Align.END);
+ grid.attach (month, 0, 1);
+ grid.attach (month_combo, 1, 1);
+ Label year = new Label(_("Year"));
+ year.set_halign (Align.END);
+ grid.attach (year, 0, 2);
+ grid.attach (year_spin, 1, 2);
+ box.pack_start (grid);
+
+ var content = this.get_content_area ();
+ content.set_valign (Align.CENTER);
+ content.add (box);
+
+ this.title = _("Change Address Book");
+ add_buttons (_("Set"), ResponseType.OK,
+ _("Cancel"), ResponseType.CANCEL,
+ null);
+ var ok_button = this.get_widget_for_response (ResponseType.OK);
+ ok_button.get_style_context ().add_class ("suggested-action");
+ this.response.connect ((id) => {
+ switch (id) {
+ case ResponseType.OK:
+ this.is_set = true;
+ changed ();
+ break;
+ case ResponseType.CANCEL:
+ break;
+ }
+ this.destroy ();
+ });
+
+ box.margin = 12;
+ box.show_all ();
+
+ AdjustingDateFn fn = () => {
+ int[] month_of_31 = {3, 5, 8, 10};
+ if (month_combo.get_active () in month_of_31) {
+ day_spin.set_range (1, 30);
+ } else if (month_combo.get_active () == 1) {
+ if (year_spin.get_value_as_int () % 4 == 0 &&
+ year_spin.get_value_as_int () % 100 != 0) {
+ day_spin.set_range (1, 29);
+ } else {
+ day_spin.set_range (1, 28);
+ }
+ }
+ };
+
+ /* adjusting day_spin value using selected month/year constraints*/
+ fn ();
+
+ month_combo.changed.connect (() => {
+ /* adjusting day_spin value using selected month constraints*/
+ fn ();
+ });
+ year_spin.value_changed.connect (() => {
+ /* adjusting day_spin value using selected year constraints*/
+ fn ();
+ });
+ }
+}
+
+public class Contacts.AddressEditor : Box {
+ private Entry? entries[7]; /* must be the number of elements in postal_element_props */
+
+ private const string[] postal_element_props = {"street", "extension", "locality", "region", "postal_code",
"po_box", "country"};
+ private static string[] postal_element_names = {_("Street"), _("Extension"), _("City"),
_("State/Province"), _("Zip/Postal Code"), _("PO box"), _("Country")};
+
+ public signal void changed ();
+
+ public AddressEditor (PostalAddressFieldDetails details) {
+ set_hexpand (true);
+ set_orientation (Orientation.VERTICAL);
+
+ for (int i = 0; i < entries.length; i++) {
+ string postal_part;
+ details.value.get (AddressEditor.postal_element_props[i], out postal_part);
+
+ entries[i] = new Entry ();
+ entries[i].set_hexpand (true);
+ entries[i].set ("placeholder-text", AddressEditor.postal_element_names[i]);
+
+ if (postal_part != null)
+ entries[i].set_text (postal_part);
+
+ entries[i].get_style_context ().add_class ("contacts-postal-entry");
+ add (entries[i]);
+
+ var entry = entries[i];
+ var prop_name = AddressEditor.postal_element_props[i];
+ entries[i].changed.connect (() => {
+ details.value.set (prop_name, entry.get_text ());
+ changed ();
+ });
+ }
+ }
+
+ public bool is_empty () {
+ foreach (var entry in entries) {
+ if (entry.get_text () != "") {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public override void grab_focus () {
+ entries[0].grab_focus ();
+ }
+}
+
+public class Contacts.EditorPropertyRow : ListBoxRow {
+ public bool is_empty { get; set; default = true; }
+ public bool is_removed { get; set; default = false; }
+ public string ptype { get; private set; }
+ public Box container;
+ public Box header;
+ public Revealer revealer;
+
+ construct {
+ this.revealer = new Revealer ();
+ //TODO: bind orientation property to available space
+ var box = new Box (Orientation.VERTICAL, 6);
+ box.set_valign (Align.START);
+ box.set_can_focus (false);
+ this.container = new Box (Orientation.HORIZONTAL, 6);
+ this.container.set_can_focus (false);
+ this.header = new Box (Orientation.HORIZONTAL, 6);
+ this.header.set_can_focus (false);
+ box.pack_start (this.header);
+ box.pack_end (this.container);
+ this.set_activatable (false);
+ this.set_selectable (false);
+ this.set_can_focus (false);
+ box.margin = 12;
+ this.revealer.add (box);
+ add (this.revealer);
+ this.get_style_context ().add_class ("editor-property-row");
+ this.revealer.bind_property ("reveal-child", this, "is-removed", BindingFlags.INVERT_BOOLEAN);
+ }
+
+ public EditorPropertyRow (string type) {
+ this.ptype = type;
+ }
+
+ // This hides the widget with an animation and then destroys it
+ public new void remove () {
+ this.revealer.set_reveal_child (false);
+ // Remove the seperator during the animation to make it look a little better
+ Timeout.add (this.revealer.get_transition_duration ()/2, () => {
+ this.set_header (null);
+ return false;
+ });
+
+ this.revealer.notify["child-revealed"].connect ( () => {
+ this.destroy ();
+ });
+ }
+
+ public void show_with_animation (bool animate = true) {
+ if (!animate) {
+ var duration = this.revealer.get_transition_duration ();
+ this.revealer.set_reveal_child (true);
+ this.revealer.set_transition_duration (duration);
+ this.show_all ();
+ } else {
+ this.show_all ();
+ this.revealer.set_reveal_child (true);
+ }
+ }
+
+ public void add_base_label (string label) {
+ var title_label = new Label (label);
+ title_label.set_hexpand (false);
+ title_label.set_halign (Align.START);
+ title_label.margin_end = 6;
+ this.header.pack_start (title_label);
+ }
+
+ public void add_base_combo (Set<AbstractFieldDetails> set, string label, TypeSet combo_type,
AbstractFieldDetails details) {
+ var title_label = new Label (label);
+ title_label.set_halign (Align.START);
+ this.header.pack_start (title_label);
+ TypeCombo combo = new TypeCombo (combo_type);
+ combo.set_hexpand (false);
+ combo.set_active_from_field_details (details);
+ this.header.pack_start (combo);
+
+ combo.changed.connect (() => {
+ combo.active_descriptor.save_to_field_details(details);
+ // Workaround: we shouldn't do a manual signal
+ (set as FakeHashSet).changed ();
+ debug ("Property phone changed");
+ });
+ }
+
+ //FIXME: create only one add_base_entry
+ public void add_base_entry_email (Set<AbstractFieldDetails> set,
+ EmailFieldDetails details,
+ string placeholder) {
+ var value_entry = new Entry ();
+ value_entry.set_input_purpose (InputPurpose.EMAIL);
+ value_entry.placeholder_text = placeholder;
+ value_entry.set_text (details.value);
+ value_entry.set_hexpand (true);
+ this.container.pack_start (value_entry);
+
+ this.is_empty = details.value == "";
+
+ value_entry.changed.connect (() => {
+ details.value = value_entry.get_text ();
+ // Workaround: we shouldn't do a manual signal
+ (set as FakeHashSet).changed ();
+ debug ("Property email changed");
+ this.is_empty = value_entry.get_text () == "";
+ });
+ }
+
+ public void add_base_entry_phone (Set<AbstractFieldDetails> set,
+ PhoneFieldDetails details,
+ string placeholder) {
+ var value_entry = new Entry ();
+ value_entry.set_input_purpose (InputPurpose.PHONE);
+ value_entry.placeholder_text = placeholder;
+ value_entry.set_text (details.value);
+ value_entry.set_hexpand (true);
+ this.container.pack_start (value_entry);
+
+ this.is_empty = details.value == "";
+
+ value_entry.changed.connect (() => {
+ details.value = value_entry.get_text ();
+ // Workaround: we shouldn't do a manual signal
+ (set as FakeHashSet).changed ();
+ debug ("Property type changed");
+
+ this.is_empty = value_entry.get_text () == "";
+ });
+ }
+
+ public void add_base_entry_url (Set<AbstractFieldDetails> set,
+ UrlFieldDetails details,
+ string placeholder) {
+ var value_entry = new Entry ();
+ value_entry.placeholder_text = placeholder;
+ value_entry.set_input_purpose (InputPurpose.URL);
+ value_entry.set_text (details.value);
+ value_entry.set_hexpand (true);
+ this.container.pack_start (value_entry);
+
+ this.is_empty = details.value == "";
+
+ value_entry.changed.connect (() => {
+ details.value = value_entry.get_text ();
+ // Workaround: we shouldn't do a manual signal
+ (set as FakeHashSet).changed ();
+ debug ("Property type changed");
+
+ this.is_empty = value_entry.get_text () == "";
+ });
+ }
+
+ public void add_base_delete (Set<AbstractFieldDetails> set, AbstractFieldDetails details) {
+ var delete_button = new Button.from_icon_name ("user-trash-symbolic");
+ delete_button.get_accessible ().set_name (_("Delete field"));
+ delete_button.set_valign (Align.START);
+ this.bind_property ("is-empty", delete_button, "sensitive", BindingFlags.SYNC_CREATE |
BindingFlags.INVERT_BOOLEAN);
+ this.container.pack_end (delete_button, false);
+
+
+ delete_button.clicked.connect (() => {
+ debug ("Property removed");
+ this.remove ();
+ set.remove (details);
+ });
+ }
+}
+
+/**
+ * A widget representing a property of a persona in the editor {@link Contact}.
+ * We can have more then one property in one properity e.g. Emails therefore we need to return a List
+ */
+public class Contacts.EditorProperty : ArrayList<EditorPropertyRow> {
+ public bool writeable { get; private set; default = false; }
+
+ public EditorProperty (Persona persona, string property_name, bool only_new = false) {
+ foreach (var s in persona.writeable_properties) {
+ if (s == property_name) {
+ this.writeable = true;
+ break;
+ }
+ }
+
+ create_for_property (persona, property_name, only_new);
+ }
+
+ void create_for_property (Persona p, string prop_name, bool only_new) {
+ switch (prop_name) {
+ case "email-addresses":
+ var details = p as EmailDetails;
+ if (details != null) {
+ var emails = Utils.sort_fields<EmailFieldDetails>(details.email_addresses);
+ if (!only_new)
+ foreach (var email in emails) {
+ add (create_for_email (details.email_addresses, email));
+ }
+ if (this.writeable)
+ add (create_for_email (details.email_addresses));
+ }
+ break;
+ case "phone-numbers":
+ var details = p as PhoneDetails;
+ if (details != null) {
+ var phones = Utils.sort_fields<PhoneFieldDetails>(details.phone_numbers);
+ if (!only_new)
+ foreach (var phone in phones) {
+ add (create_for_phone (details.phone_numbers, phone));
+ }
+ if (this.writeable)
+ add (create_for_phone (details.phone_numbers));
+ }
+ break;
+ case "urls":
+ var details = p as UrlDetails;
+ if (details != null) {
+ var urls = Utils.sort_fields<UrlFieldDetails>(details.urls);
+ if (!only_new)
+ foreach (var url in urls) {
+ add (create_for_url (details.urls, url));
+ }
+ add (create_for_url (details.urls));
+ }
+ break;
+ case "nickname":
+ var name_details = p as NameDetails;
+ if (name_details != null && name_details.nickname != null && !only_new) {
+ add (create_for_nick (name_details));
+ }
+ break;
+ case "birthday":
+ var birthday_details = p as BirthdayDetails;
+ if (birthday_details != null && !only_new) {
+ add (create_for_birthday (birthday_details));
+ }
+ break;
+ case "notes":
+ var note_details = p as NoteDetails;
+ if (note_details != null) {
+ if (!only_new)
+ foreach (var note in note_details.notes) {
+ add (create_for_note (note_details.notes, note));
+ }
+ if (this.writeable)
+ add (create_for_note (note_details.notes));
+ }
+ break;
+ case "postal-addresses":
+ var address_details = p as PostalAddressDetails;
+ if (address_details != null) {
+ if (!only_new)
+ foreach (var addr in address_details.postal_addresses) {
+ add (create_for_address (address_details.postal_addresses, addr));
+ }
+ if (this.writeable)
+ add (create_for_address (address_details.postal_addresses));
+ }
+ break;
+ }
+ }
+
+ private EditorPropertyRow create_for_email (Set<AbstractFieldDetails> set, EmailFieldDetails? details =
null) {
+ if (details == null) {
+ var parameters = new HashMultiMap<string, string> ();
+ parameters["type"] = "PERSONAL";
+ var new_details = new EmailFieldDetails ("", parameters);
+ set.add(new_details);
+ details = new_details;
+ }
+ var box = new EditorPropertyRow ("email-addresses");
+ box.add_base_combo (set, _("Email address"), TypeSet.email, details);
+ box.add_base_entry_email (set, details, _("Add email"));
+ box.add_base_delete (set, details);
+
+ box.sensitive = this.writeable;
+ return box;
+ }
+
+ private EditorPropertyRow create_for_phone (Set<AbstractFieldDetails> set, PhoneFieldDetails? details =
null) {
+ if (details == null) {
+ var parameters = new HashMultiMap<string, string> ();
+ parameters["type"] = "CELL";
+ var new_details = new PhoneFieldDetails ("", parameters);
+ set.add(new_details);
+ details = new_details;
+ }
+
+ var box = new EditorPropertyRow ("phone-numbers");
+ box.add_base_combo (set, _("Phone number"), TypeSet.phone, details);
+ box.add_base_entry_phone (set, details, _("Add number"));
+ box.add_base_delete (set, details);
+
+ box.sensitive = this.writeable;
+ return box;
+ }
+
+ // TODO: add support for different types of urls
+ private EditorPropertyRow create_for_url (Set<AbstractFieldDetails> set, UrlFieldDetails? details = null) {
+ if (details == null) {
+ var parameters = new HashMultiMap<string, string> ();
+ parameters["type"] = "PERSONAL";
+ var new_details = new UrlFieldDetails ("", parameters);
+ set.add(new_details);
+ details = new_details;
+ }
+
+ var box = new EditorPropertyRow ("urls");
+ box.add_base_label (_("Website"));
+ box.add_base_entry_url (set, details, _("https://exmaple.com"));
+ box.add_base_delete (set, details);
+
+ box.sensitive = this.writeable;
+ return box;
+ }
+
+ private EditorPropertyRow create_for_nick (NameDetails details) {
+ var box = new EditorPropertyRow ("nickname");
+ box.add_base_label (_("Nickname"));
+
+ var value_entry = new Entry ();
+ value_entry.set_text (details.nickname);
+ value_entry.set_hexpand (true);
+ box.container.pack_start (value_entry);
+
+ value_entry.changed.connect (() => {
+ details.nickname = value_entry.get_text ();
+ debug ("Nickname changed");
+ box.is_empty = value_entry.get_text () == "";
+ });
+
+ box.sensitive = this.writeable;
+ return box;
+ }
+
+ // TODO: support different types of nodes
+ private EditorPropertyRow create_for_note (Set<NoteFieldDetails> set, NoteFieldDetails? details = null) {
+ if (details == null) {
+ var parameters = new HashMultiMap<string, string> ();
+ parameters["type"] = "PERSONAL";
+ var new_details = new NoteFieldDetails ("", parameters);
+ set.add(new_details);
+ details = new_details;
+ }
+ var box = new EditorPropertyRow ("notes");
+ box.add_base_label (_("Note"));
+
+ var sw = new ScrolledWindow (null, null);
+ sw.set_shadow_type (ShadowType.OUT);
+ sw.set_size_request (-1, 100);
+ var value_text = new TextView ();
+ value_text.get_buffer ().set_text (details.value);
+ value_text.set_hexpand (true);
+ sw.add (value_text);
+ box.container.pack_start (sw);
+
+ box.add_base_delete (set, details);
+
+ value_text.get_buffer ().changed.connect (() => {
+ TextIter start, end;
+ value_text.get_buffer ().get_start_iter (out start);
+ value_text.get_buffer ().get_end_iter (out end);
+ details.value = value_text.get_buffer ().get_text (start, end, true);
+ // Workaround: we shouldn't do a manual signal
+ (set as FakeHashSet).changed ();
+ debug ("Property changed");
+ box.is_empty = details.value == "";
+ });
+
+ box.sensitive = this.writeable;
+ return box;
+ }
+
+ private EditorPropertyRow create_for_birthday (BirthdayDetails? details) {
+ DateTime date;
+ if (details.birthday == null) {
+ date = new DateTime.now ();
+ } else {
+ date = details.birthday;
+ }
+
+ var box = new EditorPropertyRow ("birthday");
+ box.add_base_label (_("Birthday"));
+
+ var button = new Button.with_label (_("Set Birthday"));
+ box.container.pack_start (button);
+
+ button.clicked.connect (() => {
+ Window parent_window = button.get_toplevel () as Window;
+ if (parent_window != null) {
+ var dialog = new BirthdayEditor (parent_window, date);
+
+ dialog.changed.connect (() => {
+ if (dialog.is_set) {
+ details.birthday = dialog.get_birthday ();
+ button.set_label (details.birthday.to_local ().format ("%x"));
+ box.is_empty = false;
+ }
+ });
+ dialog.show_all ();
+ }
+ });
+
+ box.is_empty = details.birthday == null;
+
+ var delete_button = new Button.from_icon_name ("user-trash-symbolic");
+ delete_button.get_accessible ().set_name (_("Delete field"));
+ delete_button.set_valign (Align.START);
+ box.bind_property ("is-empty", delete_button, "sensitive", BindingFlags.SYNC_CREATE |
BindingFlags.INVERT_BOOLEAN);
+ box.container.pack_end (delete_button, false);
+
+ delete_button.clicked.connect (() => {
+ debug ("Birthday removed");
+ details.birthday = null;
+ box.is_empty = true;
+ button.set_label (_("Set Birthday"));
+ });
+
+ box.sensitive = this.writeable;
+ return box;
+ }
+
+ private EditorPropertyRow create_for_address (Set<PostalAddressFieldDetails> set,
+ PostalAddressFieldDetails? details = null) {
+ if (details == null) {
+ var parameters = new HashMultiMap<string, string> ();
+ parameters["type"] = "HOME";
+ var address = new PostalAddress(null, null, null, null, null, null, null, null, null);
+ var new_details = new PostalAddressFieldDetails (address, parameters);
+ set.add(new_details);
+ details = new_details;
+ }
+ var box = new EditorPropertyRow ("postal-addresses");
+ box.add_base_combo (set, _("Address"), TypeSet.general, details);
+
+ var value_address = new AddressEditor (details);
+ box.container.pack_start (value_address);
+
+ box.add_base_delete (set, details);
+
+ value_address.changed.connect (() => {
+ // Workaround: we shouldn't do a manual signal
+ (set as FakeHashSet).changed ();
+ debug ("Address changed");
+ box.is_empty = value_address.is_empty ();
+ });
+
+ box.sensitive = this.writeable;
+ return box;
+ }
+}
diff --git a/src/contacts-fake-persona-store.vala b/src/contacts-fake-persona-store.vala
index 471a16c..5b8b50e 100644
--- a/src/contacts-fake-persona-store.vala
+++ b/src/contacts-fake-persona-store.vala
@@ -41,13 +41,13 @@ public class Contacts.FakePersonaStore : PersonaStore {
}
public override Map<string, Persona> personas {
- get { return this._personas_ro; }
+ get { return this._personas_ro; }
}
public override MaybeBool can_add_personas { get { return MaybeBool.FALSE; } }
public override MaybeBool can_alias_personas { get { return MaybeBool.FALSE; } }
public override MaybeBool can_group_personas { get { return MaybeBool.FALSE; } }
- public override MaybeBool can_remove_personas { get { return MaybeBool.FALSE; } }
+ public override MaybeBool can_remove_personas { get { return MaybeBool.TRUE; } }
public override bool is_prepared { get { return true; } }
public override bool is_quiescent { get { return true; } }
private string[] _always_writeable_properties = {};
@@ -68,193 +68,521 @@ public class Contacts.FakePersonaStore : PersonaStore {
* The FakePersona is used as a placeholder till we get the real persona from folks
* It needs to implement all Details we support so that we don't loise any information
*/
+const string BACKEND_NAME = "fake-store";
+
public class Contacts.FakePersona : Persona,
- AvatarDetails,
- BirthdayDetails,
- EmailDetails,
- ImDetails,
- NameDetails,
- NoteDetails,
- PhoneDetails,
- UrlDetails,
- PostalAddressDetails
+AvatarDetails,
+BirthdayDetails,
+EmailDetails,
+ImDetails,
+NameDetails,
+NoteDetails,
+PhoneDetails,
+UrlDetails,
+PostalAddressDetails
{
private HashTable<string, Value?> properties;
// Keep track of the persona in the actual store
- private weak Persona real_persona { get; set; default = null; }
+ public weak Persona real_persona { get; set; default = null; }
+ private string[] _writeable_properties = {};
private const string[] _linkable_properties = {};
- private const string[] _writeable_properties = {};
public override string[] linkable_properties {
get { return _linkable_properties; }
}
public override string[] writeable_properties {
- get { return _writeable_properties; }
+ get {
+ return this._writeable_properties;
+ }
}
- [CCode (notify = false)]
- public LoadableIcon? avatar
- {
- get { unowned Value? value = this.properties.get ("avatar");
- if (value == null)
- return null;
+ private ArrayList<string> _changed_properties;
+
+ construct {
+ this._changed_properties = new ArrayList<string> ();
+ }
+
+ public LoadableIcon? avatar {
+ get {
+ unowned Value? value = this.properties.get ("avatar");
return (LoadableIcon?) value;
}
- set {}
+ set {
+ this.properties.set ("avatar", value);
+ }
}
- [CCode (notify = false)]
- public string full_name
- {
- get { unowned Value? value = this.properties.get ("full-name");
+ public async void change_avatar (LoadableIcon? avatar) throws PropertyError {
+ this.avatar = avatar;
+ }
+
+ public string full_name {
+ get {
+ unowned Value? value = this.properties.get ("full-name");
if (value == null)
return "";
- return value.get_string (); }
- set {}
+ return value.get_string ();
+ }
+ set {
+ this.properties.set ("full-name", value);
+ }
}
- [CCode (notify = false)]
- public string nickname
- {
- get { unowned Value? value = this.properties.get ("nickname");
+ public string nickname {
+ get {
+ unowned Value? value = this.properties.get ("nickname");
if (value == null)
return "";
- return value.get_string (); }
- set {}
+ return value.get_string ();
+ }
+ set {
+ this.properties.set ("nickname", value);
+ }
}
- [CCode (notify = false)]
- public StructuredName? structured_name
- {
+ //TODO: implement structured_name
+ public StructuredName? structured_name {
get { return null; }
set {}
}
- [CCode (notify = false)]
- public Set<PhoneFieldDetails> phone_numbers
- {
- get { unowned Value? value = this.properties.get ("phone-numbers");
+ public Set<PhoneFieldDetails> phone_numbers {
+ get {
+ unowned Value? value = this.properties.get ("phone-numbers");
if (value == null) {
var new_value = Value (typeof (Set));
- new_value.set_object (new HashSet<PhoneFieldDetails> ());
+ var set = new FakeHashSet<PhoneFieldDetails> ();
+ new_value.set_object (set);
+ set.changed.connect (() => { notify_property ("phone-numbers"); });
this.properties.set ("phone-numbers", new_value);
value = this.properties.get ("phone-numbers");
}
return (Set<PhoneFieldDetails>) value;
}
-
- set {}
+ set {
+ this.properties.set ("phone-numbers", value);
+ }
}
- [CCode (notify = false)]
- public Set<UrlFieldDetails> urls
- {
- get { unowned Value? value = this.properties.get ("urls");
+ public Set<UrlFieldDetails> urls {
+ get {
+ unowned Value? value = this.properties.get ("urls");
if (value == null) {
var new_value = Value (typeof (Set));
- new_value.set_object (new HashSet<UrlFieldDetails> ());
+ var set = new FakeHashSet<UrlFieldDetails> ();
+ new_value.set_object (set);
+ set.changed.connect (() => { notify_property ("urls"); });
this.properties.set ("urls", new_value);
- value = this.properties.get ("urls");
+ value = new_value;
}
return (Set<UrlFieldDetails>) value;
}
-
- set {}
+ set {
+ this.properties.set ("urls", value);
+ }
}
- [CCode (notify = false)]
- public Set<PostalAddressFieldDetails> postal_addresses
- {
- get { unowned Value? value = this.properties.get ("urls");
+ public Set<PostalAddressFieldDetails> postal_addresses {
+ get {
+ unowned Value? value = this.properties.get ("postal-addresses");
if (value == null) {
var new_value = Value (typeof (Set));
- new_value.set_object (new HashSet<PostalAddressFieldDetails> ());
- this.properties.set ("urls", new_value);
+ var set = new FakeHashSet<PostalAddressFieldDetails> ();
+ new_value.set_object (set);
+ set.changed.connect (() => { notify_property ("postal-addresses"); });
+ this.properties.set ("postal-addresses", new_value);
value = new_value;
}
-
return (Set<PostalAddressFieldDetails>) value;
}
-
- set {}
+ set {
+ this.properties.set ("postal-addresses", value);
+ }
}
- [CCode (notify = false)]
- public Set<NoteFieldDetails> notes
- {
- get { unowned Value? value = this.properties.get ("notes");
+ public Set<NoteFieldDetails> notes {
+ get {
+ unowned Value? value = this.properties.get ("notes");
if (value == null) {
var new_value = Value (typeof (Set));
- new_value.set_object (new HashSet<NoteFieldDetails> ());
+ var set = new FakeHashSet<NoteFieldDetails> ();
+ new_value.set_object (set);
+ set.changed.connect (() => { notify_property ("notes"); });
this.properties.set ("notes", new_value);
value = new_value;
}
return (Set<NoteFieldDetails>) value;
}
-
- set {}
+ set {
+ this.properties.set ("notes", value);
+ }
}
- [CCode (notify = false)]
- public DateTime? birthday
- {
+ public DateTime? birthday {
get { unowned Value? value = this.properties.get ("birthday");
if (value == null)
return null;
return (DateTime) value;
}
- set {}
+ set {
+ this.properties.set ("birthday", value);
+ }
}
- [CCode (notify = false)]
- public string? calendar_event_id
- {
+ //TODO implement calender_event_id
+ public string? calendar_event_id {
get { return null; }
set {}
}
- [CCode (notify = false)]
- public MultiMap<string,ImFieldDetails> im_addresses
- {
- get { unowned Value? value = this.properties.get ("im-addresses");
+ public MultiMap<string,ImFieldDetails> im_addresses {
+ get {
+ unowned Value? value = this.properties.get ("im-addresses");
if (value == null) {
var new_value = Value (typeof (MultiMap));
- new_value.set_object (new HashMultiMap<string, ImFieldDetails> ());
+ var set = new FakeHashMultiMap<string, ImFieldDetails> ();
+ new_value.set_object (set);
this.properties.set ("im-addresses", new_value);
+ set.changed.connect (() => { notify_property ("im-addresses"); });
value = new_value;
}
-
return (MultiMap<string, ImFieldDetails>) value;
}
-
- set {}
+ set {
+ this.properties.set ("im-addresses", value);
+ }
}
- [CCode (notify = false)]
- public Set<EmailFieldDetails> email_addresses
- {
- get { unowned Value? value = this.properties.get ("email-addresses");
+ public Set<EmailFieldDetails> email_addresses {
+ get {
+ unowned Value? value = this.properties.get ("email-addresses");
if (value == null) {
var new_value = Value (typeof (Set));
- new_value.set_object (new HashSet<EmailFieldDetails> ());
+ var set = new FakeHashSet<EmailFieldDetails> ();
+ set.changed.connect (() => { notify_property ("email-addresses"); });
+ new_value.set_object (set);
this.properties.set ("email-addresses", new_value);
value = new_value;
}
-
return (Set<EmailFieldDetails>) value;
}
- set {}
+ set {
+ this.properties.set ("email-addresses", value);
+ }
}
- public FakePersona (PersonaStore store, HashTable<string, Value?> details) {
- //TODO: use correct data to fill the object
- Object (display_id: "display-id-fake-persona",
- uid: "uid-fake-persona",
- iid: "iid",
+ public FakePersona (PersonaStore store, string[] writeable_properties, HashTable<string, Value?> details) {
+ var id = Uuid.string_random();
+ var uid = Folks.Persona.build_uid (BACKEND_NAME, store.id, id);
+ var iid = "%s:%s".printf (store.id, id);
+ Object (display_id: iid,
+ uid: uid,
+ iid: iid,
store: store,
is_user: false);
this.properties = details;
+ this._writeable_properties = writeable_properties;
+ }
+
+ public FakePersona.from_real (Persona persona) {
+ var details = new HashTable<string, Value?> (str_hash, str_equal);
+ this (FakePersonaStore.the_store (), persona.writeable_properties, details);
+ // FIXME: get all properties not only writable properties
+ var props = persona.writeable_properties;
+ foreach (var prop in props) {
+ get_property_from_real (persona, prop);
+ }
+
+ this.real_persona = persona;
+ // FIXME: we are adding property changes also for things we don't care about e.g. individual
+ this.notify.connect((obj, ps) => {
+ add_to_changed_properties(ps.name);
+ });
+ }
+
+ private void get_property_from_real (Persona persona, string property_name) {
+ // TODO Implement the interface for the commented properties
+ switch (property_name) {
+ case "alias":
+ //alias = (persona as AliasDetails).alias;
+ break;
+ case "avatar":
+ avatar = (persona as AvatarDetails).avatar;
+ break;
+ case "birthday":
+ birthday = (persona as BirthdayDetails).birthday;
+ break;
+ case "calendar-event-id":
+ calendar_event_id = (persona as BirthdayDetails).calendar_event_id;
+ break;
+ case "email-addresses":
+ foreach (var e in (persona as EmailDetails).email_addresses) {
+ email_addresses.add (new EmailFieldDetails (e.value, e.parameters));
+ }
+ break;
+ case "is-favourite":
+ //is_favourite = (persona as FavouriteDetails).is_favourite;
+ break;
+ case "gender":
+ //gender = (persona as GenderDetails).gender;
+ break;
+ case "groups":
+ //groups = (persona as GroupDetails).groups;
+ break;
+ case "im-addresses":
+ im_addresses = (persona as ImDetails).im_addresses;
+ break;
+ case "local-ids":
+ //local_ids = (persona as LocalIdDetails).local_ids;
+ break;
+ case "structured-name":
+ structured_name = (persona as NameDetails).structured_name;
+ break;
+ case "full-name":
+ full_name = (persona as NameDetails).full_name;
+ break;
+ case "nickname":
+ nickname = (persona as NameDetails).nickname;
+ break;
+ case "notes":
+ foreach (var e in (persona as NoteDetails).notes) {
+ notes.add (new NoteFieldDetails (e.value, e.parameters, e.id));
+ }
+ break;
+ case "phone-numbers":
+ foreach (var e in (persona as PhoneDetails).phone_numbers) {
+ phone_numbers.add (new PhoneFieldDetails (e.value, e.parameters));
+ }
+ break;
+ case "postal-addresses":
+ foreach (var e in (persona as PostalAddressDetails).postal_addresses) {
+ postal_addresses.add (new PostalAddressFieldDetails (e.value, e.parameters));
+ }
+ break;
+ case "roles":
+ //roles (persona as RoleDetails).roles;
+ break;
+ case "urls":
+ foreach (var e in (persona as UrlDetails).urls) {
+ urls.add (new UrlFieldDetails (e.value, e.parameters));
+ }
+ break;
+ case "web-service-addresses":
+ //web_service_addresses.add_all((persona as WebServiceDetails).web_service_addresses);
+ break;
+ default:
+ debug ("Unknown property '%s' in FakePersona.get_property_from_real().", property_name);
+ break;
+ }
+ }
+
+ private void add_to_changed_properties (string property_name) {
+ debug ("Property: %s was added to the changed property list", property_name);
+ if (!this._changed_properties.contains(property_name))
+ this._changed_properties.add (property_name);
+ }
+
+ public HashTable<string, Value?> get_details () {
+ return this.properties;
+ }
+
+ public async void apply_changes_to_real () {
+ if (this.real_persona == null) {
+ warning ("No real persona to apply changes from fake persona");
+ return;
+ }
+ foreach (var prop in _changed_properties) {
+ if (properties.contains (prop)) {
+ try {
+ yield set_persona_property (this.real_persona, prop, properties.get (prop));
+ } catch (Error e) {
+ error ("Couldn't write property: %s", e.message);
+ }
+ }
+ }
+ }
+
+ private static async void set_persona_property (Persona persona,
+ string property_name, Value new_value) throws PropertyError,
IndividualAggregatorError, PropertyError {
+ switch (property_name) {
+ case "alias":
+ yield (persona as AliasDetails).change_alias ((string) new_value);
+ break;
+ case "avatar":
+ yield (persona as AvatarDetails).change_avatar ((LoadableIcon?) new_value);
+ break;
+ case "birthday":
+ yield (persona as BirthdayDetails).change_birthday ((DateTime?) new_value);
+ break;
+ case "calendar-event-id":
+ yield (persona as BirthdayDetails).change_calendar_event_id ((string?) new_value);
+ break;
+ case "email-addresses":
+ var original = (Set<EmailFieldDetails>) new_value;
+ var copy = new HashSet<EmailFieldDetails> ();
+ foreach (var e in original) {
+ if (e.value != null && e.value != "")
+ copy.add (new EmailFieldDetails (e.value, e.parameters));
+ }
+ yield (persona as EmailDetails).change_email_addresses (copy);
+ break;
+ case "is-favourite":
+ yield (persona as FavouriteDetails).change_is_favourite ((bool) new_value);
+ break;
+ case "gender":
+ yield (persona as GenderDetails).change_gender ((Gender) new_value);
+ break;
+ case "groups":
+ yield (persona as GroupDetails).change_groups ((Set<string>) new_value);
+ break;
+ case "im-addresses":
+ yield (persona as ImDetails).change_im_addresses ((MultiMap<string, ImFieldDetails>) new_value);
+ break;
+ case "local-ids":
+ yield (persona as LocalIdDetails).change_local_ids ((Set<string>) new_value);
+ break;
+ case "structured-name":
+ yield (persona as NameDetails).change_structured_name ((StructuredName?) new_value);
+ break;
+ case "full-name":
+ yield (persona as NameDetails).change_full_name ((string) new_value);
+ break;
+ case "nickname":
+ yield (persona as NameDetails).change_nickname ((string) new_value);
+ break;
+ case "notes":
+ var original = (Set<NoteFieldDetails>) new_value;
+ var copy = new HashSet<NoteFieldDetails> ();
+ foreach (var e in original) {
+ if (e.value != null && e.value != "")
+ copy.add (new NoteFieldDetails (e.value, e.parameters));
+ }
+ yield (persona as NoteDetails).change_notes (copy);
+ break;
+ case "phone-numbers":
+ var original = (Set<PhoneFieldDetails>) new_value;
+ var copy = new HashSet<PhoneFieldDetails> ();
+ foreach (var e in original) {
+ if (e.value != null && e.value != "")
+ copy.add (new PhoneFieldDetails (e.value, e.parameters));
+ }
+ yield (persona as PhoneDetails).change_phone_numbers (copy);
+ break;
+ case "postal-addresses":
+ var original = (Set<PostalAddressFieldDetails>) new_value;
+ var copy = new HashSet<PostalAddressFieldDetails> ();
+ foreach (var e in original) {
+ // TODO: make sure that the Postal Address isn't empty
+ if (e.value != null)
+ copy.add (new PostalAddressFieldDetails (e.value, e.parameters));
+ }
+ yield (persona as PostalAddressDetails).change_postal_addresses (copy);
+ break;
+ case "roles":
+ yield (persona as RoleDetails).change_roles ((Set<RoleFieldDetails>) new_value);
+ break;
+ case "urls":
+ var original = (Set<UrlFieldDetails>) new_value;
+ var copy = new HashSet<UrlFieldDetails> ();
+ foreach (var e in original) {
+ if (e.value != null && e.value != "")
+ copy.add (new UrlFieldDetails (e.value, e.parameters));
+ }
+ yield (persona as UrlDetails).change_urls (copy);
+ break;
+ case "web-service-addresses":
+ yield (persona as WebServiceDetails).change_web_service_addresses ((MultiMap<string,
WebServiceFieldDetails>) new_value);
+ break;
+ default:
+ critical ("Unknown property '%s' in Contact.set_persona_property().", property_name);
+ break;
+ }
+ }
+}
+
+/**
+ * A FakeIndividual
+ */
+public class Contacts.FakeIndividual : Individual {
+ public weak Individual real_individual { get; set; default = null; }
+ public weak FakePersona primary_persona { get; set; default = null; }
+ public FakeIndividual (Set<FakePersona>? personas) {
+ base (personas);
+ foreach (var p in personas) {
+ // Keep track of the main persona
+ if (Contacts.Utils.persona_is_main (p) || personas.size == 1)
+ primary_persona = p;
+ }
+ }
+
+ public FakeIndividual.from_real (Individual individual) {
+ var fake_personas = new HashSet<FakePersona> ();
+ foreach (var p in individual.personas) {
+ var fake_p = new FakePersona.from_real (p);
+ // Keep track of the main persona
+ if (Contacts.Utils.persona_is_main (p) || individual.personas.size == 1)
+ primary_persona = fake_p;
+ fake_personas.add (fake_p);
+ }
+ this (fake_personas);
+ this.real_individual = individual;
+ }
+
+ public async void apply_changes_to_real () {
+ if (this.real_individual == null) {
+ warning ("No real individual to apply changes from fake individual");
+ return;
+ }
+
+ foreach (var p in this.personas) {
+ var fake_persona = p as FakePersona;
+ if (fake_persona != null) {
+ yield fake_persona.apply_changes_to_real ();
+ }
+ }
+ }
+}
+
+/**
+ * This is the same as Gee.HashSet but adds a changed/added/removed signals
+ */
+public class Contacts.FakeHashSet<T> : Gee.HashSet<T> {
+ public signal void changed ();
+ public signal void added ();
+ public signal void removed ();
+
+ public FakeHashSet () {
+ base ();
+ }
+
+ public override bool add (T element) {
+ var res = base.add (element);
+ if (res) {
+ added();
+ changed ();
+ }
+ return res;
+ }
+
+ public override bool remove (T element) {
+ var res = base.remove (element);
+ if (res) {
+ removed();
+ changed ();
+ }
+ return res;
+ }
+}
+
+/**
+ * This is the same as Gee.HashMultiMap but adds a changed signal
+ */
+public class Contacts.FakeHashMultiMap<K, T> : Gee.HashMultiMap<K, T> {
+ public signal void changed ();
+
+ public FakeHashMultiMap () {
+ base ();
}
}
diff --git a/src/contacts-linking.vala b/src/contacts-linking.vala
index d5ddd2f..ba641c5 100644
--- a/src/contacts-linking.vala
+++ b/src/contacts-linking.vala
@@ -33,6 +33,7 @@ namespace Contacts {
/* Link individuals */
public async void do (LinkedList<Individual> individuals) {
+ print ("LINK\n");
var personas_to_link = new HashSet<Persona> ();
foreach (var i in individuals) {
var saved_personas = new HashSet<Persona> ();
@@ -42,10 +43,11 @@ namespace Contacts {
}
this.personas_to_link.add (saved_personas);
}
-
- // We don't need to unlink the individuals because we are using every persona
- yield link_personas(this.store, this.store.aggregator, personas_to_link);
-
+ try {
+ yield this.store.aggregator.link_personas (personas_to_link);
+ } catch (Error e) {
+ error ("Coulnd't link contacts: %s", e.message);
+ }
finished = true;
}
@@ -55,54 +57,44 @@ namespace Contacts {
.first_match(() => {return true;}).individual;
yield store.aggregator.unlink_individual (individual);
foreach (var personas in personas_to_link) {
- yield link_personas (this.store, this.store.aggregator, personas);
+ try {
+ yield this.store.aggregator.link_personas (personas);
+ } catch (Error e) {
+ error ("Coulnd't link contacts: %s", e.message);
+ }
}
}
}
public class UnLinkOperation : Object {
private weak Store store;
+
+ private HashSet<Persona> personas;
+
public UnLinkOperation(Store store) {
this.store = store;
+ this.personas = new HashSet<Persona> ();
}
/* Remove a personas from individual */
- public async void do (Individual main, Set<Persona> personas_to_remove) {
- var personas_to_keep = new HashSet<Persona> ();
+ public async void do (Individual main) {
foreach (var persona in main.personas)
- if (!personas_to_remove.contains (persona))
- personas_to_keep.add (persona);
+ personas.add (persona);
try {
yield store.aggregator.unlink_individual (main);
} catch (Error e) {
- debug ("Couldn't link personas");
+ error ("Coulnd't link contacts: %s", e.message);
}
- yield link_personas(this.store, this.store.aggregator, personas_to_keep);
}
/* Undo the unlinking */
public async void undo () {
+ try {
+ yield this.store.aggregator.link_personas (personas);
+ } catch (Error e) {
+ error ("Coulnd't link contacts: %s", e.message);
+ }
}
}
-
- /* Workaround: link_personas creates a new persona in the primary-store,
- * For some reason we can't change the primary-store directly,
- * but we can change the gsettings property.
- * Before linking we set the primary-store to be "key-file"
- * that the linking persona isn't written to a real store
- */
- private async void link_personas (Store store, IndividualAggregator aggregator, Set<Persona> personas) {
- var settings = new GLib.Settings ("org.freedesktop.folks");
- var default_store = settings.get_string ("primary-store");
- settings.set_string ("primary-store", "key-file:relationships.ini");
- try {
- yield aggregator.link_personas (personas);
- } catch (Error e) {
- debug ("%s", e.message);
- }
-
- // Rest primary-store
- settings.set_string ("primary-store", default_store);
- }
}
diff --git a/src/contacts-store.vala b/src/contacts-store.vala
index 4f1568f..e232be2 100644
--- a/src/contacts-store.vala
+++ b/src/contacts-store.vala
@@ -101,8 +101,8 @@ public class Contacts.Store : GLib.Object {
}
public void add_no_suggest_link (Individual a, Individual b) {
- var persona1 = Contacts.Utils.get_personas_for_display(a).to_array ()[0];
- var persona2 = Contacts.Utils.get_personas_for_display(b).to_array ()[0];
+ var persona1 = a.personas.to_array ()[0];
+ var persona2 = b.personas.to_array ()[0];
dont_suggest_link.set (persona1.uid, persona2.uid);
write_dont_suggest_db ();
}
@@ -148,6 +148,8 @@ public class Contacts.Store : GLib.Object {
}
}
+ debug ("Individuals changed: %d old, %d new", to_add.size, to_remove.size);
+
// Add new individuals
foreach (var i in to_add) {
if (i.personas.size > 0)
@@ -173,7 +175,7 @@ public class Contacts.Store : GLib.Object {
callback();
});
yield;
- this.disconnect (signal_id);
+ disconnect (signal_id);
}
Individual? matched = null;
@@ -198,11 +200,11 @@ public class Contacts.Store : GLib.Object {
try {
yield account_manager.prepare_async (null);
- account_manager.account_enabled.connect (this.check_account_caps);
- account_manager.account_disabled.connect (this.check_account_caps);
+ account_manager.account_enabled.connect (check_account_caps);
+ account_manager.account_disabled.connect (check_account_caps);
foreach (var account in account_manager.dup_valid_accounts ())
- yield this.check_account_caps (account);
+ yield check_account_caps (account);
} catch (GLib.Error e) {
warning ("Unable to check accounts caps %s", e.message);
}
diff --git a/src/contacts-utils.vala b/src/contacts-utils.vala
index f25c3a6..1e95fea 100644
--- a/src/contacts-utils.vala
+++ b/src/contacts-utils.vala
@@ -164,6 +164,18 @@ namespace Contacts.Utils {
return stores;
}
+ public PersonaStore[] get_eds_address_books_from_backend (BackendStore backend_store) {
+ PersonaStore[] stores = {};
+ foreach (var backend in backend_store.enabled_backends.values) {
+ foreach (var persona_store in backend.persona_stores.values) {
+ if (persona_store.type_id == "eds") {
+ stores += persona_store;
+ }
+ }
+ }
+ return stores;
+ }
+
public PersonaStore? get_key_file_address_book (Store contacts_store) {
foreach (var backend in contacts_store.backend_store.enabled_backends.values) {
foreach (var persona_store in backend.persona_stores.values) {
@@ -186,6 +198,15 @@ namespace Contacts.Utils {
dialog.destroy();
}
+ public bool persona_is_main (Persona persona) {
+ var store = persona.store;
+ if (!store.is_primary_store)
+ return false;
+
+ // Mark google contacts not in "My Contacts" as non-main
+ return !persona_is_google_other (persona);
+ }
+
public bool has_main_persona (Individual individual) {
var result = false;
foreach (var p in individual.personas) {
diff --git a/src/contacts-window.vala b/src/contacts-window.vala
index 0ea8f1a..e409478 100644
--- a/src/contacts-window.vala
+++ b/src/contacts-window.vala
@@ -22,6 +22,14 @@ using Folks;
[GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-window.ui")]
public class Contacts.Window : Gtk.ApplicationWindow {
+
+ private const GLib.ActionEntry[] action_entries = {
+ { "edit-contact", edit_contact },
+ { "share-contact", share_contact },
+ { "unlink-contact", unlink_contact },
+ { "delete-contact", delete_contact }
+ };
+
[GtkChild]
private Leaflet header;
[GtkChild]
@@ -41,8 +49,6 @@ public class Contacts.Window : Gtk.ApplicationWindow {
[GtkChild]
private Overlay notification_overlay;
[GtkChild]
- private Button add_button;
- [GtkChild]
private Button select_cancel_button;
[GtkChild]
private MenuButton hamburger_menu_button;
@@ -51,10 +57,14 @@ public class Contacts.Window : Gtk.ApplicationWindow {
[GtkChild]
private ModelButton sort_on_surname_button;
[GtkChild]
+ private MenuButton contact_menu_button;
+ [GtkChild]
private ToggleButton favorite_button;
private bool ignore_favorite_button_toggled;
[GtkChild]
- private Button edit_button;
+ private Button unlink_button;
+ [GtkChild]
+ private Button add_button;
[GtkChild]
private Button cancel_button;
[GtkChild]
@@ -91,6 +101,10 @@ public class Contacts.Window : Gtk.ApplicationWindow {
store: contacts_store
);
+ SimpleActionGroup actions = new SimpleActionGroup ();
+ actions.add_action_entries (action_entries, this);
+ this.insert_action_group ("window", actions);
+
this.settings = settings;
this.sort_on_firstname_button.clicked.connect (() => {
this.settings.sort_on_surname = false;
@@ -166,10 +180,6 @@ public class Contacts.Window : Gtk.ApplicationWindow {
this.contact_pane = new ContactPane (this, this.store);
this.contact_pane.visible = true;
this.contact_pane.hexpand = true;
- this.contact_pane.will_delete.connect ( (individual) => {
- this.list_pane.hide_contact (individual);
- delete_contacts (new ArrayList<Individual>.wrap ({ individual }));
- });
this.contact_pane.contacts_linked.connect (contact_pane_contacts_linked_cb);
this.contact_pane.display_name_changed.connect ((display_name) => {
this.right_header.title = display_name;
@@ -213,9 +223,9 @@ public class Contacts.Window : Gtk.ApplicationWindow {
= (this.state == UiState.NORMAL || this.state == UiState.SHOWING);
// UI when showing a contact
- this.edit_button.visible
- = this.favorite_button.visible
- = (this.state == UiState.SHOWING);
+ this.contact_menu_button.visible
+ = this.favorite_button.visible
+ = (this.state == UiState.SHOWING);
// Selecting UI
this.select_cancel_button.visible = (this.state == UiState.SELECTING);
@@ -247,8 +257,11 @@ public class Contacts.Window : Gtk.ApplicationWindow {
show_list_pane ();
}
- [GtkCallback]
- private void on_edit_button_clicked () {
+ private void share_contact () {
+ debug ("Share isn't implemented, yet");
+ }
+
+ private void edit_contact () {
if (this.contact_pane.individual == null)
return;
@@ -256,7 +269,7 @@ public class Contacts.Window : Gtk.ApplicationWindow {
var name = this.contact_pane.individual.display_name;
this.right_header.title = _("Editing %s").printf (name);
- this.contact_pane.start_editing ();
+ this.contact_pane.edit_contact ();
}
[GtkCallback]
@@ -264,26 +277,57 @@ public class Contacts.Window : Gtk.ApplicationWindow {
// Don't change the contact being favorite while switching between the two of them
if (this.ignore_favorite_button_toggled)
return;
+ if (this.contact_pane.individual == null)
+ return;
var is_fav = this.contact_pane.individual.is_favourite;
this.contact_pane.individual.is_favourite = !is_fav;
}
- private void stop_editing (bool drop_changes = false) {
- if (this.state == UiState.CREATING) {
- show_list_pane ();
+ private void unlink_contact () {
+ var individual = this.contact_pane.individual;
+ if (individual == null)
+ return;
+
+ set_shown_contact (null);
+ this.state = UiState.NORMAL;
+
+ var operation = new UnLinkOperation (this.store);
+ operation.do.begin (individual);
- if (drop_changes) {
- this.contact_pane.stop_editing (drop_changes);
- } else {
- this.contact_pane.create_contact.begin ();
+ var b = new Button.with_mnemonic (_("_Undo"));
+ var notification = new InAppNotification (_("Contacts unlinked"), b);
+
+ /* signal handlers */
+ b.clicked.connect ( () => {
+ /* here, we will link the thing in question */
+ operation.undo.begin ();
+ notification.dismiss ();
+ });
+
+ add_notification (notification);
+ }
+
+ private void delete_contact () {
+ var individual = this.contact_pane.individual;
+ if (individual == null)
+ return;
+
+ this.list_pane.hide_contact (individual);
+ delete_contacts (new ArrayList<Individual>.wrap ({ individual }));
+ }
+
+ private void stop_editing (bool cancel = false) {
+ if (this.state == UiState.CREATING) {
+ if (cancel) {
+ show_list_pane ();
}
this.state = UiState.NORMAL;
} else {
show_contact_pane ();
- this.contact_pane.stop_editing (drop_changes);
this.state = UiState.SHOWING;
}
+ this.contact_pane.stop_editing (cancel);
if (this.contact_pane.individual != null) {
this.right_header.title = this.contact_pane.individual.display_name;
@@ -372,6 +416,13 @@ public class Contacts.Window : Gtk.ApplicationWindow {
this.select_cancel_button.clicked.connect (() => { this.state = UiState.NORMAL; });
this.done_button.clicked.connect (() => stop_editing ());
this.cancel_button.clicked.connect (() => stop_editing (true));
+
+ this.contact_pane.notify["individual"].connect (() => {
+ var individual = this.contact_pane.individual;
+ if (individual == null)
+ return;
+ this.unlink_button.set_visible (individual.personas.size > 1);
+ });
}
[GtkCallback]
diff --git a/src/meson.build b/src/meson.build
index 2592e74..c663c61 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -74,22 +74,23 @@ libcontacts_dep = declare_dependency(
# The gnome-contacts binary
contacts_vala_sources = files(
+ 'contacts-addressbook-list.vala',
'contacts-accounts-list.vala',
'contacts-app.vala',
'contacts-avatar.vala',
'contacts-avatar-selector.vala',
'contacts-contact-editor.vala',
- 'contacts-contact-form.vala',
'contacts-contact-list.vala',
'contacts-contact-pane.vala',
'contacts-contact-sheet.vala',
'contacts-crop-cheese-dialog.vala',
+ 'contacts-editor-persona.vala',
+ 'contacts-editor-property.vala',
'contacts-in-app-notification.vala',
'contacts-link-suggestion-grid.vala',
'contacts-linked-personas-dialog.vala',
'contacts-linking.vala',
'contacts-list-pane.vala',
- 'contacts-max-width-bin.vala',
'contacts-settings.vala',
'contacts-setup-window.vala',
'contacts-type-combo.vala',
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]