summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNiels De Graef <nielsdegraef@gmail.com>2017-09-20 02:11:59 +0200
committerNiels De Graef <nielsdegraef@gmail.com>2018-01-26 11:17:43 +0100
commit7f1dd656af97658ecc7565fc704a3080b9dc045f (patch)
treed6c06170ea9f60b86b79f26f13e5cb8dcd40874e
parent80c33aee1776aafd2378509938f8228251288738 (diff)
downloadgnome-contacts-wip/nielsdg/contact-editor-rewrite.tar.gz
TODO: * Check whether changes are always properly saved * Check if saved when no name was filled in * Add properties
-rw-r--r--data/ui/contacts-contact-editor.ui43
-rw-r--r--src/contacts-avatar-selector.vala4
-rw-r--r--src/contacts-contact-editor.vala1013
-rw-r--r--src/contacts-contact-pane.vala221
-rw-r--r--src/contacts-types.vala42
-rw-r--r--src/editor/contacts-editor-addresses-editor.vala114
-rw-r--r--src/editor/contacts-editor-avatar-editor.vala86
-rw-r--r--src/editor/contacts-editor-birthday-editor.vala145
-rw-r--r--src/editor/contacts-editor-composite-editor.vala66
-rw-r--r--src/editor/contacts-editor-contact-editor.vala259
-rw-r--r--src/editor/contacts-editor-details-editor-factory.vala101
-rw-r--r--src/editor/contacts-editor-details-editor.vala136
-rw-r--r--src/editor/contacts-editor-emails-editor.vala74
-rw-r--r--src/editor/contacts-editor-full-name-editor.vala50
-rw-r--r--src/editor/contacts-editor-nickname-editor.vala55
-rw-r--r--src/editor/contacts-editor-notes-editor.vala75
-rw-r--r--src/editor/contacts-editor-phones-editor.vala76
-rw-r--r--src/editor/contacts-editor-urls-editor.vala68
-rw-r--r--src/meson.build15
19 files changed, 1413 insertions, 1230 deletions
diff --git a/data/ui/contacts-contact-editor.ui b/data/ui/contacts-contact-editor.ui
index 57a862f..81886f7 100644
--- a/data/ui/contacts-contact-editor.ui
+++ b/data/ui/contacts-contact-editor.ui
@@ -4,52 +4,36 @@
<menu id="edit-contact-menu">
<item>
- <attribute name="action">edit.add.email-addresses.home</attribute>
- <attribute name="label" translatable="yes">Home email</attribute>
+ <attribute name="action">edit.add-email-addresses</attribute>
+ <attribute name="label" translatable="yes">Email address</attribute>
</item>
<item>
- <attribute name="action">edit.add.email-addresses.work</attribute>
- <attribute name="label" translatable="yes">Work email</attribute>
+ <attribute name="action">edit.add-phone-numbers</attribute>
+ <attribute name="label" translatable="yes">Phone number</attribute>
</item>
<item>
- <attribute name="action">edit.add.phone-numbers.cell</attribute>
- <attribute name="label" translatable="yes">Mobile phone</attribute>
- </item>
- <item>
- <attribute name="action">edit.add.phone-numbers.home</attribute>
- <attribute name="label" translatable="yes">Home phone</attribute>
- </item>
- <item>
- <attribute name="action">edit.add.phone-numbers.work</attribute>
- <attribute name="label" translatable="yes">Work phone</attribute>
- </item>
- <item>
- <attribute name="action">edit.add.urls</attribute>
+ <attribute name="action">edit.add-urls</attribute>
<attribute name="label" translatable="yes">Website</attribute>
</item>
<item>
- <attribute name="action">edit.add.nickname</attribute>
+ <attribute name="action">edit.add-nickname</attribute>
<attribute name="label" translatable="yes">Nickname</attribute>
</item>
<item>
- <attribute name="action">edit.add.birthday</attribute>
+ <attribute name="action">edit.add-birthday</attribute>
<attribute name="label" translatable="yes">Birthday</attribute>
</item>
<item>
- <attribute name="action">edit.add.postal-addresses.home</attribute>
- <attribute name="label" translatable="yes">Home address</attribute>
- </item>
- <item>
- <attribute name="action">edit.add.postal-addresses.work</attribute>
- <attribute name="label" translatable="yes">Work address</attribute>
+ <attribute name="action">edit.add-postal-addresses</attribute>
+ <attribute name="label" translatable="yes">Address</attribute>
</item>
<item>
- <attribute name="action">edit.add.notes</attribute>
+ <attribute name="action">edit.add-notes</attribute>
<attribute name="label" translatable="yes">Notes</attribute>
</item>
</menu>
- <template class="ContactsContactEditor" parent="GtkGrid">
+ <template class="ContactsEditorContactEditor" parent="GtkGrid">
<property name="visible">True</property>
<property name="orientation">vertical</property>
<child>
@@ -69,7 +53,6 @@
<property name="column_spacing">12</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
- <signal name="size-allocate" handler="on_container_grid_size_allocate" after="true" />
</object>
</child>
</object>
@@ -108,13 +91,13 @@
</child>
<child>
<object class="GtkButton" id="linked_button">
- <property name="visible">True</property>
+ <property name="visible">False</property>
<property name="label" translatable="yes">Linked Accounts</property>
</object>
</child>
<child>
<object class="GtkButton" id="remove_button">
- <property name="visible">True</property>
+ <property name="visible">False</property>
<property name="label" translatable="yes">Remove Contact</property>
<style>
<class name="destructive-action"/>
diff --git a/src/contacts-avatar-selector.vala b/src/contacts-avatar-selector.vala
index 83e3631..f53287c 100644
--- a/src/contacts-avatar-selector.vala
+++ b/src/contacts-avatar-selector.vala
@@ -72,9 +72,9 @@ public class Contacts.AvatarSelector : Dialog {
*/
public signal void set_avatar (GLib.Icon avatar_icon);
- public AvatarSelector (Window main_window, Contact? contact) {
+ public AvatarSelector (Gtk.Window? parent, Contact? contact) {
Object (
- transient_for: main_window,
+ transient_for: parent,
use_header_bar: 1
);
diff --git a/src/contacts-contact-editor.vala b/src/contacts-contact-editor.vala
deleted file mode 100644
index 3cde0c2..0000000
--- a/src/contacts-contact-editor.vala
+++ /dev/null
@@ -1,1013 +0,0 @@
-/* -*- Mode: vala; indent-tabs-mode: t; c-basic-offset: 2; tab-width: 8 -*- */
-/*
- * Copyright (C) 2011 Alexander Larsson <alexl@redhat.com>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 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.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 ();
- }
-}
-
-[GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-contact-editor.ui")]
-public class Contacts.ContactEditor : Grid {
-
- private const string[] DEFAULT_PROPS_NEW_CONTACT = {
- "email-addresses.personal",
- "phone-numbers.cell",
- "postal-addresses.home"
- };
-
- private Contact contact;
-
- [GtkChild]
- private Grid container_grid;
- private weak Widget focus_widget;
-
- private Entry name_entry;
-
- private Avatar avatar;
-
- [GtkChild]
- private ScrolledWindow main_sw;
-
- [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 int last_row;
- /* 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;
- }
-
- 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.update_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.update_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.update_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 (3, 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 (details);
- if (type != null)
- combo.set_to (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, 3, row, 1, 1);
-
- /* Notify change to upper layer */
- combo.changed.connect (() => {
- 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, 3, 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, 3, 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, 3, 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 (details);
- if (type != null)
- combo.set_to (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, 3, 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 = Contact.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 = Contact.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 (3, 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);
- }
-
- [GtkCallback]
- private void on_container_grid_size_allocate (Allocation alloc) {
- if (focus_widget != null &&
- focus_widget is Widget) {
- focus_widget.grab_focus ();
- focus_widget = null;
- }
- }
-
- public ContactEditor (SimpleActionGroup editor_actions) {
- this.container_grid.set_focus_vadjustment (this.main_sw.get_vadjustment ());
-
- this.main_sw.get_style_context ().add_class ("contacts-main-view");
- this.main_sw.get_style_context ().add_class ("view");
-
- this.add_detail_button.get_popover ().insert_action_group ("edit", editor_actions);
-
- this.writable_personas = new HashMap<string, HashMap<string, Field?>> ();
- }
-
- /**
- * Adjusts the ContactEditor to the given contact.
- * Use clear() to make sure nothing is lingering from the previous one.
- */
- public void edit (Contact c) {
- contact = c;
-
- remove_button.show ();
- remove_button.sensitive = contact.can_remove_personas ();
- linked_button.show ();
- linked_button.sensitive = contact.individual.personas.size > 1;
-
- create_avatar_button ();
- create_name_entry ();
-
- int i = 3;
- int last_store_position = 0;
- bool is_first_persona = true;
-
- var personas = c.get_personas_for_display ();
- foreach (var p in personas) {
- if (!is_first_persona) {
- var store_name = new Label("");
- store_name.set_markup (Markup.printf_escaped ("<span font='16px bold'>%s</span>",
- Contact.format_persona_store_name_for_contact (p)));
- store_name.set_halign (Align.START);
- store_name.xalign = 0.0f;
- store_name.margin_start = 6;
- container_grid.attach (store_name, 0, i, 2, 1);
- last_store_position = ++i;
- }
-
- var rw_props = Contact.sort_persona_properties (p.writeable_properties);
- if (rw_props.length != 0) {
- writable_personas.set (p.uid, new HashMap<string, Field?> ());
- foreach (var prop in rw_props) {
- add_edit_row (p, prop, ref i);
- }
- }
-
- if (is_first_persona) {
- last_row = i - 1;
- }
-
- if (i != 3) {
- is_first_persona = false;
- }
-
- if (i == last_store_position) {
- i--;
- container_grid.get_child_at (0, i).destroy ();
- }
- }
- }
-
- /**
- * Adjusts the ContactEditor for a new contact.
- * Use clear() to make sure nothing is lingering from the previous one.
- */
- public void set_new_contact () {
- remove_button.hide ();
- linked_button.hide ();
-
- create_avatar_button ();
- create_name_entry ();
- this.last_row = 2;
-
- 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;
- }
-
- public void clear () {
- foreach (var w in container_grid.get_children ()) {
- w.destroy ();
- }
-
- remove_button.set_sensitive (false);
- linked_button.set_sensitive (false);
-
- /* clean metadata as well */
- has_birthday_row = false;
- has_nickname_row = false;
- has_notes_row = false;
-
- writable_personas.clear ();
- contact = 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 (contact != null) {
- p.persona = contact.find_persona_from_uid (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 void add_new_row_for_property (Persona? p, string prop_name, string? type = null) {
- /* Somehow, I need to ensure that p is the main/default/first persona */
- Persona persona = null;
- if (contact != null) {
- if (p == null) {
- persona = new FakePersona (contact);
- writable_personas.set (persona.uid,
- new HashMap<string, Field?> ());
- } else {
- persona = p;
- }
- }
-
- 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 ();
- }
-
- // Creates the contact's current avatar in a big button on top of the Editor
- private void create_avatar_button () {
- this.avatar = new Avatar (PROFILE_SIZE, this.contact);
-
- var button = new Button ();
- button.get_accessible ().set_name (_("Change avatar"));
- button.image = this.avatar;
- button.clicked.connect (on_avatar_button_clicked);
-
- this.container_grid.attach (button, 0, 0, 1, 3);
- }
-
- // Show the avatar dialog when the avatar is clicked
- private void on_avatar_button_clicked (Button avatar_button) {
- var dialog = new AvatarSelector ((Window) get_toplevel (), this.contact);
- dialog.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);
- });
- dialog.run ();
- }
-
- 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;
- }
-
- // Creates the big name entry on the top
- private void create_name_entry () {
- 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.contact != null)
- this.name_entry.text = this.contact.individual.display_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, 3, 3);
- }
-
- public bool name_changed () {
- return this.name_entry.get_data<bool> ("changed");
- }
-
- public Value get_full_name_value () {
- Value v = Value (typeof (string));
- v.set_string (this.name_entry.get_text ());
- return v;
- }
-}
diff --git a/src/contacts-contact-pane.vala b/src/contacts-contact-pane.vala
index 3f6a443..38d9924 100644
--- a/src/contacts-contact-pane.vala
+++ b/src/contacts-contact-pane.vala
@@ -44,30 +44,29 @@ public class Contacts.ContactPane : Stack {
[GtkChild]
private Box contact_editor_page;
- private ContactEditor editor;
-
- private SimpleActionGroup edit_contact_actions;
- 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 },
- };
+ private Editor.ContactEditor? editor;
public bool on_edit_mode;
private LinkSuggestionGrid suggestion_grid;
- /* Signals */
+
public signal void contacts_linked (string? main_contact, string linked_contact, LinkOperation operation);
public signal void will_delete (Contact contact);
+
+ public ContactPane (Window parent_window, Store contacts_store) {
+ this.parent_window = parent_window;
+ this.store = contacts_store;
+ this.store.quiescent.connect (update_sheet);
+
+ create_contact_sheet ();
+
+ this.suggestion_grid = null;
+
+ /* edit mode widgetry, third page */
+ this.on_edit_mode = false;
+ }
+
public void update_sheet () {
if (on_edit_mode) {
/* this was triggered by some signal, do nothing */
@@ -86,7 +85,7 @@ public class Contacts.ContactPane : Stack {
foreach (var ind in matches.keys) {
var c = Contact.from_individual (ind);
if (c != null && contact.suggest_link_to (c)) {
- add_suggestion (c);
+ add_suggestion (c);
}
}
}
@@ -145,47 +144,6 @@ public class Contacts.ContactPane : Stack {
set_visible_child (this.none_selected_page);
}
- public ContactPane (Window parent_window, Store contacts_store) {
- this.parent_window = parent_window;
- this.store = contacts_store;
- this.store.quiescent.connect (update_sheet);
-
- this.edit_contact_actions = new SimpleActionGroup ();
- this.edit_contact_actions.add_action_entries (action_entries, this);
-
- create_contact_sheet ();
-
- this.suggestion_grid = null;
-
- /* edit mode widgetry, third page */
- this.on_edit_mode = false;
- this.editor = new ContactEditor (this.edit_contact_actions);
- this.editor.linked_button.clicked.connect (linked_accounts);
- this.editor.remove_button.clicked.connect (delete_contact);
- this.contact_editor_page.add (this.editor);
-
- /* 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.DEFAULT |
- BindingFlags.SYNC_CREATE |
- BindingFlags.INVERT_BOOLEAN);
-
- var notes_action = this.edit_contact_actions.lookup_action ("add.notes") as SimpleAction;
- this.editor.bind_property ("has-notes-row",
- notes_action, "enabled",
- BindingFlags.DEFAULT |
- BindingFlags.SYNC_CREATE |
- BindingFlags.INVERT_BOOLEAN);
- }
-
private void create_contact_sheet () {
this.sheet = new ContactSheet ();
this.sheet.hexpand = true;
@@ -205,16 +163,6 @@ public class Contacts.ContactPane : Stack {
this.contact_sheet_page.get_child ().get_style_context ().add_class ("view");
}
- 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 (contact.find_primary_persona (),
- tok[1],
- tok.length > 2 ? tok[2].up () : null);
- }
- }
-
private void linked_accounts () {
var dialog = new LinkedPersonasDialog (this.parent_window, contact);
if (dialog.run () == ResponseType.CLOSE && dialog.any_unlinked) {
@@ -225,6 +173,22 @@ public class Contacts.ContactPane : Stack {
dialog.destroy ();
}
+ // Start editing a contact: initialize and show the contact editor
+ private void load_contact_editor (Contact? contact) {
+ this.editor = new Editor.ContactEditor (contact, this.store);
+ this.editor.linked_button.clicked.connect (linked_accounts);
+ this.editor.remove_button.clicked.connect (delete_contact);
+ this.contact_editor_page.add (this.editor);
+ set_visible_child (this.contact_editor_page);
+ }
+
+ private void remove_contact_editor () {
+ SignalHandler.disconnect_by_func (this.editor.linked_button, (void*) linked_accounts, this);
+ SignalHandler.disconnect_by_func (this.editor.remove_button, (void*) delete_contact, this);
+ this.contact_editor_page.remove (this.editor);
+ this.editor = null;
+ }
+
void delete_contact () {
if (contact != null) {
contact.hide ();
@@ -238,72 +202,38 @@ public class Contacts.ContactPane : Stack {
return;
if (on_edit) {
- if (contact == null) {
- return;
- }
+ if (this.contact == null)
+ return;
- on_edit_mode = true;
+ this.on_edit_mode = true;
- sheet.clear ();
+ this.sheet.clear ();
if (suggestion_grid != null) {
- suggestion_grid.destroy ();
- suggestion_grid = null;
+ this.suggestion_grid.destroy ();
+ this.suggestion_grid = null;
}
- editor.clear ();
- editor.edit (contact);
- editor.show_all ();
- set_visible_child (this.contact_editor_page);
+ load_contact_editor (this.contact);
} else {
- on_edit_mode = false;
+ this.on_edit_mode = false;
/* saving changes */
if (!drop_changes) {
- foreach (var prop in editor.properties_changed ().entries) {
- Contact.set_persona_property.begin (prop.value.persona, prop.key, prop.value.value,
- (obj, result) => {
- try {
- Contact.set_persona_property.end (result);
- } catch (Error e2) {
- show_message (e2.message);
- update_sheet ();
- }
- });
- }
-
- if (editor.name_changed ()) {
- var v = editor.get_full_name_value ();
- Contact.set_individual_property.begin (contact,
- "full-name", v,
- (obj, result) => {
- try {
- Contact.set_individual_property.end (result);
- } catch (Error e) {
- show_message (e.message);
- /* FIXME: add this back */
- /* l.set_markup (Markup.printf_escaped ("<span font='16'>%s</span>", contact.display_name)); */
- }
- });
- }
- if (editor.avatar_changed ()) {
- var v = editor.get_avatar_value ();
- Contact.set_individual_property.begin (contact,
- "avatar", v,
- (obj, result) => {
- try {
- Contact.set_individual_property.end (result);
- } catch (GLib.Error e) {
- show_message (e.message);
- }
- });
- }
+ this.editor.save_changes.begin ( (obj, res) => {
+ try {
+ this.editor.save_changes.end (res);
+ } catch (Error e) {
+ show_message (e.message);
+ update_sheet ();
+ }
+ });
}
- editor.clear ();
+ remove_contact_editor ();
- if (contact != null) {
- sheet.clear ();
- sheet.update (contact);
+ if (this.contact != null) {
+ this.sheet.clear ();
+ this.sheet.update (contact);
set_visible_child (this.contact_sheet_page);
} else {
set_visible_child (this.none_selected_page);
@@ -321,54 +251,25 @@ public class Contacts.ContactPane : Stack {
suggestion_grid = null;
}
- editor.set_new_contact ();
-
- set_visible_child (this.contact_editor_page);
+ this.contact = null;
+ load_contact_editor (this.contact);
}
// Creates a new contact from the details in the ContactEditor
public async void create_contact () {
- var details = new HashTable<string, Value?> (str_hash, str_equal);
-
- // 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;
-
// Leave edit mode
set_edit_mode (false, true);
- if (details.size () == 0) {
- show_message_dialog (_("You need to enter some data"));
- return;
- }
-
- if (this.store.aggregator.primary_store == null) {
- show_message_dialog (_("No primary addressbook configured"));
- return;
- }
-
- // Create the contact
- var primary_store = this.store.aggregator.primary_store;
- Persona? persona = null;
try {
- persona = yield Contact.create_primary_persona_for_details (primary_store, details);
+ var contact = yield this.editor.save_changes ();
+ // Now show it to the user
+ if (contact != null)
+ this.parent_window.set_shown_contact (contact);
+ else
+ show_message_dialog (_("Unable to find newly created contact"));
} catch (Error e) {
show_message_dialog (_("Unable to create new contacts: %s").printf (e.message));
- return;
}
-
- // Now show it to the user
- var contact = this.store.find_contact_with_persona (persona);
- if (contact != null)
- this.parent_window.set_shown_contact (contact);
- else
- show_message_dialog (_("Unable to find newly created contact"));
}
private void show_message_dialog (string message) {
diff --git a/src/contacts-types.vala b/src/contacts-types.vala
index de8344d..0d2ce8f 100644
--- a/src/contacts-types.vala
+++ b/src/contacts-types.vala
@@ -227,24 +227,18 @@ public class Contacts.TypeSet : Object {
return _("Other");
}
- public void update_details (AbstractFieldDetails details, TreeIter iter) {
- var old_parameters = details.parameters;
- details.parameters = new HashMultiMap<string, string> ();
+ public void update_type_parameter (MultiMap<string, string> parameters, TreeIter iter) {
bool has_pref = false;
- foreach (var value in old_parameters.get ("type")) {
- if (value.ascii_casecmp ("PREF") == 0) {
- has_pref = true;
- break;
- }
- }
- foreach (var param in old_parameters.get_keys()) {
- if (param != "type" && param != X_GOOGLE_LABEL) {
- foreach (var value in old_parameters.get (param)) {
- details.parameters.set (param, value);
- }
+ foreach (var val in parameters["type"]) {
+ if (val.ascii_casecmp ("PREF") == 0) {
+ has_pref = true;
+ break;
}
}
+ parameters.remove_all("type");
+ parameters.remove_all(X_GOOGLE_LABEL);
+
Data data;
string display_name;
store.get (iter, 0, out display_name, 1, out data);
@@ -253,21 +247,21 @@ public class Contacts.TypeSet : Object {
assert (data != custom_dummy); // Not custom...
if (data == null) { // A custom label
- details.parameters.set ("type", "OTHER");
- details.parameters.set (X_GOOGLE_LABEL, display_name);
+ parameters["type"] = "OTHER";
+ parameters[X_GOOGLE_LABEL] = display_name;
} else {
if (data == other_dummy) {
- details.parameters.set ("type", "OTHER");
+ parameters["type"] = "OTHER";
} else {
- InitData *init_data = data.init_data.data;
- for (int j = 0; j < MAX_TYPES && init_data.types[j] != null; j++) {
- details.parameters.set ("type", init_data.types[j]);
- }
+ InitData *init_data = data.init_data.data;
+ for (int j = 0; j < MAX_TYPES && init_data.types[j] != null; j++) {
+ parameters["type"] = init_data.types[j];
+ }
}
}
if (has_pref)
- details.parameters.set ("type", "PREF");
+ parameters["type"] = "PREF";
}
public bool is_custom (TreeIter iter) {
@@ -493,9 +487,9 @@ public class Contacts.TypeCombo : Grid {
set_from_iter (iter);
}
- public void update_details (AbstractFieldDetails details) {
+ public void update_type_parameter (MultiMap<string, string> parameters) {
TreeIter iter;
combo.get_active_iter (out iter);
- type_set.update_details (details, iter);
+ type_set.update_type_parameter (parameters, iter);
}
}
diff --git a/src/editor/contacts-editor-addresses-editor.vala b/src/editor/contacts-editor-addresses-editor.vala
new file mode 100644
index 0000000..ebda5f2
--- /dev/null
+++ b/src/editor/contacts-editor-addresses-editor.vala
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2017 Niels De Graef <nielsdegraef@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 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 Folks;
+using Gee;
+using Gtk;
+
+public class Contacts.Editor.AddressesEditor : CompositeEditor<PostalAddressDetails, PostalAddressFieldDetails> {
+
+ public override string persona_property {
+ get { return "postal-addresses"; }
+ }
+
+ public AddressesEditor (PostalAddressDetails? details = null) {
+ if (details != null) {
+ var address_fields = Contact.sort_fields<PostalAddressFieldDetails>(details.postal_addresses);
+ foreach (var address_field_detail in address_fields)
+ this.child_editors.add (new AddressEditor (this, address_field_detail));
+ } else {
+ // No addresss were passed on => make a blank home address
+ this.child_editors.add (new AddressEditor (this, null, "HOME"));
+ }
+ }
+
+ public override async void save (PostalAddressDetails address_details) throws PropertyError {
+ yield address_details.change_postal_addresses (aggregate_children ());
+ }
+
+ public class AddressEditor : CompositeEditorChild<PostalAddressFieldDetails> {
+ private TypeCombo type_combo;
+ private Box address_widget;
+ private Button delete_button;
+
+ public Entry? entries[7]; /* must be the number of elements in postal_element_props */
+ 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 AddressEditor (AddressesEditor parent, PostalAddressFieldDetails? details = null, string? type = null) {
+ this.type_combo = parent.create_type_combo (TypeSet.general, details);
+ this.type_combo.valign = Gtk.Align.START;
+ this.address_widget = create_address_widget (parent);
+ this.delete_button = parent.create_delete_button ();
+ this.delete_button.valign = Gtk.Align.START;
+
+ if (details != null && details.value != null) {
+ var address = details.value;
+ this.entries[0].text = address.street ?? "";
+ this.entries[1].text = address.extension ?? "";
+ this.entries[2].text = address.locality ?? "";
+ this.entries[3].text = address.region ?? "";
+ this.entries[4].text = address.postal_code ?? "";
+ this.entries[5].text = address.po_box ?? "";
+ this.entries[6].text = address.country ?? "";
+ }
+ if (type != null)
+ this.type_combo.set_to (type);
+ }
+
+ public override int attach_to_grid (Grid container_grid, int row) {
+ container_grid.attach (this.type_combo, 0, row);
+ container_grid.attach (this.address_widget, 1, row);
+ container_grid.attach (this.delete_button, 2, row);
+
+ return 1;
+ }
+
+ public override PostalAddressFieldDetails create_details () {
+ var address = new PostalAddress (
+ this.entries[5].text, // po_box
+ this.entries[1].text, // extension
+ this.entries[0].text, // street
+ this.entries[2].text, // locality
+ this.entries[3].text, // region
+ this.entries[4].text, // postal_code
+ this.entries[6].text, // country
+ "derp?", // XXX
+ "");
+ // XXX parameters
+ return new PostalAddressFieldDetails (address, null);
+ }
+
+ private Box create_address_widget (AddressesEditor parent) {
+ var address_box = new Box(Orientation.VERTICAL, 0);
+ address_box.hexpand = true;
+ address_box.show ();
+
+ for (int i = 0; i < entries.length; i++) {
+ string? postal_part = null;
+ /* details.value.get (POSTAL_ELEMENT_PROPS[i], out postal_part); */
+
+ entries[i] = parent.create_entry (postal_part, POSTAL_ELEMENT_NAMES[i]);
+ entries[i].get_style_context ().add_class ("contacts-entry");
+ entries[i].get_style_context ().add_class ("contacts-postal-entry");
+ address_box.add (entries[i]);
+ }
+
+ return address_box;
+ }
+ }
+}
diff --git a/src/editor/contacts-editor-avatar-editor.vala b/src/editor/contacts-editor-avatar-editor.vala
new file mode 100644
index 0000000..ecb1e48
--- /dev/null
+++ b/src/editor/contacts-editor-avatar-editor.vala
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2017 Niels De Graef <nielsdegraef@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 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 Folks;
+using Gee;
+using Gtk;
+
+public class Contacts.Editor.AvatarEditor : DetailsEditor<AvatarDetails> {
+
+ private Contact? contact;
+
+ private Avatar avatar;
+
+ // The button containing the Avatar
+ private Button avatar_button;
+
+ private LoadableIcon? avatar_icon = null;
+
+ public override string persona_property {
+ get { return "avatar"; }
+ }
+
+ public AvatarEditor (Contact? contact = null, AvatarDetails? details = null) {
+ this.contact = contact;
+
+ //X XXX button
+ this.avatar = new Avatar (PROFILE_SIZE, contact);
+ this.avatar.vexpand = false;
+
+ this.avatar_button = new Button ();
+ this.avatar_button.image = this.avatar;
+ this.avatar_button.show ();
+ this.avatar_button.clicked.connect (on_avatar_button_clicked);
+ }
+
+ public override int attach_to_grid (Grid container_grid, int row) {
+ container_grid.attach (this.avatar_button, 0, row, 1, 3);
+ return 0;
+ }
+
+ public override async void save (AvatarDetails avatar_details) throws PropertyError {
+ yield avatar_details.change_avatar (this.avatar_icon);
+ }
+
+ public override Value create_value () {
+ Value v = Value (this.avatar_icon.get_type ());
+ v.set_object (this.avatar_icon);
+ return v;
+ }
+
+ // Show the avatar dialog when the avatar is clicked
+ private void on_avatar_button_clicked (Button button) {
+ var dialog = new AvatarSelector ((Gtk.Window) button.get_toplevel(), this.contact);
+ dialog.set_avatar.connect ( (icon) => {
+ this.avatar_icon = icon as LoadableIcon;
+ this.dirty = 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 (Error e) {
+ debug ("Couldn't load the chosen avatar: %s", e.message);
+ }
+
+ this.avatar.set_pixbuf (a_pixbuf);
+ });
+
+ dialog.run ();
+ dialog.destroy ();
+ }
+}
diff --git a/src/editor/contacts-editor-birthday-editor.vala b/src/editor/contacts-editor-birthday-editor.vala
new file mode 100644
index 0000000..f9c19ef
--- /dev/null
+++ b/src/editor/contacts-editor-birthday-editor.vala
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2017 Niels De Graef <nielsdegraef@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 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 Folks;
+using Gee;
+using Gtk;
+
+public class Contacts.Editor.BirthdayEditor : DetailsEditor<BirthdayDetails> {
+ private Label label;
+
+ private Grid date_grid;
+ private SpinButton day_spin;
+ private ComboBoxText month_combo;
+ private SpinButton year_spin;
+
+ private Button delete_button;
+
+ public override string persona_property {
+ get { return "birthday"; }
+ }
+
+ /**
+ * The day of the month (ranging from 1 to 31, depending on the month)
+ */
+ private int day {
+ get { return this.day_spin.get_value_as_int (); }
+ set { this.day_spin.set_value (value); }
+ }
+
+ /**
+ * The month (ranging from 1 to 12)
+ */
+ private int month {
+ get { return this.month_combo.get_active (); }
+ set { this.month_combo.set_active (value - 1); }
+ }
+
+ /**
+ * The year
+ */
+ private int year {
+ get { return this.year_spin.get_value_as_int (); }
+ set { this.year_spin.set_value (value); }
+ }
+
+ public BirthdayEditor (BirthdayDetails? details = null) {
+ DateTime date;
+ if (details != null && details.birthday != null)
+ date = details.birthday.to_local ();
+ else
+ date = new DateTime.now_local ();
+
+ this.label = create_label (_("Birthday"));
+ this.date_grid = create_date_widget (date);
+ this.delete_button = create_delete_button ();
+
+ this.day = date.get_day_of_month ();
+ this.month = date.get_month ();
+ this.year = date.get_year ();
+ set_day_spin_range ();
+
+ // Now that we've set the date for first time, listen to changes
+ this.day_spin.changed.connect ( () => { this.dirty = true; });
+ this.month_combo.changed.connect ( () => {
+ this.dirty = true;
+ set_day_spin_range ();
+ });
+ this.year_spin.changed.connect ( () => {
+ this.dirty = true;
+ set_day_spin_range ();
+ });
+ }
+
+ public override int attach_to_grid (Grid container_grid, int row) {
+ container_grid.attach (this.label, 0, row);
+ container_grid.attach (this.date_grid, 1, row);
+ container_grid.attach (this.delete_button, 2, row);
+
+ return 1;
+ }
+
+ public override async void save (BirthdayDetails birthday_details) throws PropertyError {
+ yield birthday_details.change_birthday (create_datetime ().to_utc ());
+ }
+
+ public override Value create_value () {
+ var result = Value (typeof (DateTime));
+ result.set_boxed (create_datetime ().to_utc ());
+ return result;
+ }
+
+ private DateTime create_datetime () {
+ return new DateTime.local (this.year, this.month + 1, this.day, 0, 0, 0);
+ }
+
+ private Grid create_date_widget (DateTime? date) {
+ var date_grid = new Grid ();
+ date_grid.column_spacing = 12;
+
+ // Day
+ this.day_spin = new SpinButton.with_range (1.0, 31.0, 1.0);
+ this.day_spin.digits = 0;
+ this.day_spin.numeric = true;
+ date_grid.add (day_spin);
+
+ // Month
+ this.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);
+ this.month_combo.append_text (month.format ("%B"));
+ }
+ this.month_combo.get_style_context ().add_class ("contacts-combo");
+ this.month_combo.hexpand = true;
+ date_grid.add (month_combo);
+
+ // Year
+ this.year_spin = new SpinButton.with_range (1800, 3000, 1);
+ this.year_spin.digits = 0;
+ this.year_spin.numeric = true;
+ date_grid.add (year_spin);
+
+ date_grid.show_all ();
+ return date_grid;
+ }
+
+ private void set_day_spin_range () {
+ var days_in_month = Date.get_days_in_month ((DateMonth) this.month, (DateYear) this.year);
+ this.day_spin.set_range (1, days_in_month);
+ }
+}
diff --git a/src/editor/contacts-editor-composite-editor.vala b/src/editor/contacts-editor-composite-editor.vala
new file mode 100644
index 0000000..7a400a3
--- /dev/null
+++ b/src/editor/contacts-editor-composite-editor.vala
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2017 Niels De Graef <nielsdegraef@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 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 Folks;
+using Gee;
+using Gtk;
+
+/**
+ * An interface for DetailsEditors that contain multiple child Element.
+ * It has a ChildDetails type (C), for the Details a child widget represents
+ */
+public abstract class Contacts.Editor.CompositeEditor<D, C> : DetailsEditor<D> {
+
+ protected Gee.List<CompositeEditorChild<C>> child_editors = new LinkedList<CompositeEditorChild<C>> ();
+
+ public override int attach_to_grid (Grid container_grid, int start_row) {
+ var current_row = start_row;
+ foreach (var child_editor in this.child_editors)
+ current_row += child_editor.attach_to_grid (container_grid, current_row);
+
+ return current_row - start_row;
+ }
+
+ public override Value create_value () {
+ var children = aggregate_children ();
+ var val = Value (children.get_type ());
+ val.set_object (children);
+ return val;
+ }
+
+ protected HashSet<C> aggregate_children () {
+ var children = new HashSet<C> ();
+ foreach (var child_editor in this.child_editors)
+ children.add (child_editor.create_details ());
+ return children;
+ }
+}
+
+/**
+ * A child to a CompositeEditor.
+ */
+public abstract class Contacts.Editor.CompositeEditorChild<D> : Object {
+
+ protected MultiMap<string, string> parameters;
+
+ /**
+ * Creates the details for this CompositeEditorChild, based on the (edited) values.
+ */
+ public abstract D create_details ();
+
+ public abstract int attach_to_grid (Grid container_grid, int start_row);
+}
diff --git a/src/editor/contacts-editor-contact-editor.vala b/src/editor/contacts-editor-contact-editor.vala
new file mode 100644
index 0000000..41eea24
--- /dev/null
+++ b/src/editor/contacts-editor-contact-editor.vala
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2011 Alexander Larsson <alexl@redhat.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 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 errordomain Contacts.SaveError {
+ EMPTY_DATA,
+ NO_PRIMARY_ADDRESSBOOK,
+}
+
+[GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-contact-editor.ui")]
+public class Contacts.Editor.ContactEditor : Grid {
+
+ private const string[] DEFAULT_PROPS_NEW_CONTACT = {
+ "email-addresses",
+ "phone-numbers",
+ "postal-addresses"
+ };
+
+ // We have a form with fields for each persona.
+ private struct Form {
+ Persona? persona; // null iff new contact
+ Gee.List<Editor.DetailsEditor> editors;
+ }
+
+ // The contact we're editing, or null if creating a new one.
+ private Contact? contact;
+
+ private Store store;
+
+ // The first row of the container_grid that is empty.
+ private int next_row = 0;
+
+ private Gee.List<Form?> forms = new LinkedList<Form?> ();
+
+ private Editor.DetailsEditorFactory details_editor_factory = new Editor.DetailsEditorFactory ();
+
+ [GtkChild]
+ private Grid container_grid;
+
+ // Template subwidgets
+ [GtkChild]
+ private ScrolledWindow main_sw;
+ [GtkChild]
+ private MenuButton add_detail_button;
+ [GtkChild]
+ public Button linked_button;
+ [GtkChild]
+ public Button remove_button;
+
+ // Actions
+ private SimpleActionGroup edit_contact_actions;
+ private const GLib.ActionEntry[] action_entries = {
+ { "add-email-addresses", on_add_detail },
+ { "add-phone-numbers", on_add_detail },
+ { "add-urls", on_add_detail },
+ { "add-nickname", on_add_detail },
+ { "add-birthday", on_add_detail },
+ { "add-postal-addresses", on_add_detail },
+ { "add-notes", on_add_detail },
+ };
+
+ 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;
+ }
+
+ public ContactEditor (Contact? contact, Store store) {
+ this.contact = contact;
+ this.store = store;
+
+ create_actions ();
+ init_layout ();
+
+ if (contact != null) {
+ // Load the contact's personas and their editable properties
+ bool first_persona = true;
+ foreach (var persona in contact.get_personas_for_display ()) {
+ add_widgets_for_persona (persona, first_persona);
+ first_persona = false;
+ }
+
+ // Show "Remove" and "Link" buttons
+ this.remove_button.show ();
+ this.remove_button.sensitive = this.contact.can_remove_personas ();
+ this.linked_button.show ();
+ this.linked_button.sensitive = this.contact.individual.personas.size > 1;
+ } else {
+ // Init the editor with the default properties
+ add_widgets_for_persona (null);
+ }
+ }
+
+ private void create_actions () {
+ this.edit_contact_actions = new SimpleActionGroup ();
+ this.edit_contact_actions.add_action_entries (action_entries, this);
+ }
+
+ // Initializes the basic layout
+ private void init_layout () {
+ this.container_grid.set_focus_vadjustment (this.main_sw.get_vadjustment ());
+
+ this.main_sw.get_child ().get_style_context ().add_class ("contacts-main-view");
+ this.main_sw.get_child ().get_style_context ().add_class ("view");
+
+ this.add_detail_button.get_popover ().insert_action_group ("edit", this.edit_contact_actions);
+
+ // enable/disable actions
+ var birthday_action = this.edit_contact_actions.lookup_action ("add.birthday") as SimpleAction;
+ // XXX de volgende dingen werken niet meer want die properties zijn weg :-)
+ /* 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;
+ /* bind_property ("has-nickname-row", nickname_action, "enabled", */
+ /* BindingFlags.DEFAULT | BindingFlags.SYNC_CREATE | BindingFlags.INVERT_BOOLEAN); */
+
+ var notes_action = this.edit_contact_actions.lookup_action ("add.notes") as SimpleAction;
+ /* bind_property ("has-notes-row", notes_action, "enabled", */
+ /* BindingFlags.DEFAULT | BindingFlags.SYNC_CREATE | BindingFlags.INVERT_BOOLEAN); */
+ }
+
+ // Adds the widgets for the details in a persona
+ private void add_widgets_for_persona (Persona? persona, bool first_persona = true) {
+ var form = Form ();
+ form.persona = persona;
+ form.editors = new ArrayList<Editor.DetailsEditor> ();
+ this.forms.add (form);
+
+ if (first_persona) {
+ create_avatar_frame (form);
+ create_name_entry (form);
+ this.next_row += 3;
+ } else {
+ // Don't show the name on the default persona
+ var store_name = new Label (Contact.format_persona_store_name_for_contact (persona));
+ store_name.halign = Align.START;
+ store_name.xalign = 0.0f; // XXX don't use xalign
+ store_name.margin_start = 6;
+ this.container_grid.attach (store_name, 0, this.next_row, 2);
+ this.next_row++;
+ }
+
+ string[] writeable_props;
+ if (persona != null)
+ writeable_props = Contact.sort_persona_properties (persona.writeable_properties);
+ else
+ writeable_props = DEFAULT_PROPS_NEW_CONTACT;
+
+ foreach (var prop in writeable_props)
+ add_property (form, prop, (persona == null));
+ }
+
+ private void add_property (Form form, string prop_name, bool allow_empty = false) {
+ var editor = this.details_editor_factory.create_details_editor (form.persona, prop_name, allow_empty);
+ if (editor != null) {
+ form.editors.add (editor);
+ var rows_added = editor.attach_to_grid (this.container_grid, this.next_row);
+ this.next_row += rows_added;
+ }
+ }
+
+ // Creates the contact's current avatar, the big frame on top of the Editor
+ private void create_avatar_frame (Form form) {
+ var avatar_editor = new Editor.AvatarEditor (this.contact, form.persona as AvatarDetails);
+ avatar_editor.attach_to_grid (this.container_grid, 0);
+ form.editors.add (avatar_editor);
+ }
+
+ // Creates the big name entry on the top
+ private void create_name_entry (Form form) {
+ var full_name_editor = new Editor.FullNameEditor (this.contact, form.persona as NameDetails);
+ full_name_editor.attach_to_grid (this.container_grid, 0);
+ form.editors.add (full_name_editor);
+ }
+
+ public async Contact save_changes () throws Error {
+ if (this.contact == null) {
+ var details = new HashTable<string, Value?> (str_hash, str_equal);
+ var contacts_store = this.store;
+
+ //XXX check if name is filled in
+ var form = this.forms[0];
+ foreach (var details_editor in form.editors)
+ if (details_editor.dirty)
+ details[details_editor.persona_property] = details_editor.create_value ();
+
+ if (details.size () != 0)
+ throw new SaveError.EMPTY_DATA (_("You need to enter some data"));
+
+ if (contacts_store.aggregator.primary_store == null)
+ throw new SaveError.NO_PRIMARY_ADDRESSBOOK (_("No primary addressbook configured"));
+
+ // Create the contact
+ var primary_store = contacts_store.aggregator.primary_store;
+ var persona = yield Contact.create_primary_persona_for_details (primary_store, details);
+
+ return contacts_store.find_contact_with_persona (persona);
+ }
+
+ //XXX check for empty values
+ warning("SAVING WITH %d forms", this.forms.size);
+ foreach (var form in this.forms) {
+ warning("FORM WITH %d editors", form.editors.size);//XXX
+ foreach (var details_editor in form.editors) {
+ debug("FORM EDITOR %s (dirty: %s)", details_editor.get_type().name(), (details_editor.dirty).to_string());//XXX
+ if (details_editor.dirty)
+ yield details_editor.save_to_persona (form.persona);
+ }
+ }
+ return this.contact;
+ }
+
+ private void on_add_detail (SimpleAction action, Variant? parameter) {
+ var tok = action.name.split ("-", 2);
+
+ // The name of the property we're adding
+ var property = tok[1];
+
+ // Get the form for the primary persona (if any)
+ Form? form = null;
+ if (contact != null) {
+ var primary_persona = contact.find_primary_persona ();
+ foreach (var f in this.forms) {
+ if (f.persona == primary_persona) {
+ form = f;
+ break;
+ }
+ }
+ }
+ form = form ?? this.forms[0]; // Take the first form available
+
+ // Add the property to the form
+ add_property (form, property, true);
+ }
+}
diff --git a/src/editor/contacts-editor-details-editor-factory.vala b/src/editor/contacts-editor-details-editor-factory.vala
new file mode 100644
index 0000000..854846d
--- /dev/null
+++ b/src/editor/contacts-editor-details-editor-factory.vala
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2017 Niels De Graef <nielsdegraef@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 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 Folks;
+using Gee;
+using Gtk;
+
+/**
+ * A Factory for DetailEditors.
+ */
+public class Contacts.Editor.DetailsEditorFactory : Object {
+
+ /**
+ * Creates a DetailEditor for a specific property, given a persona.
+ * @return The newly created editor, or null if no editor was created.
+ */
+ public DetailsEditor? create_details_editor (Persona? p, string prop_name, bool allow_empty = false) {
+ switch (prop_name) {
+ case "birthday":
+ return create_birthday_editor (p, allow_empty);
+ case "email-addresses":
+ return create_emails_editor (p, allow_empty);
+ case "nickname":
+ return create_nickname_editor (p, allow_empty);
+ case "notes":
+ return create_notes_editor (p, allow_empty);
+ case "phone-numbers":
+ return create_phones_editor (p, allow_empty);
+ case "postal-addresses":
+ return create_addresses_editor (p, allow_empty);
+ case "urls":
+ return create_urls_editor (p, allow_empty);
+ default:
+ debug ("Unsupported property name \"%s\"", prop_name);
+ return null;
+ }
+ }
+
+ public BirthdayEditor? create_birthday_editor (Persona? p, bool allow_empty) {
+ var birthday_details = p as BirthdayDetails;
+ if (!allow_empty && (birthday_details == null || birthday_details.birthday == null))
+ return null;
+ return new BirthdayEditor (p as BirthdayDetails);
+ }
+
+ public EmailsEditor? create_emails_editor (Persona? p, bool allow_empty) {
+ var email_details = p as EmailDetails;
+ if (!allow_empty && (email_details == null || email_details.email_addresses.is_empty))
+ return null;
+ return new EmailsEditor (email_details);
+ }
+
+ public NicknameEditor? create_nickname_editor (Persona? p, bool allow_empty) {
+ var name_details = p as NameDetails;
+ if (!allow_empty && (name_details == null || name_details.nickname == null || name_details.nickname == ""))
+ return null;
+ return new NicknameEditor (name_details);
+ }
+
+ public NotesEditor? create_notes_editor (Persona? p, bool allow_empty) {
+ var note_details = p as NoteDetails;
+ if (!allow_empty && (note_details == null || note_details.notes.is_empty))
+ return null;
+ return new NotesEditor (note_details);
+ }
+
+ public PhonesEditor? create_phones_editor (Persona? p, bool allow_empty) {
+ var phone_details = p as PhoneDetails;
+ if (!allow_empty && (phone_details == null || phone_details.phone_numbers.is_empty))
+ return null;
+ return new PhonesEditor (phone_details);
+ }
+
+ public AddressesEditor? create_addresses_editor (Persona? p, bool allow_empty) {
+ var address_details = p as PostalAddressDetails;
+ if (!allow_empty && (address_details == null || address_details.postal_addresses.is_empty))
+ return null;
+ return new AddressesEditor (address_details);
+ }
+
+ public UrlsEditor? create_urls_editor (Persona? p, bool allow_empty) {
+ var url_details = p as UrlDetails;
+ if (!allow_empty && (url_details == null || url_details.urls.is_empty))
+ return null;
+ return new UrlsEditor (url_details);
+ }
+}
diff --git a/src/editor/contacts-editor-details-editor.vala b/src/editor/contacts-editor-details-editor.vala
new file mode 100644
index 0000000..1301d33
--- /dev/null
+++ b/src/editor/contacts-editor-details-editor.vala
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2017 Niels De Graef <nielsdegraef@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 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 Folks;
+using Gee;
+using Gtk;
+
+/**
+ * A DetailsEditor is an Element that can handle a specific property of a Persona.
+ */
+public abstract class Contacts.Editor.DetailsEditor<D> : Object {
+
+ /**
+ * Fired when the user asks to remove the EditorElement.
+ */
+ public signal void removed ();
+
+ /**
+ * Returns whether the DetailsEditor has unsaved changes.
+ */
+ public bool dirty { get; protected set; default = false; }
+
+ /**
+ * Returns the Persona property (well, the string) this EditorElement takes care of.
+ */
+ public abstract string persona_property { get; }
+
+ /**
+ * Attaches the element to the grid (possibly over multiple rows).
+ *
+ * @param container_grid The grid to which the element should be added.
+ * @param start_row The row at which we should start editing.
+ *
+ * @return The amount of rows that were added to the grid by this EditorElement.
+ */
+ public abstract int attach_to_grid (Grid container_grid, int start_row);
+
+ /**
+ * Saves the (edited) value to the Details object.
+ */
+ public abstract async void save (D details) throws PropertyError;
+
+ public async void save_to_persona (Persona persona) throws PropertyError {
+ yield save ((D) persona);
+ }
+
+ /**
+ * Returns a Value that can be used for methods like Folks.PersonaStore.add_persona_from_details()
+ */
+ public abstract Value create_value ();
+
+ /* Helper methods for building
+ ----------------------------- */
+ public TypeCombo create_type_combo (TypeSet type_set, AbstractFieldDetails? details = null) {
+ var combo = new TypeCombo (type_set);
+ combo.hexpand = false;
+ if (details != null)
+ combo.set_active (details);
+ combo.valign = Align.CENTER; // XXX why not START?
+ combo.changed.connect (() => { this.dirty = true; });
+ combo.show ();
+
+ return combo;
+ }
+
+ public Label create_label (string text) {
+ var label = new Label (text);
+ label.hexpand = false;
+ label.valign = Align.START;
+ label.halign = Align.END;
+ label.margin_end = 6;
+ label.get_style_context ().add_class ("dim-label");
+ label.show ();
+
+ return label;
+ }
+
+ public Entry create_entry (string? text, string? placeholder = null) {
+ var entry = new Entry ();
+ entry.hexpand = true;
+ if (text != null)
+ entry.text = text;
+ if (placeholder != null)
+ entry.placeholder_text = placeholder;
+ entry.show ();
+
+ entry.changed.connect (() => { this.dirty = true; });
+
+ return entry;
+ }
+
+ // XXX scrolledwindow?
+ public ScrolledWindow create_textview (string? text = null) {
+ var sw = new ScrolledWindow (null, null);
+ sw.shadow_type = ShadowType.OUT;
+ sw.set_size_request (-1, 100);
+
+ var value_text = new TextView ();
+ if (text != null)
+ value_text.get_buffer ().set_text (text);
+ value_text.hexpand = true;
+
+ sw.add (value_text);
+ sw.show_all ();
+
+ value_text.get_buffer ().changed.connect (() => { this.dirty = true; });
+
+ /* return value_text; */
+ return sw;
+ }
+
+ public Button create_delete_button () {
+ var delete_button = new Button.from_icon_name ("edit-delete-symbolic");
+ delete_button.valign = Align.START;
+ delete_button.get_accessible ().set_name (_("Delete field"));
+ delete_button.get_style_context ().add_class ("flat");
+ delete_button.clicked.connect (() => removed ());
+ delete_button.show ();
+
+ return delete_button;
+ }
+}
diff --git a/src/editor/contacts-editor-emails-editor.vala b/src/editor/contacts-editor-emails-editor.vala
new file mode 100644
index 0000000..b21e07b
--- /dev/null
+++ b/src/editor/contacts-editor-emails-editor.vala
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2017 Niels De Graef <nielsdegraef@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 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 Folks;
+using Gee;
+using Gtk;
+
+public class Contacts.Editor.EmailsEditor : CompositeEditor<EmailDetails, EmailFieldDetails> {
+
+ public override string persona_property {
+ get { return "email-addresses"; }
+ }
+
+ public EmailsEditor (EmailDetails? details = null) {
+ if (details != null) {
+ var email_fields = Contact.sort_fields<EmailFieldDetails>(details.email_addresses);
+ foreach (var email_field_detail in email_fields)
+ this.child_editors.add (new EmailEditor (this, email_field_detail));
+ } else {
+ // No emails were passed on => make a single personal email address
+ this.child_editors.add (new EmailEditor (this, null, "PERSONAL"));
+ }
+ }
+
+ public override async void save (EmailDetails email_details) throws PropertyError {
+ yield email_details.change_email_addresses (aggregate_children ());
+ }
+
+ /**
+ * Deals with a single email address field.
+ */
+ public class EmailEditor : CompositeEditorChild<EmailFieldDetails> {
+ private TypeCombo type_combo;
+ private Entry email_entry;
+ private Button delete_button;
+
+ public EmailEditor (EmailsEditor parent, EmailFieldDetails? details = null, string? type = null) {
+ this.type_combo = parent.create_type_combo (TypeSet.email, details);
+ string? email = (details != null)? details.value : null;
+ this.email_entry = parent.create_entry (email, _("Add email"));
+ this.delete_button = parent.create_delete_button ();
+
+ if (type != null)
+ this.type_combo.set_to (type);
+ }
+
+ public override int attach_to_grid (Grid container_grid, int row) {
+ container_grid.attach (this.type_combo, 0, row);
+ container_grid.attach (this.email_entry, 1, row);
+ container_grid.attach (this.delete_button, 2, row);
+
+ return 1;
+ }
+
+ public override EmailFieldDetails create_details () {
+ // XXX parameters
+ return new EmailFieldDetails (this.email_entry.text, null);
+ }
+ }
+}
diff --git a/src/editor/contacts-editor-full-name-editor.vala b/src/editor/contacts-editor-full-name-editor.vala
new file mode 100644
index 0000000..32622a9
--- /dev/null
+++ b/src/editor/contacts-editor-full-name-editor.vala
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2017 Niels De Graef <nielsdegraef@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 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 Folks;
+using Gee;
+using Gtk;
+
+public class Contacts.Editor.FullNameEditor : DetailsEditor<NameDetails> {
+
+ private Entry name_entry;
+
+ public override string persona_property {
+ get { return "full-name"; }
+ }
+
+ public FullNameEditor (Contact? contact = null, NameDetails? details = null) {
+ string? name = (contact != null)? contact.individual.display_name : null;
+ this.name_entry = create_entry (name, _("Add name"));
+ this.name_entry.valign = Align.CENTER;
+ }
+
+ public override int attach_to_grid (Grid container_grid, int row) {
+ container_grid.attach (this.name_entry, 1, row, 2, 3);
+ return 0;
+ }
+
+ public override async void save (NameDetails name_details) throws PropertyError {
+ yield name_details.change_full_name (this.name_entry.text);
+ }
+
+ public override Value create_value () {
+ Value v = Value (typeof (string));
+ v.set_string (this.name_entry.text);
+ return v;
+ }
+}
diff --git a/src/editor/contacts-editor-nickname-editor.vala b/src/editor/contacts-editor-nickname-editor.vala
new file mode 100644
index 0000000..2ad0093
--- /dev/null
+++ b/src/editor/contacts-editor-nickname-editor.vala
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2017 Niels De Graef <nielsdegraef@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 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 Folks;
+using Gee;
+using Gtk;
+
+public class Contacts.Editor.NicknameEditor : DetailsEditor<NameDetails> {
+ private Label label;
+ private Entry nickname_entry;
+ private Button delete_button;
+
+ public override string persona_property {
+ get { return "nickname"; }
+ }
+
+ public NicknameEditor (NameDetails? details = null) {
+ this.label = create_label (_("Nickname"));
+ string? nickname = (details != null)? details.nickname : null;
+ this.nickname_entry = create_entry (nickname);
+ this.delete_button = create_delete_button ();
+ }
+
+ public override int attach_to_grid (Grid container_grid, int row) {
+ container_grid.attach (this.label, 0, row);
+ container_grid.attach (this.nickname_entry, 1, row);
+ container_grid.attach (this.delete_button, 2, row);
+
+ return 1;
+ }
+
+ public override async void save (NameDetails name_details) throws PropertyError {
+ yield name_details.change_nickname (this.nickname_entry.text);
+ }
+
+ public override Value create_value () {
+ var result = Value (typeof (string));
+ result.set_string (nickname_entry.text);
+ return result;
+ }
+}
diff --git a/src/editor/contacts-editor-notes-editor.vala b/src/editor/contacts-editor-notes-editor.vala
new file mode 100644
index 0000000..d177a54
--- /dev/null
+++ b/src/editor/contacts-editor-notes-editor.vala
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2017 Niels De Graef <nielsdegraef@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 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 Folks;
+using Gee;
+using Gtk;
+
+/**
+ * Deals with multiple "Notes"
+ */
+public class Contacts.Editor.NotesEditor : CompositeEditor<NoteDetails, NoteFieldDetails> {
+
+ public override string persona_property {
+ get { return "notes"; }
+ }
+
+ public NotesEditor (NoteDetails? details = null) {
+ if (details != null) {
+ foreach (var note_field_detail in details.notes)
+ this.child_editors.add (new NoteEditor (this, note_field_detail));
+ } else {
+ // No notes were passed on => make a single blank editor
+ this.child_editors.add (new NoteEditor (this));
+ }
+ }
+
+ public override async void save (NoteDetails note_details) throws PropertyError {
+ yield note_details.change_notes (aggregate_children ());
+ }
+
+ /**
+ * Deals with a single "Notes" field.
+ */
+ public class NoteEditor : CompositeEditorChild<NoteFieldDetails> {
+ private Label label;
+ private ScrolledWindow note_textview;
+ private Button delete_button;
+
+ public NoteEditor (NotesEditor parent, NoteFieldDetails? details = null) {
+ this.label = parent.create_label (_("Note"));
+ var text = (details != null)? details.value : null;
+ this.note_textview = parent.create_textview (text);
+ this.delete_button = parent.create_delete_button ();
+ }
+
+ public override int attach_to_grid (Grid container_grid, int row) {
+ container_grid.attach (this.label, 0, row);
+ container_grid.attach (this.note_textview, 1, row);
+ container_grid.attach (this.delete_button, 2, row);
+
+ return 1;
+ }
+
+ public override NoteFieldDetails create_details () {
+ // XXX parameters
+ // XXX scrolledwindow
+ return new NoteFieldDetails ("test niels", null);
+ /* return new NoteFieldDetails (this.note_textview.buffer.text, null); */
+ }
+ }
+}
diff --git a/src/editor/contacts-editor-phones-editor.vala b/src/editor/contacts-editor-phones-editor.vala
new file mode 100644
index 0000000..e601693
--- /dev/null
+++ b/src/editor/contacts-editor-phones-editor.vala
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2017 Niels De Graef <nielsdegraef@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 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 Folks;
+using Gee;
+using Gtk;
+
+public class Contacts.Editor.PhonesEditor : CompositeEditor<PhoneDetails, PhoneFieldDetails> {
+
+ public override string persona_property {
+ get { return "phone-numbers"; }
+ }
+
+ public PhonesEditor (PhoneDetails? details = null) {
+ if (details != null) {
+ var phone_fields = Contact.sort_fields<PhoneFieldDetails>(details.phone_numbers);
+ foreach (var phone_nr_detail in phone_fields)
+ this.child_editors.add (new PhoneEditor (this, phone_nr_detail));
+ } else {
+ // No phones were passed on => make a single cell phone number
+ this.child_editors.add (new PhoneEditor (this, null, "CELL"));
+ }
+ }
+
+ public override async void save (PhoneDetails phone_details) throws PropertyError {
+ yield phone_details.change_phone_numbers (aggregate_children ());
+ }
+
+ public class PhoneEditor : CompositeEditorChild<PhoneFieldDetails> {
+ private TypeCombo type_combo;
+ private Entry phone_entry;
+ private Button delete_button;
+
+ public PhoneEditor (PhonesEditor parent, PhoneFieldDetails? details = null, string? type = null) {
+ this.type_combo = parent.create_type_combo (TypeSet.phone, details);
+ string? phone_nr = (details != null)? details.value : null;
+ this.phone_entry = parent.create_entry (phone_nr, _("Add number"));
+ this.delete_button = parent.create_delete_button ();
+
+ if (details != null && details.parameters != null)
+ this.parameters = details.parameters;
+ else
+ this.parameters = new HashMultiMap<string, string> ();
+
+ if (type != null)
+ this.type_combo.set_to (type);
+ }
+
+ public override int attach_to_grid (Grid container_grid, int row) {
+ container_grid.attach (this.type_combo, 0, row);
+ container_grid.attach (this.phone_entry, 1, row);
+ container_grid.attach (this.delete_button, 2, row);
+
+ return 1;
+ }
+
+ public override PhoneFieldDetails create_details () {
+ this.type_combo.update_type_parameter (this.parameters);
+ return new PhoneFieldDetails (this.phone_entry.text, this.parameters);
+ }
+ }
+}
diff --git a/src/editor/contacts-editor-urls-editor.vala b/src/editor/contacts-editor-urls-editor.vala
new file mode 100644
index 0000000..15fad62
--- /dev/null
+++ b/src/editor/contacts-editor-urls-editor.vala
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2017 Niels De Graef <nielsdegraef@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 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 Folks;
+using Gee;
+using Gtk;
+
+public class Contacts.Editor.UrlsEditor : CompositeEditor<UrlDetails, UrlFieldDetails> {
+
+ public override string persona_property {
+ get { return "urls"; }
+ }
+
+ public UrlsEditor (UrlDetails? details = null) {
+ if (details != null) {
+ /* var url_fields = Contact.sort_fields<UrlFieldDetails>(details.urls); */
+ /* foreach (var url_field_detail in url_fields) */
+ foreach (var url_field_detail in details.urls)
+ this.child_editors.add (new UrlEditor (this, url_field_detail));
+ } else {
+ // No urls were passed on => make a single blank editor
+ this.child_editors.add (new UrlEditor (this));
+ }
+ }
+
+ public override async void save (UrlDetails url_details) throws PropertyError {
+ yield url_details.change_urls (aggregate_children ());
+ }
+
+ public class UrlEditor : CompositeEditorChild<UrlFieldDetails> {
+ private Label label;
+ private Entry url_entry;
+ private Button delete_button;
+
+ public UrlEditor (UrlsEditor parent, UrlFieldDetails? details = null) {
+ this.label = parent.create_label (_("Website"));
+ this.url_entry = parent.create_entry ((details != null)? details.value : null);
+ this.delete_button = parent.create_delete_button ();
+ }
+
+ public override int attach_to_grid (Grid container_grid, int row) {
+ container_grid.attach (this.label, 0, row);
+ container_grid.attach (this.url_entry, 1, row);
+ container_grid.attach (this.delete_button, 2, row);
+
+ return 1;
+ }
+
+ public override UrlFieldDetails create_details () {
+ // XXX parameters
+ return new UrlFieldDetails (this.url_entry.text, null);
+ }
+ }
+}
diff --git a/src/meson.build b/src/meson.build
index fc20c72..b55f85e 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -6,11 +6,24 @@ install_data('org.gnome.Contacts.gschema.xml',
# The gnome-contacts binary
contacts_vala_sources = [
+ 'editor/contacts-editor-addresses-editor.vala',
+ 'editor/contacts-editor-avatar-editor.vala',
+ 'editor/contacts-editor-birthday-editor.vala',
+ 'editor/contacts-editor-composite-editor.vala',
+ 'editor/contacts-editor-contact-editor.vala',
+ 'editor/contacts-editor-details-editor-factory.vala',
+ 'editor/contacts-editor-details-editor.vala',
+ 'editor/contacts-editor-emails-editor.vala',
+ 'editor/contacts-editor-full-name-editor.vala',
+ 'editor/contacts-editor-nickname-editor.vala',
+ 'editor/contacts-editor-notes-editor.vala',
+ 'editor/contacts-editor-phones-editor.vala',
+ 'editor/contacts-editor-urls-editor.vala',
+
'contacts-accounts-list.vala',
'contacts-app.vala',
'contacts-avatar.vala',
'contacts-avatar-selector.vala',
- 'contacts-contact-editor.vala',
'contacts-contact-list.vala',
'contacts-contact-pane.vala',
'contacts-contact-sheet.vala',