diff options
author | Niels De Graef <nielsdegraef@gmail.com> | 2017-09-20 02:11:59 +0200 |
---|---|---|
committer | Niels De Graef <nielsdegraef@gmail.com> | 2018-01-26 11:17:43 +0100 |
commit | 7f1dd656af97658ecc7565fc704a3080b9dc045f (patch) | |
tree | d6c06170ea9f60b86b79f26f13e5cb8dcd40874e | |
parent | 80c33aee1776aafd2378509938f8228251288738 (diff) | |
download | gnome-contacts-wip/nielsdg/contact-editor-rewrite.tar.gz |
ContactEditor: Rewritewip/nielsdg/contact-editor-rewrite
TODO:
* Check whether changes are always properly saved
* Check if saved when no name was filled in
* Add properties
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', |