summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNiels De Graef <nielsdegraef@gmail.com>2018-12-01 17:42:24 +0100
committerNiels De Graef <nielsdegraef@gmail.com>2019-03-19 20:28:36 +0100
commite9bbc8e4f79436a167ee4197c72e4c9db64f0251 (patch)
treeffcfd049a22256c65930806129967abc3895065b
parent332d438f1bc1d746106c9a0873a816f2081cd6be (diff)
downloadgnome-contacts-wip/nielsdg/plugins-for-form-fields.tar.gz
WIP: Rewrite the ContactEditor/ContactSheetwip/nielsdg/plugins-for-form-fields
Rather than using a large if-else construct with several copy-pasted chunks, we create a `PersonaProperty` class, which represents a property instance of a persona. These object can then create an appropriate widget that allows the user to view, edit or remove that specific property. As a consequence, we can extract and simplify parts of the code on how to keep the list of properties saved: we can just have a `GListModel` of said property instances in the common `ContactForm` parent class. The subclasses `ContactSheet` and `ContactEditor` can then deal with this list appropriately. In the long term, we can simplify this probably even more; for now, it's good enough to have this over the old code, which was getting a bit unmaintainable.
-rw-r--r--data/ui/contacts-contact-form.ui14
-rw-r--r--data/ui/style.css4
-rw-r--r--src/contacts-contact-editor.vala890
-rw-r--r--src/contacts-contact-form.vala120
-rw-r--r--src/contacts-contact-pane.vala60
-rw-r--r--src/contacts-contact-sheet.vala242
-rw-r--r--src/contacts-contact.vala153
-rw-r--r--src/contacts-persona-property.vala1106
-rw-r--r--src/contacts-type-combo.vala1
-rw-r--r--src/contacts-type-descriptor.vala85
-rw-r--r--src/contacts-utils.vala68
-rw-r--r--src/meson.build1
12 files changed, 1528 insertions, 1216 deletions
diff --git a/data/ui/contacts-contact-form.ui b/data/ui/contacts-contact-form.ui
index b0c1fd1..6233fad 100644
--- a/data/ui/contacts-contact-form.ui
+++ b/data/ui/contacts-contact-form.ui
@@ -3,6 +3,7 @@
<!-- interface-requires gtk+ 3.22 -->
<template class="ContactsContactForm" parent="GtkGrid">
<property name="visible">True</property>
+ <property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="main_sw">
<property name="visible">True</property>
@@ -24,7 +25,20 @@
<property name="row_spacing">12</property>
<property name="column_spacing">16</property>
<property name="margin">36</property>
+ <property name="margin_top">30</property>
<property name="margin_bottom">24</property>
+ <child>
+ <object class="GtkListBox" id="form_container">
+ <property name="visible">True</property>
+ <property name="hexpand">True</property>
+ <property name="selection_mode">none</property>
+ </object>
+ <packing>
+ <property name="top_attach">1</property>
+ <property name="left_attach">0</property>
+ <property name="width">2</property>
+ </packing>
+ </child>
</object>
</child>
</object>
diff --git a/data/ui/style.css b/data/ui/style.css
index d77b446..6526c21 100644
--- a/data/ui/style.css
+++ b/data/ui/style.css
@@ -20,6 +20,10 @@ row.contact-data-row {
background-color: mix(@theme_bg_color, @theme_base_color, 0.4);
}
+.contacts-contact-form list {
+ background-color: rgba(0, 0, 0, 0);
+}
+
.contacts-suggestion {
border-top: 1px solid @borders;
background-color: shade(@theme_bg_color, 0.9);
diff --git a/src/contacts-contact-editor.vala b/src/contacts-contact-editor.vala
index 3398bf0..64e7c2b 100644
--- a/src/contacts-contact-editor.vala
+++ b/src/contacts-contact-editor.vala
@@ -19,46 +19,6 @@ using Gtk;
using Folks;
using Gee;
-public class Contacts.AddressEditor : Box {
- public Entry? entries[7]; /* must be the number of elements in postal_element_props */
- public PostalAddressFieldDetails details;
-
- public const string[] postal_element_props = {"street", "extension", "locality", "region", "postal_code", "po_box", "country"};
- public static string[] postal_element_names = {_("Street"), _("Extension"), _("City"), _("State/Province"), _("Zip/Postal Code"), _("PO box"), _("Country")};
-
- public signal void changed ();
-
- public AddressEditor (PostalAddressFieldDetails _details) {
- set_hexpand (true);
- set_orientation (Orientation.VERTICAL);
-
- details = _details;
-
- for (int i = 0; i < entries.length; i++) {
- string postal_part;
- details.value.get (AddressEditor.postal_element_props[i], out postal_part);
-
- entries[i] = new Entry ();
- entries[i].set_hexpand (true);
- entries[i].set ("placeholder-text", AddressEditor.postal_element_names[i]);
-
- if (postal_part != null)
- entries[i].set_text (postal_part);
-
- entries[i].get_style_context ().add_class ("contacts-postal-entry");
- add (entries[i]);
-
- entries[i].changed.connect (() => {
- changed ();
- });
- }
- }
-
- public override void grab_focus () {
- entries[0].grab_focus ();
- }
-}
-
/**
* A widget that allows the user to edit a given {@link Contact}.
*/
@@ -73,10 +33,6 @@ public class Contacts.ContactEditor : ContactForm {
private weak Widget focus_widget;
- private Entry name_entry;
-
- private Avatar avatar;
-
[GtkChild]
private MenuButton add_detail_button;
@@ -86,23 +42,6 @@ public class Contacts.ContactEditor : ContactForm {
[GtkChild]
public Button remove_button;
- public struct PropertyData {
- Persona? persona;
- Value value;
- }
-
- struct RowData {
- AbstractFieldDetails details;
- }
-
- struct Field {
- bool changed;
- HashMap<int, RowData?> rows;
- }
-
- /* 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;
}
@@ -116,7 +55,6 @@ public class Contacts.ContactEditor : ContactForm {
}
construct {
- this.writable_personas = new HashMap<string, HashMap<string, Field?>> ();
this.container_grid.size_allocate.connect(on_container_grid_size_allocate);
}
@@ -146,679 +84,65 @@ public class Contacts.ContactEditor : ContactForm {
}
private void fill_in_contact () {
- int i = 3;
- int last_store_position = 0;
- bool is_first_persona = true;
-
- var personas = this.contact.get_personas_for_display ();
- foreach (var p in personas) {
- if (!is_first_persona) {
- this.container_grid.attach (create_persona_store_label (p), 0, i, 2);
- last_store_position = ++i;
- }
-
- var rw_props = sort_persona_properties (p.writeable_properties);
- if (rw_props.length != 0) {
- this.writable_personas[p.uid] = new HashMap<string, Field?> ();
- foreach (var prop in rw_props)
- add_edit_row (p, prop, ref i);
- }
-
- if (is_first_persona)
- this.last_row = i - 1;
-
- if (i != 3)
- is_first_persona = false;
-
- if (i == last_store_position) {
- i--;
- this.container_grid.get_child_at (0, i).destroy ();
+ foreach (var p in this.contact.individual.personas) {
+ foreach (var prop_name in p.writeable_properties) {
+ var prop = add_edit_row (p, prop_name);
+ if (prop != null)
+ add_property (prop);
}
}
}
private void fill_in_empty () {
- this.last_row = 2;
-
- this.writable_personas["null-persona.hack"] = new HashMap<string, Field?> ();
- foreach (var prop in DEFAULT_PROPS_NEW_CONTACT) {
- var tok = prop.split (".");
- add_new_row_for_property (null, tok[0], tok[1].up ());
- }
-
- this.focus_widget = this.name_entry;
- }
-
- Value get_value_from_emails (HashMap<int, RowData?> rows) {
- var new_details = new HashSet<EmailFieldDetails>();
-
- foreach (var row_entry in rows.entries) {
- var combo = container_grid.get_child_at (0, row_entry.key) as TypeCombo;
- var entry = container_grid.get_child_at (1, row_entry.key) as Entry;
-
- /* Ignore empty entries. */
- if (entry.get_text () == "")
- continue;
-
- combo.active_descriptor.save_to_field_details (row_entry.value.details);
- var details = new EmailFieldDetails (entry.get_text (), row_entry.value.details.parameters);
- new_details.add (details);
- }
- var new_value = Value (new_details.get_type ());
- new_value.set_object (new_details);
-
- return new_value;
- }
-
- Value get_value_from_phones (HashMap<int, RowData?> rows) {
- var new_details = new HashSet<PhoneFieldDetails>();
-
- foreach (var row_entry in rows.entries) {
- var combo = container_grid.get_child_at (0, row_entry.key) as TypeCombo;
- var entry = container_grid.get_child_at (1, row_entry.key) as Entry;
-
- /* Ignore empty entries. */
- if (entry.get_text () == "")
- continue;
-
- combo.active_descriptor.save_to_field_details (row_entry.value.details);
- var details = new PhoneFieldDetails (entry.get_text (), row_entry.value.details.parameters);
- new_details.add (details);
- }
- var new_value = Value (new_details.get_type ());
- new_value.set_object (new_details);
- return new_value;
- }
-
- Value get_value_from_urls (HashMap<int, RowData?> rows) {
- var new_details = new HashSet<UrlFieldDetails>();
-
- foreach (var row_entry in rows.entries) {
- var entry = container_grid.get_child_at (1, row_entry.key) as Entry;
-
- /* Ignore empty entries. */
- if (entry.get_text () == "")
- continue;
-
- var details = new UrlFieldDetails (entry.get_text (), row_entry.value.details.parameters);
- new_details.add (details);
- }
- var new_value = Value (new_details.get_type ());
- new_value.set_object (new_details);
- return new_value;
- }
-
- Value get_value_from_nickname (HashMap<int, RowData?> rows) {
- var new_value = Value (typeof (string));
- foreach (var row_entry in rows.entries) {
- var entry = container_grid.get_child_at (1, row_entry.key) as Entry;
-
- /* Ignore empty entries. */
- if (entry.get_text () == "")
- continue;
-
- new_value.set_string (entry.get_text ());
+ foreach (var prop_name in DEFAULT_PROPS_NEW_CONTACT) {
+ var tok = prop_name.split (".");
+ var prop = add_edit_row (null, tok[0], true, tok[1].up ());
+ if (prop != null)
+ add_property (prop);
}
- 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;
+ this.focus_widget = this.name_widget;
}
- Value get_value_from_notes (HashMap<int, RowData?> rows) {
- var new_details = new HashSet<NoteFieldDetails>();
-
- foreach (var row_entry in rows.entries) {
- var text = (container_grid.get_child_at (1, row_entry.key) as Bin).get_child () as TextView;
- TextIter start, end;
- text.get_buffer ().get_start_iter (out start);
- text.get_buffer ().get_end_iter (out end);
- var value = text.get_buffer ().get_text (start, end, true);
- if (value != "") {
- var details = new NoteFieldDetails (value, row_entry.value.details.parameters);
- new_details.add (details);
- }
- }
- var new_value = Value (new_details.get_type ());
- new_value.set_object (new_details);
- return new_value;
- }
-
- Value get_value_from_addresses (HashMap<int, RowData?> rows) {
- var new_details = new HashSet<PostalAddressFieldDetails>();
-
- foreach (var row_entry in rows.entries) {
- var combo = container_grid.get_child_at (0, row_entry.key) as TypeCombo;
- var addr_editor = container_grid.get_child_at (1, row_entry.key) as AddressEditor;
- combo.active_descriptor.save_to_field_details (row_entry.value.details);
-
- var new_value = new PostalAddress (addr_editor.details.value.po_box,
- addr_editor.details.value.extension,
- addr_editor.details.value.street,
- addr_editor.details.value.locality,
- addr_editor.details.value.region,
- addr_editor.details.value.postal_code,
- addr_editor.details.value.country,
- addr_editor.details.value.address_format,
- addr_editor.details.id);
- for (int i = 0; i < addr_editor.entries.length; i++)
- new_value.set (AddressEditor.postal_element_props[i], addr_editor.entries[i].get_text ());
-
- var details = new PostalAddressFieldDetails(new_value, row_entry.value.details.parameters);
- new_details.add (details);
- }
- var new_value = Value (new_details.get_type ());
- new_value.set_object (new_details);
- return new_value;
- }
-
- void set_field_changed (int row) {
- foreach (var fields in writable_personas.values) {
- foreach (var entry in fields.entries) {
- if (row in entry.value.rows.keys) {
- if (entry.value.changed)
- return;
-
- entry.value.changed = true;
- return;
- }
- }
- }
- }
-
- new void remove_row (int row) {
- foreach (var fields in writable_personas.values) {
- foreach (var field_entry in fields.entries) {
- foreach (var idx in field_entry.value.rows.keys) {
- if (idx == row) {
- var child = container_grid.get_child_at (0, row);
- child.destroy ();
- child = container_grid.get_child_at (1, row);
- child.destroy ();
- child = container_grid.get_child_at (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_from_field_details (details);
- if (type != null)
- combo.set_active_from_vcard_type (type);
- combo.set_valign (Align.CENTER);
- container_grid.attach (combo, 0, row, 1, 1);
-
- var value_entry = new Entry ();
- value_entry.set_text (value);
- value_entry.set_hexpand (true);
- container_grid.attach (value_entry, 1, row, 1, 1);
-
- if (type_set == TypeSet.email) {
- value_entry.placeholder_text = _("Add email");
- } else if (type_set == TypeSet.phone) {
- value_entry.placeholder_text = _("Add number");
- }
-
- var delete_button = new Button.from_icon_name ("user-trash-symbolic", IconSize.MENU);
- delete_button.get_accessible ().set_name (_("Delete field"));
- container_grid.attach (delete_button, 3, row, 1, 1);
-
- /* Notify change to upper layer */
- combo.changed.connect ((c) => {
- set_field_changed (get_current_row (combo));
- });
- value_entry.changed.connect (() => {
- set_field_changed (get_current_row (value_entry));
- });
- delete_button.clicked.connect (() => {
- remove_row (get_current_row (delete_button));
- });
-
- if (value == "")
- focus_widget = value_entry;
- }
-
- void attach_row_with_entry_labeled (string title, AbstractFieldDetails? details, string value, int row) {
- var title_label = new Label (title);
- title_label.set_hexpand (false);
- title_label.set_halign (Align.START);
- title_label.margin_end = 6;
- container_grid.attach (title_label, 0, row, 1, 1);
-
- var value_entry = new Entry ();
- value_entry.set_text (value);
- value_entry.set_hexpand (true);
- container_grid.attach (value_entry, 1, row, 1, 1);
-
- var delete_button = new Button.from_icon_name ("user-trash-symbolic", IconSize.MENU);
- delete_button.get_accessible ().set_name (_("Delete field"));
- container_grid.attach (delete_button, 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_from_field_details (details);
- if (type != null)
- combo.set_active_from_vcard_type (type);
- container_grid.attach (combo, 0, row, 1, 1);
-
- var value_address = new AddressEditor (details);
- container_grid.attach (value_address, 1, row, 1, 1);
-
- var delete_button = new Button.from_icon_name ("user-trash-symbolic", IconSize.MENU);
- delete_button.get_accessible ().set_name (_("Delete field"));
- delete_button.set_valign (Align.START);
- container_grid.attach (delete_button, 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";
+ PersonaProperty? add_edit_row (Persona? p, string prop_name, bool add_empty = false, string? type = null) {
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 });
- }
- }
+ if (add_empty || EmailsProperty.should_show (p))
+ return new EditableEmailsProperty (p);
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 });
- }
- }
+ if (add_empty || PhoneNrsProperty.should_show (p))
+ return new EditablePhoneNrsProperty (p);
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 });
- }
- }
+ if (add_empty || UrlsProperty.should_show (p))
+ return new EditableUrlsProperty (p);
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 });
- }
- }
+ if (add_empty || NicknameProperty.should_show (p))
+ return new EditableNicknameProperty (p);
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 });
- }
+ if (add_empty || BirthdayProperty.should_show (p))
+ return new EditableBirthdayProperty (p);
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 });
- }
- }
+ if (add_empty || NotesProperty.should_show (p))
+ return new EditableNotesProperty (p);
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 });
- }
- }
+ if (add_empty || PostalAddressesProperty.should_show (p))
+ return new EditablePostalAddressesProperty (p);
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);
+ return null;
}
private void on_container_grid_size_allocate (Allocation alloc) {
@@ -828,97 +152,82 @@ public class Contacts.ContactEditor : ContactForm {
}
}
- 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);
- }
+ public async void save_changes () throws Error {
+ for (uint i = 0; i < this.props.get_n_items (); i++) {
+ var prop = this.props.get_item (i) as EditableProperty;
+ if (prop != null) {
+ yield prop.save_changes ();
+ debug ("Successfully saved property '%s'", prop.property_name);
}
}
- return props_set;
+ if (name_changed ()) {
+ var v = get_full_name_value ();
+ yield this.contact.set_individual_property ("full-name", v);
+ debug ("Successfully saved name");
+ /*XXX*/
+ /* display_name_changed (v.get_string ()); */
+ }
+
+ if (avatar_changed ()) {
+ var v = get_avatar_value ();
+ yield this.contact.set_individual_property ("avatar", v);
+ debug ("Successfully saved avatar");
+ }
}
- 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 (this.store, contact);
- writable_personas[persona.uid] = new HashMap<string, Field?> ();
- } else {
- persona = p;
- }
+ public HashTable<string, Value?> create_details_for_new_contact () {
+ var details = new HashTable<string, Value?> (str_hash, str_equal);
+
+ // Collect the details from the editor
+ if (name_changed ())
+ details["full-name"] = get_full_name_value ();
+
+ if (avatar_changed ())
+ details["avatar"] = get_avatar_value ();
+
+ for (uint i = 0; i < this.props.get_n_items (); i++) {
+ var prop = this.props.get_item (i) as EditableProperty;
+ if (prop != null)
+ details[prop.property_name] = prop.create_value ();
}
- 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;
- }
+ return details;
+ }
+
+ public void add_new_row_for_property (Persona? p, string prop_name, string? type = null) {
+ // First check if the prop doesn't exist already
+ var prop = get_field (p, prop_name);
+ debug ("Tryig to add prop for property: %s, existing? %p", prop_name, prop);
+
+ if (prop != null) {
+ // XXX check if we can add, or focus existing
+ return;
}
- 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 ();
+
+ prop = add_edit_row (p, prop_name, true, type);
+ if (prop != null)
+ add_property (prop);
}
// 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);
+ this.avatar_widget = new Avatar (PROFILE_SIZE, this.contact);
var button = new Button ();
button.get_accessible ().set_name (_("Change avatar"));
- button.image = this.avatar;
+ button.image = (Avatar) this.avatar_widget;
button.clicked.connect (on_avatar_button_clicked);
- this.container_grid.attach (button, 0, 0, 1, 3);
+ attach_avatar_widget (button);
}
// Show the avatar popover when the avatar is clicked
private void on_avatar_button_clicked (Button avatar_button) {
var popover = new AvatarSelector (avatar_button, this.contact);
popover.set_avatar.connect ( (icon) => {
- this.avatar.set_data ("value", icon);
- this.avatar.set_data ("changed", true);
+ this.avatar_widget.set_data ("value", icon);
+ this.avatar_widget.set_data ("changed", true);
Gdk.Pixbuf? a_pixbuf = null;
try {
@@ -927,17 +236,17 @@ public class Contacts.ContactEditor : ContactForm {
} catch {
}
- this.avatar.set_pixbuf (a_pixbuf);
+ ((Avatar) this.avatar_widget).set_pixbuf (a_pixbuf);
});
popover.show();
}
public bool avatar_changed () {
- return this.avatar.get_data<bool> ("changed");
+ return this.avatar_widget.get_data<bool> ("changed");
}
public Value get_avatar_value () {
- GLib.Icon icon = this.avatar.get_data<GLib.Icon> ("value");
+ GLib.Icon icon = this.avatar_widget.get_data<GLib.Icon> ("value");
Value v = Value (icon.get_type ());
v.set_object (icon);
return v;
@@ -945,30 +254,27 @@ public class Contacts.ContactEditor : ContactForm {
// 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);
+ var name_entry = new Entry ();
+ name_entry.placeholder_text = _("Add name");
+ name_entry.set_data ("changed", false);
+ set_name_widget (name_entry);
if (this.contact != null)
- this.name_entry.text = this.contact.individual.display_name;
+ name_entry.text = this.contact.individual.display_name;
/* structured name change */
- this.name_entry.changed.connect (() => {
- this.name_entry.set_data ("changed", true);
+ name_entry.changed.connect (() => {
+ 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");
+ return this.name_widget.get_data<bool> ("changed");
}
public Value get_full_name_value () {
Value v = Value (typeof (string));
- v.set_string (this.name_entry.get_text ());
+ v.set_string (((Entry) this.name_widget).text);
return v;
}
}
diff --git a/src/contacts-contact-form.vala b/src/contacts-contact-form.vala
index f44a2fb..3366e3e 100644
--- a/src/contacts-contact-form.vala
+++ b/src/contacts-contact-form.vala
@@ -49,42 +49,128 @@ public abstract class Contacts.ContactForm : Grid {
[GtkChild]
protected Grid container_grid;
- protected int last_row = 0;
+
+ [GtkChild]
+ protected ListBox form_container;
+ protected GLib.ListStore props = new GLib.ListStore (typeof (PersonaProperty));
+
+ protected SizeGroup labels_sizegroup = new SizeGroup (SizeGroupMode.HORIZONTAL);
+ protected SizeGroup values_sizegroup = new SizeGroup (SizeGroupMode.HORIZONTAL);
+ protected SizeGroup actions_sizegroup = new SizeGroup (SizeGroupMode.HORIZONTAL);
+
+ // Seperate treatment for the header widgets
+ protected Widget avatar_widget;
+ protected Widget name_widget;
construct {
this.container_grid.set_focus_vadjustment (this.main_sw.get_vadjustment ());
this.main_sw.get_style_context ().add_class ("contacts-contact-form");
+
+ this.form_container.bind_model (this.props, create_row);
+ this.form_container.set_header_func (create_persona_store_header);
+ }
+
+ private Gtk.Widget create_row (Object object) {
+ unowned PersonaProperty property = (PersonaProperty) object;
+ return property.create_row (this.labels_sizegroup, this.values_sizegroup, this.actions_sizegroup);
}
- protected string[] sort_persona_properties (string[] props) {
- CompareDataFunc<string> compare_properties = (a, b) => {
- foreach (var prop in SORTED_PROPERTIES) {
- if (a == prop)
- return (b == prop)? 0 : -1;
+ private void create_persona_store_header (ListBoxRow row, ListBoxRow? before) {
+ // Leave out the persona store header at the start
+ if (before == null) {
+ row.set_header (null);
+ return;
+ }
+
+ PropertyWidget current = (PropertyWidget) row;
+ PropertyWidget previous = (PropertyWidget) before;
+ if (current.prop.persona == null || previous.prop.persona == null)
+ return;
+ if (current.prop.persona == previous.prop.persona)
+ return;
+
+ var label = create_persona_store_label (current.prop.persona);
+ row.set_header (label);
+ }
+
+ private static int compare_persona_properties (Object obj_a, Object obj_b) {
+ unowned PersonaProperty a = (PersonaProperty) obj_a;
+ unowned PersonaProperty b = (PersonaProperty) obj_b;
+
+ // First compare personas
+ var persona_comparison = Utils.compare_personas_on_store (a.persona, b.persona);
+ if (persona_comparison != 0)
+ return persona_comparison;
+
+ // Then compare the properties by name (so that they are sorted as in SORTED_PROPERTIES
+ var property_name_comp = compare_property_names (a.property_name, b.property_name);
+ if (property_name_comp != 0)
+ return property_name_comp;
- if (b == prop)
- return 1;
- }
+ // Next, check for the VCard PREF attribute
+ // XXX TODO
+ return 0;
+ }
- return 0;
- };
+ private static int compare_property_names (string a, string b) {
+ foreach (unowned string prop in SORTED_PROPERTIES) {
+ if (a == prop)
+ return (b == prop)? 0 : -1;
- var sorted_props = new ArrayList<string> ();
- foreach (var s in props)
- sorted_props.add (s);
+ if (b == prop)
+ return 1;
+ }
- sorted_props.sort ((owned) compare_properties);
- return sorted_props.to_array ();
+ return 0;
}
protected Label create_persona_store_label (Persona p) {
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.halign = Align.START;
store_name.xalign = 0.0f;
store_name.margin_start = 6;
+ store_name.visible = true;
return store_name;
}
+
+ protected PersonaProperty? get_property_for_name (string name) {
+ for (uint i = 0; i < this.props.get_n_items(); i++) {
+ var prop = (PersonaProperty) this.props.get_item (i);
+ if (prop.property_name == name)
+ return prop;
+ }
+
+ return null;
+ }
+
+ protected void add_property (PersonaProperty property) {
+ this.props.insert_sorted (property, compare_persona_properties);
+ }
+
+ protected PersonaProperty? get_field (Persona? persona, string property_name) {
+ for (int i = 0; i < this.props.get_n_items(); i++) {
+ var prop = (PersonaProperty) this.props.get_item (i);
+ if (prop.persona == persona && prop.property_name == property_name)
+ return prop;
+ }
+
+ return null;
+ }
+
+ protected void attach_avatar_widget (Widget avatar_widget) {
+ avatar_widget.vexpand = false;
+ avatar_widget.halign = Align.START;
+ container_grid.attach (avatar_widget, 0, 0);
+ this.labels_sizegroup.add_widget (avatar_widget);
+ }
+
+ protected void set_name_widget (Widget name_widget) {
+ this.name_widget = name_widget;
+ this.name_widget.hexpand = true;
+ this.name_widget.valign = Align.CENTER;
+ this.container_grid.attach (this.name_widget, 1, 0);
+ }
}
diff --git a/src/contacts-contact-pane.vala b/src/contacts-contact-pane.vala
index 1fabeff..4be8e89 100644
--- a/src/contacts-contact-pane.vala
+++ b/src/contacts-contact-pane.vala
@@ -184,8 +184,8 @@ public class Contacts.ContactPane : Stack {
if (tok[0] == "add") {
editor.add_new_row_for_property (contact.find_primary_persona (),
- tok[1],
- tok.length > 2 ? tok[2].up () : null);
+ tok[1],
+ tok.length > 2 ? tok[2].up () : null);
}
}
@@ -221,10 +221,21 @@ public class Contacts.ContactPane : Stack {
if (!this.on_edit_mode)
return;
+ // Get a strong reference
+ ContactEditor editor = this.editor;
this.on_edit_mode = false;
/* saving changes */
- if (!drop_changes)
- save_editor_changes.begin ();
+ if (!drop_changes) {
+ warning ("Editor:saving changes");
+ editor.save_changes.begin ((obj, res) => {
+ try {
+ editor.save_changes.end (res);
+ } catch (Error e) {
+ critical(e.message); // XXX
+ show_message (e.message);
+ }
+ });
+ }
remove_contact_editor ();
@@ -234,35 +245,6 @@ public class Contacts.ContactPane : Stack {
set_visible_child (this.none_selected_page);
}
- private async void save_editor_changes () {
- foreach (var prop in this.editor.properties_changed ().entries) {
- try {
- yield Contact.set_persona_property (prop.value.persona, prop.key, prop.value.value);
- } catch (Error e) {
- show_message (e.message);
- }
- }
-
- if (this.editor.name_changed ()) {
- var v = this.editor.get_full_name_value ();
- try {
- yield this.contact.set_individual_property ("full-name", v);
- display_name_changed (v.get_string ());
- } catch (Error e) {
- show_message (e.message);
- }
- }
-
- if (this.editor.avatar_changed ()) {
- var v = this.editor.get_avatar_value ();
- try {
- yield this.contact.set_individual_property ("avatar", v);
- } catch (Error e) {
- show_message (e.message);
- }
- }
- }
-
public void new_contact () {
this.on_edit_mode = true;
this.contact = null;
@@ -273,17 +255,7 @@ public class Contacts.ContactPane : Stack {
// 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;
+ var details = this.editor.create_details_for_new_contact ();
// Leave edit mode
stop_editing (true);
diff --git a/src/contacts-contact-sheet.vala b/src/contacts-contact-sheet.vala
index 62f2e3f..cbdb5fd 100644
--- a/src/contacts-contact-sheet.vala
+++ b/src/contacts-contact-sheet.vala
@@ -27,103 +27,51 @@ using Gee;
public class Contacts.ContactSheet : ContactForm {
public ContactSheet (Contact contact, Store store) {
- this.contact = contact;
- this.store = store;
+ this.contact = contact;
+ this.store = store;
- this.contact.individual.notify.connect (update);
- this.contact.individual.personas_changed.connect (update);
- this.store.quiescent.connect (update);
+ this.contact.individual.notify.connect (update);
+ this.contact.individual.personas_changed.connect (update);
+ this.store.quiescent.connect (update);
- update ();
- }
-
- private Button add_row_with_button (string label, string value, bool use_link_button = false) {
- var type_label = new Label (label);
- type_label.xalign = 1.0f;
- type_label.set_halign (Align.END);
- type_label.get_style_context ().add_class ("dim-label");
- this.container_grid.attach (type_label, 0, this.last_row);
-
- var value_button = use_link_button? new LinkButton (value) : new Button.with_label (value);
- value_button.focus_on_click = false;
- value_button.relief = ReliefStyle.NONE;
- value_button.halign = Align.START;
- this.container_grid.attach (value_button, 1, this.last_row);
- this.last_row++;
-
- (value_button.get_child () as Label).set_ellipsize (Pango.EllipsizeMode.END);
- (value_button.get_child () as Label).wrap_mode = Pango.WrapMode.CHAR;
-
- return value_button;
- }
-
- void add_row_with_label (string label_value, string value) {
- var type_label = new Label (label_value);
- type_label.xalign = 1.0f;
- type_label.set_halign (Align.END);
- type_label.set_valign (Align.START);
- type_label.get_style_context ().add_class ("dim-label");
- this.container_grid.attach (type_label, 0, this.last_row, 1, 1);
-
- var value_label = new Label (value);
- value_label.set_line_wrap (true);
- value_label.xalign = 0.0f;
- value_label.set_halign (Align.START);
- value_label.set_ellipsize (Pango.EllipsizeMode.END);
- value_label.wrap_mode = Pango.WrapMode.CHAR;
- value_label.set_selectable (true);
-
- /* FIXME: hardcode gap to match the button size */
- type_label.margin_top = 3;
- value_label.margin_start = 6;
- value_label.margin_top = 3;
- value_label.margin_bottom = 3;
-
- this.container_grid.attach (value_label, 1, this.last_row, 1, 1);
- this.last_row++;
+ update ();
}
private void update () {
- this.last_row = 0;
- this.container_grid.foreach ((child) => this.container_grid.remove (child));
+ clear_previous_details ();
- var image_frame = new Avatar (PROFILE_SIZE, this.contact);
- image_frame.set_vexpand (false);
- image_frame.set_valign (Align.START);
- this.container_grid.attach (image_frame, 0, 0, 1, 3);
+ this.avatar_widget = new Avatar (PROFILE_SIZE, this.contact);
+ attach_avatar_widget (this.avatar_widget);
create_name_label ();
- this.last_row += 3; // Name/Avatar takes up 3 rows
-
- var personas = this.contact.get_personas_for_display ();
- /* Cause personas are sorted properly I can do this */
- foreach (var p in personas) {
- bool is_first_persona = (this.last_row == 3);
- int persona_store_pos = this.last_row;
- if (!is_first_persona) {
- this.container_grid.attach (create_persona_store_label (p), 0, this.last_row, 3);
- this.last_row++;
- }
-
- foreach (var prop in ContactForm.SORTED_PROPERTIES)
- add_row_for_property (p, prop);
-
- // Nothing to show in the persona: don't mention it
- bool is_empty_persona = (this.last_row == persona_store_pos + 1);
- if (!is_first_persona && is_empty_persona) {
- this.container_grid.remove_row (persona_store_pos);
- this.last_row--;
+ foreach (var p in this.contact.individual.personas) {
+ foreach (var prop_name in ContactForm.SORTED_PROPERTIES) {
+ var prop = add_row_for_property (p, prop_name);
+ if (prop != null)
+ add_property (prop);
}
}
show_all ();
}
- private void update_name_label (Gtk.Label name_label) {
+ private void clear_previous_details () {
+ if (this.avatar_widget != null)
+ this.avatar_widget.destroy ();
+ this.avatar_widget = null;
+
+ if (this.name_widget != null)
+ this.name_widget.destroy ();
+ this.name_widget = null;
+
+ this.props.remove_all ();
+ }
+
+ private void update_name_widget () {
var name = Markup.printf_escaped ("<span font='16'>%s</span>",
this.contact.individual.display_name);
- name_label.set_markup (name);
+ ((Label) this.name_widget).set_markup (name);
}
private void create_name_label () {
@@ -131,139 +79,49 @@ public class Contacts.ContactSheet : ContactForm {
name_label.ellipsize = Pango.EllipsizeMode.END;
name_label.xalign = 0f;
name_label.selectable = true;
- this.container_grid.attach (name_label, 1, 0, 1, 3);
- update_name_label (name_label);
+ set_name_widget (name_label);
+
+ update_name_widget ();
this.contact.individual.notify["display-name"].connect ((obj, spec) => {
- update_name_label (name_label);
+ update_name_widget ();
});
}
- private void add_row_for_property (Persona persona, string property) {
- switch (property) {
+ private PersonaProperty? add_row_for_property (Persona persona, string property_name) {
+ switch (property_name) {
case "email-addresses":
- add_emails (persona);
+ if (EmailsProperty.should_show (persona))
+ return new EmailsProperty (persona);
break;
case "phone-numbers":
- add_phone_nrs (persona);
- break;
- case "im-addresses":
- add_im_addresses (persona);
+ if (PhoneNrsProperty.should_show (persona))
+ return new PhoneNrsProperty (persona);
break;
case "urls":
- add_urls (persona);
+ if (UrlsProperty.should_show (persona))
+ return new UrlsProperty (persona);
break;
case "nickname":
- add_nickname (persona);
+ if (NicknameProperty.should_show (persona))
+ return new NicknameProperty (persona);
break;
case "birthday":
- add_birthday (persona);
+ if (BirthdayProperty.should_show (persona))
+ return new BirthdayProperty (persona);
break;
case "notes":
- add_notes (persona);
+ if (NotesProperty.should_show (persona))
+ return new NotesProperty (persona);
break;
case "postal-addresses":
- add_postal_addresses (persona);
+ if (PostalAddressesProperty.should_show (persona))
+ return new PostalAddressesProperty (persona);
break;
default:
- debug ("Unsupported property: %s", property);
+ debug ("Unsupported property: %s", property_name);
break;
}
- }
- private void add_emails (Persona persona) {
- var details = persona as EmailDetails;
- if (details != null) {
- var emails = Contact.sort_fields<EmailFieldDetails>(details.email_addresses);
- foreach (var email in emails) {
- var button = add_row_with_button (TypeSet.email.format_type (email), email.value);
- button.clicked.connect (() => {
- Utils.compose_mail ("%s <%s>".printf(this.contact.individual.display_name, email.value));
- });
- }
- }
- }
-
- private void add_phone_nrs (Persona persona) {
- var phone_details = persona as PhoneDetails;
- if (phone_details != null) {
- var phones = Contact.sort_fields<PhoneFieldDetails>(phone_details.phone_numbers);
- foreach (var phone in phones) {
-#if HAVE_TELEPATHY
- if (this.store.caller_account != null) {
- var button = add_row_with_button (TypeSet.phone.format_type (phone), phone.value);
- button.clicked.connect (() => {
- Utils.start_call (phone.value, this.store.caller_account);
- });
- } else {
- add_row_with_label (TypeSet.phone.format_type (phone), phone.value);
- }
-#else
- add_row_with_label (TypeSet.phone.format_type (phone), phone.value);
-#endif
- }
- }
- }
-
- private void add_im_addresses (Persona persona) {
-#if HAVE_TELEPATHY
- var im_details = persona as ImDetails;
- if (im_details != null) {
- foreach (var protocol in im_details.im_addresses.get_keys ()) {
- foreach (var id in im_details.im_addresses[protocol]) {
- if (persona is Tpf.Persona) {
- var button = add_row_with_button (ImService.get_display_name (protocol), id.value);
- button.clicked.connect (() => {
- var im_persona = this.contact.find_im_persona (protocol, id.value);
- if (im_persona != null) {
- var type = im_persona.presence_type;
- if (type != PresenceType.UNSET && type != PresenceType.ERROR &&
- type != PresenceType.OFFLINE && type != PresenceType.UNKNOWN) {
- Utils.start_chat (this.contact, protocol, id.value);
- }
- }
- });
- }
- }
- }
- }
-#endif
- }
-
- private void add_urls (Persona persona) {
- var url_details = persona as UrlDetails;
- if (url_details != null) {
- foreach (var url in url_details.urls)
- add_row_with_button (_("Website"), url.value, true);
- }
- }
-
- private void add_nickname (Persona persona) {
- var name_details = persona as NameDetails;
- if (name_details != null && is_set (name_details.nickname))
- add_row_with_label (_("Nickname"), name_details.nickname);
- }
-
- private void add_birthday (Persona persona) {
- var birthday_details = persona as BirthdayDetails;
- if (birthday_details != null && birthday_details.birthday != null)
- add_row_with_label (_("Birthday"), birthday_details.birthday.to_local ().format ("%x"));
- }
-
- private void add_notes (Persona persona) {
- var note_details = persona as NoteDetails;
- if (note_details != null) {
- foreach (var note in note_details.notes)
- add_row_with_label (_("Note"), note.value);
- }
- }
-
- private void add_postal_addresses (Persona persona) {
- var addr_details = persona as PostalAddressDetails;
- if (addr_details != null) {
- foreach (var addr in addr_details.postal_addresses) {
- var all_strs = string.joinv ("\n", Contact.format_address (addr.value));
- add_row_with_label (TypeSet.general.format_type (addr), all_strs);
- }
- }
+ return null;
}
}
diff --git a/src/contacts-contact.vala b/src/contacts-contact.vala
index f0cc442..35056f5 100644
--- a/src/contacts-contact.vala
+++ b/src/contacts-contact.vala
@@ -82,7 +82,7 @@ public class Contacts.Contact : GLib.Object {
return false;
// Mark google contacts not in "My Contacts" as non-main
- return !persona_is_google_other (persona);
+ return !Utils.persona_is_google_other (persona);
}
private bool calc_is_main () {
@@ -134,89 +134,6 @@ public class Contacts.Contact : GLib.Object {
return false;
}
- private static bool has_pref (AbstractFieldDetails details) {
- var evolution_pref = details.get_parameter_values ("x-evolution-ui-slot");
- if (evolution_pref != null && Utils.get_first (evolution_pref) == "1")
- return true;
-
- foreach (var param in details.parameters["type"]) {
- if (param.ascii_casecmp ("PREF") == 0)
- return true;
- }
- return false;
- }
-
- private static TypeSet select_typeset_from_fielddetails (AbstractFieldDetails a) {
- if (a is EmailFieldDetails)
- return TypeSet.email;
- if (a is PhoneFieldDetails)
- return TypeSet.phone;
- return TypeSet.general;
- }
-
- public static int compare_fields (void* _a, void* _b) {
- var a = (AbstractFieldDetails) _a;
- var b = (AbstractFieldDetails) _b;
-
- // Fields with a PREF hint always go first (see VCard PREF attribute)
- var a_has_pref = has_pref (a);
- if (a_has_pref != has_pref (b))
- return (a_has_pref)? -1 : 1;
-
- // sort by field type first (e.g. "Home", "Work")
- var type_set = select_typeset_from_fielddetails (a);
- var result = type_set.format_type (a).ascii_casecmp (type_set.format_type (b));
- if (result != 0)
- return result;
-
- // Try to compare by value if types are equal
- var aa = a as AbstractFieldDetails<string>;
- var bb = b as AbstractFieldDetails<string>;
- if (aa != null && bb != null)
- return strcmp (aa.value, bb.value);
-
- // No heuristics to fall back to.
- warning ("Unsupported AbstractFieldDetails value type");
- return 0;
- }
-
- public static Gee.List<T> sort_fields<T> (Collection<T> fields) {
- var res = new ArrayList<T>();
- res.add_all (fields);
- res.sort (Contact.compare_fields);
- return res;
- }
-
- public static string[] format_address (PostalAddress addr) {
- string[] lines = {};
-
- if (is_set (addr.street))
- lines += addr.street;
-
- if (is_set (addr.extension))
- lines += addr.extension;
-
- if (is_set (addr.locality))
- lines += addr.locality;
-
- if (is_set (addr.region))
- lines += addr.region;
-
- if (is_set (addr.postal_code))
- lines += addr.postal_code;
-
- if (is_set (addr.po_box))
- lines += addr.po_box;
-
- if (is_set (addr.country))
- lines += addr.country;
-
- if (is_set (addr.address_format))
- lines += addr.address_format;
-
- return lines;
- }
-
#if HAVE_TELEPATHY
public Tpf.Persona? find_im_persona (string protocol, string im_address) {
var iid = protocol + ":" + im_address;
@@ -286,38 +203,12 @@ public class Contacts.Contact : GLib.Object {
}
public Gee.List<Persona> get_personas_for_display () {
- CompareDataFunc<Persona> compare_persona_by_store = (a, b) => {
- var store_a = a.store;
- var store_b = b.store;
-
- // In the same store, sort Google 'other' contacts last
- if (store_a == store_b) {
- if (!persona_is_google (a))
- return 0;
-
- var a_is_other = persona_is_google_other (a);
- if (a_is_other != persona_is_google_other (b))
- return a_is_other? 1 : -1;
- }
-
- // Sort primary stores before others
- if (store_a.is_primary_store != store_b.is_primary_store)
- return (store_a.is_primary_store)? -1 : 1;
-
- // E-D-S stores get prioritized
- if ((store_a.type_id == "eds") != (store_b.type_id == "eds"))
- return (store_a.type_id == "eds")? -1 : 1;
-
- // Normal case: use alphabetical sorting
- return strcmp (store_a.id, store_b.id);
- };
-
var persona_list = new ArrayList<Persona>();
foreach (var persona in individual.personas)
if (persona.store.type_id != "key-file")
persona_list.add (persona);
- persona_list.sort ((owned) compare_persona_by_store);
+ persona_list.sort (Utils.compare_personas_on_store);
return persona_list;
}
@@ -373,7 +264,7 @@ public class Contacts.Contact : GLib.Object {
public bool has_mainable_persona () {
foreach (var p in individual.personas) {
if (p.store.type_id == "eds" &&
- !persona_is_google_other (p))
+ !Utils.persona_is_google_other (p))
return true;
}
return false;
@@ -385,8 +276,8 @@ public class Contacts.Contact : GLib.Object {
bool all_unlinkable = true;
foreach (var p in individual.personas) {
- if (!persona_is_google_other (p) ||
- persona_is_google_profile (p))
+ if (!Utils.persona_is_google_other (p) ||
+ Utils.persona_is_google_profile (p))
all_unlinkable = false;
}
@@ -405,42 +296,12 @@ public class Contacts.Contact : GLib.Object {
return !this.is_main || !other.has_mainable_persona();
}
- private static bool persona_is_google (Persona persona) {
- return persona.store.type_id == "eds" && esource_uid_is_google (persona.store.id);
- }
-
- /**
- * Return true only for personas which are in a Google address book, but which
- * are not in the user's "My Contacts" group in the address book.
- */
- public static bool persona_is_google_other (Persona persona) {
- if (!persona_is_google (persona))
- return false;
-
- var p = persona as Edsf.Persona;
- return p != null && !p.in_google_personal_group;
- }
-
- public static bool persona_is_google_profile (Persona persona) {
- if (!persona_is_google_other (persona))
- return false;
-
- var u = persona as UrlDetails;
- if (u != null && u.urls.size == 1) {
- foreach (var url in u.urls) {
- if (/https?:\/\/www.google.com\/profiles\/[0-9]+$/.match(url.value))
- return true;
- }
- }
- return false;
- }
-
public static string format_persona_store_name_for_contact (Persona persona) {
var store = persona.store;
if (store.type_id == "eds") {
- if (persona_is_google_profile (persona))
+ if (Utils.persona_is_google_profile (persona))
return _("Google Circles");
- else if (persona_is_google_other (persona))
+ else if (Utils.persona_is_google_other (persona))
return _("Google");
string? eds_name = lookup_esource_name_by_uid_for_contact (store.id);
diff --git a/src/contacts-persona-property.vala b/src/contacts-persona-property.vala
new file mode 100644
index 0000000..3630149
--- /dev/null
+++ b/src/contacts-persona-property.vala
@@ -0,0 +1,1106 @@
+/*
+ * Copyright (C) 2018 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 Gtk;
+using Folks;
+using Gee;
+
+/**
+ * A PersonaProperty is an abstraction of the property of a {@link Folks.Persona}.
+ *
+ * Since the contents of a property often isn't allowed to contain invalid
+ * information -such as an empty value-, a PersonaProperty allows us to deal
+ * with missing or invalid data as the user is inputting it to the UI.
+ *
+ * It also gives you a convenient APIT to show the property in the UI, using
+ * the create_row() method, which works well with a ListBox and an underlying
+ * GListModel (for example in a {@link Contacts.ContactForm}).
+ */
+public abstract class Contacts.PersonaProperty : Object {
+
+ /**
+ * The {@link Folks.Persona} this property belongs to.
+ */
+ public Persona? persona { get; construct set; default = null; }
+
+ /**
+ * The canonical name of this property, as used by libfolks.
+ * Note that this often maps to a lower-case version of the VCard property.
+ */
+ public string property_name { get; construct set; }
+
+ /**
+ * Specifies that the property is filled, i.e. that no extra information can be added anymore.
+ * For example, a Persona can only have a single birthday.
+ */
+ public abstract bool filled { get; }
+
+ /**
+ * Creates a widget that can show the property inside a {@link Gtk.ListBox}.
+ */
+ public ListBoxRow create_row (SizeGroup label_group,
+ SizeGroup value_group,
+ SizeGroup actions_group) {
+ var row = new PropertyWidget (this, label_group, value_group, actions_group);
+ row.margin_top = 18;
+ row.hexpand = true;
+
+ // The subclass is responsible for making the appropriate widgets
+ create_widgets (row);
+
+ row.show_all ();
+
+ return row;
+ }
+
+ /**
+ * Each subclass is responsible for creating the necessary UI and
+ * incorporating it into the given {@link PropertyWidget}.
+ */
+ protected abstract void create_widgets (PropertyWidget prop_widget);
+}
+
+public abstract class Contacts.AggregatedPersonaProperty : PersonaProperty {
+
+ //XXX
+ /* protected ListModel elements; */
+
+ // By default, one can always add as many elements to this property as possible
+ public override bool filled { get { return false; } }
+
+ /**
+ * The number of elements in this property. For example, this will be 2 for 2 email addresses.
+ */
+ public abstract int n_elements { get; }
+}
+
+public interface Contacts.EditableProperty : PersonaProperty {
+
+ /* public abstract void add_empty (string? hint); */
+
+ /**
+ * Creates a new {@link GLib.Value} from the content of this property.
+ * This method is used when a new contact is created.
+ */
+ public abstract Value? create_value ();
+
+ /**
+ * Saves the content of this property to the {@link Folks.Persona}. Note that
+ * it is a programmer error to call this when `this.persona == null`.
+ *
+ * XXX TODO FIXME: this will time out and fail in Edsf personas if the property didn't change.
+ * Either we need to fix this in folks or make *absolutely* sure the values changed
+ */
+ public abstract async void save_changes () throws PropertyError;
+
+ protected bool check_if_equal (Collection<AbstractFieldDetails> old_field_details,
+ Collection<AbstractFieldDetails> new_field_details) {
+ // Compare FieldDetails (maybe use equal_static? using a Set)
+ foreach (var old_field_detail in old_field_details) {
+ bool got_match = false;
+ foreach (var new_field_detail in new_field_details) {
+ // Check if the values are equal
+ if (!old_field_detail.values_equal (new_field_detail))
+ continue;
+
+ // We can't use AbstractFieldDetails.parameters_equal here,
+ // since custom labels should be compared case-sensitive, while standard
+ // ones shouldn't really.
+
+ // Only compare the fields we know about => at this point only the
+ // type-related ones
+ if (!TypeDescriptor.check_type_parameters_equal (old_field_detail.parameters,
+ new_field_detail.parameters))
+ continue;
+
+ got_match = true;
+ }
+
+ if (!got_match)
+ return false;
+ }
+
+ return true;
+ }
+}
+
+/**
+ * Represents a way of showing a given {@link Contacts.PersonaProperty} in a
+ * {@link Gtk.ListBox}, such as one would find in a ContactForm.
+ */
+public class Contacts.PropertyWidget : ListBoxRow {
+
+ private Grid grid = new Grid ();
+
+ private unowned SizeGroup labels_group;
+ private unowned SizeGroup values_group;
+ private unowned SizeGroup actions_group;
+
+// The parent prop
+/// XXX maybe only store the persona?
+ public weak PersonaProperty prop { get; construct set; }
+
+ construct {
+ this.selectable = false;
+ this.activatable = false;
+
+ this.grid.column_spacing = 12;
+ this.grid.row_spacing = 18;
+ this.grid.hexpand = true;
+ add (this.grid);
+ }
+
+ public PropertyWidget (PersonaProperty parent, SizeGroup labels, SizeGroup values, SizeGroup actions) {
+ Object (prop: parent);
+
+ this.labels_group = labels;
+ this.values_group = values;
+ this.actions_group = actions;
+ }
+
+ // Get the latest row number. This might have changed due to e.g. deletion of some row
+ private int get_last_row_nr () {
+ int last_row = -1;
+ foreach (var child in this.grid.get_children ())
+ last_row = int.max (last_row, get_child_row (child));
+
+ return last_row;
+ }
+
+ // Returns the top-attach child property or -1 if not a child
+ public int get_child_row (Widget child) {
+ int top_attach = -1;
+ this.grid.child_get (child, "top-attach", out top_attach);
+ return top_attach;
+ }
+
+ public void add_row (Widget label, Widget value, Widget? actions = null) {
+ int row_nr = get_last_row_nr () + 1;
+ this.grid.attach (label, 0, row_nr);
+ this.grid.attach (value, 1, row_nr);
+ if (actions != null)
+ this.grid.attach (actions, 2, row_nr);
+
+ this.labels_group.add_widget (label);
+ this.values_group.add_widget (value);
+ if (actions != null)
+ this.actions_group.add_widget (actions);
+ }
+
+ // Buidler
+ // Up next are some
+ public Label create_type_label (string? text) {
+ var label = new Label (text ?? "");
+
+ label.xalign = 1.0f;
+ label.halign = Align.END;
+ label.valign = Align.START;
+ label.get_style_context ().add_class ("dim-label");
+
+ return label;
+ }
+
+ public TypeCombo create_type_combo (TypeSet typeset, TypeDescriptor initial_type) {
+ var combo = new TypeCombo (typeset);
+ combo.active_descriptor = initial_type;
+ combo.valign = Align.START;
+ return combo;
+ }
+
+ public Label create_value_label (string? text, bool use_markup = false) {
+ var label = new Label (text ?? "");
+ label.use_markup = use_markup;
+ label.set_line_wrap (true);
+ label.xalign = 0.0f;
+ label.set_halign (Align.START);
+ label.set_ellipsize (Pango.EllipsizeMode.END);
+ label.wrap_mode = Pango.WrapMode.CHAR;
+ label.set_selectable (true);
+
+ return label;
+ }
+
+ public Label create_value_link (string text, string url) {
+ var link = "<a href=\"%s\">%s</a>".printf (url, text);
+ return create_value_label (link, true);
+ }
+
+ public Entry create_value_entry (string? text) {
+ var value_entry = new Entry ();
+ value_entry.text = text;
+ value_entry.hexpand = true;
+
+ return value_entry;
+ }
+
+ public Widget create_value_textview (string? text) {
+ var sw = new ScrolledWindow (null, null);
+ sw.shadow_type = ShadowType.OUT;
+ sw.set_size_request (-1, 100);
+
+ var value_text = new TextView ();
+ value_text.buffer.text = text;
+ value_text.hexpand = true;
+ sw.add (value_text);
+
+ return sw;
+ }
+
+ public Button create_delete_button (string? description) {
+ var delete_button = new Button.from_icon_name ("user-trash-symbolic", IconSize.MENU);
+ delete_button.valign = Align.START;
+ delete_button.get_accessible ().set_name (description);
+ delete_button.clicked.connect ((button) => {
+ int top_attach;
+ this.grid.child_get (delete_button, "top-attach", out top_attach);
+ this.grid.remove_row (top_attach);
+ });
+ return delete_button;
+ }
+}
+
+public class Contacts.NicknameProperty : PersonaProperty {
+
+ protected string nickname = "";
+
+ public override bool filled { get { return this.nickname != ""; } }
+
+ public NicknameProperty (Persona? persona) {
+ Object (
+ property_name: "nickname",
+ persona: persona
+ );
+
+ if (persona != null)
+ this.nickname = ((NameDetails) persona).nickname;
+ }
+
+ public static bool should_show (Persona persona) {
+ unowned NameDetails? details = persona as NameDetails;
+ return (details != null && details.nickname != "");
+ }
+
+ protected override void create_widgets (PropertyWidget prop_widget) {
+ var type_label = prop_widget.create_type_label (_("Nickname"));
+ var value_label = prop_widget.create_value_label (this.nickname);
+ prop_widget.add_row (type_label, value_label);
+ }
+}
+
+public class Contacts.EditableNicknameProperty : NicknameProperty, EditableProperty {
+
+ private bool deleted { get; set; default = false; }
+
+ public override bool filled { get { return base.filled && !this.deleted; } }
+
+ public EditableNicknameProperty (Persona? persona) {
+ base (persona);
+ }
+
+ protected override void create_widgets (PropertyWidget prop_widget) {
+ var type_label = prop_widget.create_type_label (_("Nickname"));
+ var nickname_entry = prop_widget.create_value_entry (this.nickname);
+ nickname_entry.changed.connect ((editable) => {
+ this.nickname = editable.get_chars ();
+ });
+
+ var delete_button = prop_widget.create_delete_button (_("Remove nickname"));
+ delete_button.clicked.connect ((b) => { this.deleted = true; });
+ prop_widget.add_row (type_label, nickname_entry, delete_button);
+ }
+
+ public Value? create_value () {
+ if (this.nickname == "")
+ return null;
+
+ var new_value = Value (typeof (string));
+ new_value.set_string (this.nickname);
+ return new_value;
+ }
+
+ public async void save_changes () throws PropertyError {
+ assert (this.persona != null);
+
+ if (this.deleted) {
+ yield ((NameDetails) this.persona).change_nickname ("");
+ return;
+ }
+
+ if (this.nickname == ((NameDetails) this.persona).nickname)
+ return;
+
+ yield ((NameDetails) this.persona).change_nickname (this.nickname);
+ }
+}
+
+public class Contacts.BirthdayProperty : PersonaProperty {
+
+ // In local timezone
+ protected DateTime birthday = new DateTime.now_local ();
+
+ // this.birthday is never null, so it is always filled
+ public override bool filled { get { return true; } }
+
+ public BirthdayProperty (Persona? persona) {
+ Object (
+ property_name: "birthday",
+ persona: persona
+ );
+
+ if (persona != null) {
+ unowned BirthdayDetails details = (BirthdayDetails) persona;
+ this.birthday = details.birthday.to_local ();
+ }
+ }
+
+ public static bool should_show (Persona persona) {
+ unowned BirthdayDetails? details = persona as BirthdayDetails;
+ return (details != null && details.birthday != null);
+ }
+
+ protected override void create_widgets (PropertyWidget prop_widget) {
+ var type_label = prop_widget.create_type_label (_("Birthday"));
+ var value_label = prop_widget.create_value_label (this.birthday.format ("%x"));
+ prop_widget.add_row (type_label, value_label);
+ }
+}
+
+public class Contacts.EditableBirthdayProperty : BirthdayProperty, EditableProperty {
+
+ private bool deleted { get; set; default = false; }
+
+ public override bool filled { get { return !this.deleted; } }
+
+ public EditableBirthdayProperty (Persona? persona) {
+ base (persona);
+ }
+
+ protected override void create_widgets (PropertyWidget prop_widget) {
+ var type_label = prop_widget.create_type_label (_("Birthday"));
+ var birthday_entry = create_date_widget ();
+ var delete_button = prop_widget.create_delete_button (_("Remove birthday"));
+ delete_button.clicked.connect ((b) => { this.deleted = true; });
+ prop_widget.add_row (type_label, birthday_entry, delete_button);
+ }
+
+ private Widget create_date_widget () {
+ var box = new Grid ();
+ box.column_spacing = 12;
+
+ // Day
+ 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 (this.birthday.get_day_of_month ());
+ box.add (day_spin);
+
+ // 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 (this.birthday.get_month () - 1);
+ month_combo.hexpand = true;
+ box.add (month_combo);
+
+ // Year
+ var year_spin = new SpinButton.with_range (1800, 3000, 1);
+ year_spin.set_digits (0);
+ year_spin.numeric = true;
+ year_spin.set_value (this.birthday.get_year ());
+ box.add (year_spin);
+
+ // We can't set the day/month/year directly, so calculate the diff and add that
+ day_spin.changed.connect (() => {
+ var diff = day_spin.get_value_as_int () - this.birthday.get_day_of_month ();
+ this.birthday = this.birthday.add_days (diff);
+ });
+ month_combo.changed.connect (() => {
+ adjust_date_range (year_spin, month_combo, day_spin);
+
+ var diff = (month_combo.get_active () + 1) - this.birthday.get_month ();
+ this.birthday = this.birthday.add_months (diff);
+ });
+ year_spin.changed.connect (() => {
+ adjust_date_range (year_spin, month_combo, day_spin);
+
+ var diff = year_spin.get_value_as_int () - this.birthday.get_year ();
+ this.birthday = this.birthday.add_years (diff);
+ });
+
+ return box;
+ }
+
+ // Make sure our user can't make an invalid date (e.g. February 31)
+ private void adjust_date_range (SpinButton year_spin, ComboBoxText month_combo, SpinButton day_spin) {
+ const 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) {
+ var year = (DateYear) year_spin.get_value_as_int ();
+ var nr_days = year.is_leap_year ()? 29 : 28;
+ day_spin.set_range (1, nr_days);
+ }
+ }
+
+ public Value? create_value () {
+ // Check if it got deleted
+ if (this.birthday == null)
+ return null;
+
+ var new_value = Value (typeof (DateTime));
+ new_value.set_boxed (this.birthday.to_utc ());
+ return new_value;
+ }
+
+ public async void save_changes () throws PropertyError {
+ assert (this.persona != null);
+
+ // The birthday property got deleted
+ if (this.birthday == null) {
+ yield ((BirthdayDetails) this.persona).change_birthday (null);
+ return;
+ }
+
+ var new_birthday = this.birthday.to_utc ();
+ if (new_birthday == ((BirthdayDetails) this.persona).birthday)
+ return;
+
+ yield ((BirthdayDetails) this.persona).change_birthday (new_birthday);
+ }
+}
+
+public class Contacts.PhoneNrsProperty : AggregatedPersonaProperty {
+
+ protected class PhoneNr : Object {
+ public TypeDescriptor type_descr { get; set; }
+ public string number { get; set; default = ""; }
+ public MultiMap<string, string>? parameters { get; set; default = null; }
+ public bool deleted { get; set; default = false; }
+
+ public PhoneNr.dummy (string type_str) {
+ Object (type_descr: TypeSet.phone.lookup_descriptor_in_store (type_str));
+ }
+
+ public PhoneNr (PhoneFieldDetails details) {
+ Object (type_descr: TypeSet.phone.lookup_descriptor_for_field_details (details),
+ number: details.value,
+ parameters: details.parameters);
+ }
+ }
+
+ protected Gee.List<PhoneNr?> phone_nrs = new ArrayList<PhoneNr?> ();
+
+ public override int n_elements { get { return this.phone_nrs.size; } }
+
+ public PhoneNrsProperty (Persona persona) {
+ Object (
+ property_name: "phone-numbers",
+ persona: persona
+ );
+
+ if (this.persona != null) {
+ foreach (var phone in ((PhoneDetails?) persona).phone_numbers) {
+ this.phone_nrs.add (new PhoneNr (phone));
+ }
+ }
+ }
+
+ public static bool should_show (Persona persona) {
+ unowned PhoneDetails? details = persona as PhoneDetails;
+ return (details != null && !details.phone_numbers.is_empty);
+ }
+
+ protected override void create_widgets (PropertyWidget prop_widget) {
+ foreach (PhoneNr phone_nr in this.phone_nrs)
+ add_field (prop_widget, phone_nr);
+ }
+
+ protected virtual void add_field (PropertyWidget prop_widget, PhoneNr phone_nr) {
+ var type_label = prop_widget.create_type_label (phone_nr.type_descr.display_name);
+ var value_label = prop_widget.create_value_label (phone_nr.number);
+ prop_widget.add_row (type_label, value_label);
+ }
+}
+
+public class Contacts.EditablePhoneNrsProperty : PhoneNrsProperty, EditableProperty {
+
+ public EditablePhoneNrsProperty (Persona? persona) {
+ base (persona);
+
+ if (persona == null) {
+ // Fill in a dummy value
+ this.phone_nrs.add (new PhoneNrsProperty.PhoneNr.dummy ("Mobile"));
+ }
+ }
+
+ protected override void add_field (PropertyWidget prop_widget, PhoneNrsProperty.PhoneNr phone_nr) {
+ var type_combo = prop_widget.create_type_combo (TypeSet.phone, phone_nr.type_descr);
+ type_combo.changed.connect ((combo) => {
+ phone_nr.type_descr = type_combo.active_descriptor;
+ });
+
+ var entry = prop_widget.create_value_entry (phone_nr.number);
+ entry.changed.connect ((editable) => {
+ phone_nr.number = editable.get_chars ();
+ });
+
+ var delete_button = prop_widget.create_delete_button (_("Remove phone number"));
+ delete_button.clicked.connect ((b) => {
+ phone_nr.deleted = true;
+ });
+
+ prop_widget.add_row (type_combo, entry, delete_button);
+ }
+
+ public Value? create_value () {
+ if (this.phone_nrs.is_empty)
+ return null;
+
+ var new_details = create_new_field_details ();
+
+ // Check if we only had empty phone_nrs
+ if (new_details.is_empty)
+ return null;
+
+ var result = Value (new_details.get_type ());
+ result.set_object (new_details);
+ return result;
+ }
+
+ public async void save_changes () throws PropertyError {
+ assert (this.persona != null);
+
+ var new_phone_nrs = create_new_field_details ();
+
+ // Check if we didn't have any changes. This is a necessary step
+ // XXX explain why (timeout)
+ var old_phone_nrs = ((PhoneDetails) this.persona).phone_numbers;
+ if (!check_if_equal (old_phone_nrs, new_phone_nrs))
+ yield ((PhoneDetails) this.persona).change_phone_numbers (new_phone_nrs);
+ }
+
+ private HashSet<PhoneFieldDetails>? create_new_field_details () {
+ var new_details = new HashSet<PhoneFieldDetails> ();
+ foreach (PhoneNrsProperty.PhoneNr phone_nr in this.phone_nrs) {
+ if (phone_nr.number == "" || phone_nr.deleted)
+ continue;
+
+ var parameters = phone_nr.type_descr.add_type_to_parameters (phone_nr.parameters);
+ var phone = new PhoneFieldDetails (phone_nr.number, parameters);
+ new_details.add (phone);
+ }
+
+ return new_details;
+ }
+}
+
+public class Contacts.EmailsProperty : AggregatedPersonaProperty {
+
+ protected class Email : Object {
+ public TypeDescriptor type_descr { get; set; }
+ public string address { get; set; default = ""; }
+ public MultiMap<string, string>? parameters { get; set; default = null; }
+ public bool deleted { get; set; default = false; }
+
+ public Email.dummy (string type_str) {
+ Object (type_descr: TypeSet.email.lookup_descriptor_in_store (type_str));
+ }
+
+ public Email (EmailFieldDetails details) {
+ Object (type_descr: TypeSet.email.lookup_descriptor_for_field_details (details),
+ address: details.value,
+ parameters: details.parameters);
+ }
+ }
+
+ protected Gee.List<Email> emails = new ArrayList<Email> ();
+
+ public override int n_elements { get { return this.emails.size; } }
+
+ public EmailsProperty (Persona? persona) {
+ Object (
+ property_name: "email-addresses",
+ persona: persona
+ );
+
+ if (persona != null) {
+ unowned EmailDetails? details = persona as EmailDetails;
+ foreach (var email in details.email_addresses)
+ this.emails.add (new Email (email));
+ }
+ }
+
+ public static bool should_show (Persona persona) {
+ unowned EmailDetails? details = persona as EmailDetails;
+ return (details != null && !details.email_addresses.is_empty);
+ }
+
+ protected override void create_widgets (PropertyWidget prop_widget) {
+ foreach (var email in this.emails)
+ add_field (prop_widget, email);
+ }
+
+ protected virtual void add_field (PropertyWidget prop_widget, Email email) {
+ var type_label = prop_widget.create_type_label (email.type_descr.display_name);
+ var url = "mailto:" + Uri.escape_string (email.address, "@", false);
+ var value_label = prop_widget.create_value_link (email.address, url);
+ prop_widget.add_row (type_label, value_label);
+ }
+}
+
+public class Contacts.EditableEmailsProperty : EmailsProperty, EditableProperty {
+
+ public EditableEmailsProperty (Persona? persona) {
+ base (persona);
+
+ if (persona == null)
+ this.emails.add (new Email.dummy ("Personal"));
+ }
+
+ protected override void add_field (PropertyWidget prop_widget, EmailsProperty.Email email) {
+ var type_combo = prop_widget.create_type_combo (TypeSet.email, email.type_descr);
+ type_combo.changed.connect ((combo) => {
+ email.type_descr = type_combo.active_descriptor;
+ });
+
+ var entry = prop_widget.create_value_entry (email.address);
+ entry.changed.connect ((editable) => {
+ email.address = editable.get_chars ();
+ });
+ var delete_button = prop_widget.create_delete_button (_("Remove email address"));
+
+ prop_widget.add_row (type_combo, entry, delete_button);
+ }
+
+ public Value? create_value () {
+ if (this.emails.is_empty)
+ return null;
+
+ var new_details = create_new_field_details ();
+ // Check if we only had empty emails
+ if (new_details.is_empty)
+ return null;
+
+ var result = Value (new_details.get_type ());
+ result.set_object (new_details);
+ return result;
+ }
+
+ public async void save_changes () throws PropertyError {
+ assert (this.persona != null);
+
+ var new_emails = create_new_field_details ();
+ var old_emails = ((EmailDetails) this.persona).email_addresses;
+
+ if (!check_if_equal (old_emails, new_emails))
+ yield ((EmailDetails) this.persona).change_email_addresses (new_emails);
+ }
+
+ private HashSet<EmailFieldDetails>? create_new_field_details () {
+ var new_details = new HashSet<EmailFieldDetails> ();
+ foreach (var email in this.emails) {
+ if (email.address != "" || email.deleted)
+ continue;
+
+ var parameters = email.type_descr.add_type_to_parameters (email.parameters);
+ var details = new EmailFieldDetails (email.address, parameters);
+ new_details.add (details);
+ }
+
+ return new_details;
+ }
+}
+
+public class Contacts.UrlsProperty : AggregatedPersonaProperty {
+
+ protected class Url : Object {
+ public string url { get; set; default = ""; }
+ public MultiMap<string, string>? parameters { get; set; default = null; }
+ public bool deleted { get; set; default = false; }
+
+ public Url.dummy () {
+ }
+
+ public Url (UrlFieldDetails details) {
+ Object (url: details.value, parameters: details.parameters);
+ }
+ }
+
+ protected Gee.List<Url> urls = new ArrayList<Url> ();
+
+ public override int n_elements { get { return this.urls.size; } }
+
+ public UrlsProperty (Persona? persona) {
+ Object (
+ property_name: "urls",
+ persona: persona
+ );
+
+ if (persona != null) {
+ unowned UrlDetails? details = persona as UrlDetails;
+ foreach (var detail in details.urls)
+ this.urls.add (new Url (detail));
+ }
+ }
+
+ public static bool should_show (Persona persona) {
+ unowned UrlDetails? details = persona as UrlDetails;
+ return (details != null && !details.urls.is_empty);
+ }
+
+ protected override void create_widgets (PropertyWidget prop_widget) {
+ foreach (var url in this.urls)
+ add_field (prop_widget, url);
+ }
+
+ protected virtual void add_field (PropertyWidget prop_widget, Url url) {
+ var type_label = prop_widget.create_type_label (_("Website"));
+ var url_link = Uri.escape_string (url.url, "@", false);
+ var value_label = prop_widget.create_value_link (url.url, url_link);
+
+ prop_widget.add_row (type_label, value_label);
+ }
+}
+
+public class Contacts.EditableUrlsProperty : UrlsProperty, EditableProperty {
+
+ public EditableUrlsProperty (Persona? persona) {
+ base (persona);
+
+ if (persona == null)
+ this.urls.add (new Url.dummy());
+ }
+
+ protected override void add_field (PropertyWidget prop_widget, UrlsProperty.Url url) {
+ var type_label = prop_widget.create_type_label (_("Website"));
+ var entry = prop_widget.create_value_entry (url.url);
+ entry.changed.connect ((editable) => {
+ url.url = editable.get_chars ();
+ });
+ var delete_button = prop_widget.create_delete_button (_("Remove website"));
+
+ prop_widget.add_row (type_label, entry, delete_button);
+ }
+
+ public Value? create_value () {
+ if (this.urls.is_empty)
+ return null;
+
+ var new_details = create_new_field_details ();
+ // Check if we only had empty urls
+ if (new_details.is_empty)
+ return null;
+
+ var result = Value (new_details.get_type ());
+ result.set_object (new_details);
+ return result;
+ }
+
+ public async void save_changes () throws PropertyError {
+ assert (this.persona != null);
+
+ var new_urls = create_new_field_details ();
+ yield ((UrlDetails) this.persona).change_urls (new_urls);
+ }
+
+ private HashSet<UrlFieldDetails>? create_new_field_details () {
+ var new_details = new HashSet<UrlFieldDetails> ();
+ foreach (var url in this.urls) {
+ if (url.url == "" || url.deleted)
+ continue;
+
+ var url_details = new UrlFieldDetails (url.url, url.parameters);
+ new_details.add (url_details);
+ }
+
+ return new_details;
+ }
+}
+
+public class Contacts.NotesProperty : AggregatedPersonaProperty {
+
+ protected class Note : Object {
+ public string text { get; set; default = ""; }
+ public MultiMap<string, string>? parameters { get; set; default = null; }
+ public bool deleted { get; set; default = false; }
+
+ public Note.dummy () {
+ }
+
+ public Note (NoteFieldDetails details) {
+ Object (text: details.value, parameters: details.parameters);
+ }
+ }
+
+ protected Gee.List<Note> notes = new ArrayList<Note> ();
+
+ public override int n_elements { get { return this.notes.size; } }
+
+ public NotesProperty (Persona? persona) {
+ Object (
+ property_name: "notes",
+ persona: persona
+ );
+
+ if (persona != null) {
+ unowned NoteDetails? details = persona as NoteDetails;
+ foreach (var note_detail in details.notes)
+ this.notes.add (new Note (note_detail));
+ }
+ }
+
+ public static bool should_show (Persona persona) {
+ unowned NoteDetails? details = persona as NoteDetails;
+ return (details != null && !details.notes.is_empty);
+ }
+
+ protected override void create_widgets (PropertyWidget prop_widget) {
+ foreach (var note in this.notes)
+ add_field (prop_widget, note);
+ }
+
+ protected virtual void add_field (PropertyWidget prop_widget, Note note) {
+ var type_label = prop_widget.create_type_label (_("Note"));
+ var value_label = prop_widget.create_value_label (note.text);
+ prop_widget.add_row (type_label, value_label);
+ }
+}
+
+public class Contacts.EditableNotesProperty : NotesProperty, EditableProperty {
+
+ public EditableNotesProperty (Persona persona) {
+ base (persona);
+
+ if (persona == null)
+ this.notes.add (new Note.dummy ());
+ }
+
+ protected override void add_field (PropertyWidget prop_widget, NotesProperty.Note note) {
+ var type_label = prop_widget.create_type_label (_("Note"));
+ var textview_container = prop_widget.create_value_textview (note.text);
+ /* XXX entry.changed.connect ((editable) => { */
+ /* this.urls[row_nr] = editable.get_chars (); */
+ /* }); */
+ var delete_button = prop_widget.create_delete_button (_("Remove note"));
+
+ prop_widget.add_row (type_label, textview_container, delete_button);
+ }
+
+ public Value? create_value () {
+ if (this.notes.is_empty)
+ return null;
+
+ var new_details = create_new_field_details ();
+ // Check if we only had empty addresses
+ if (new_details.is_empty)
+ return null;
+
+ var result = Value (new_details.get_type ());
+ result.set_object (new_details);
+ return result;
+ }
+
+ public async void save_changes () throws PropertyError {
+ assert (this.persona != null);
+
+ var new_addrs = create_new_field_details ();
+ yield ((NoteDetails) this.persona).change_notes (new_addrs);
+ }
+
+ private HashSet<NoteFieldDetails>? create_new_field_details () {
+ var new_details = new HashSet<NoteFieldDetails> ();
+ foreach (var note in this.notes) {
+ if (note.text == "" || note.deleted)
+ continue;
+
+ var note_detail = new NoteFieldDetails (note.text, note.parameters);
+ new_details.add (note_detail);
+ }
+
+ return new_details;
+ }
+}
+
+public class Contacts.PostalAddressesProperty : AggregatedPersonaProperty {
+
+ // Note that this is a wrapper around Folkd.PostalAddress
+ protected class PostalAddr : Object {
+ public TypeDescriptor type_descr { get; set; }
+ public PostalAddress address { get; set; }
+ public MultiMap<string, string>? parameters { get; set; default = null; }
+ public bool deleted { get; set; default = false; }
+
+ public PostalAddr.dummy (string type_str) {
+ Object (
+ type_descr: TypeSet.general.lookup_descriptor_in_store (type_str),
+ address: new PostalAddress("", "", "", "", "", "", "", "", "")
+ );
+ }
+
+ public PostalAddr (PostalAddressFieldDetails details) {
+ Object (type_descr: TypeSet.general.lookup_descriptor_for_field_details (details),
+ address: details.value,
+ parameters: details.parameters);
+ }
+
+ public bool is_empty () {
+ return this.address.po_box == "" &&
+ this.address.extension == "" &&
+ this.address.street == "" &&
+ this.address.locality == "" &&
+ this.address.region == "" &&
+ this.address.postal_code == "" &&
+ this.address.country == "" &&
+ this.address.address_format == "";
+ }
+ }
+
+ protected Gee.List<PostalAddr> addresses = new ArrayList<PostalAddr> ();
+
+ public override int n_elements { get { return this.addresses.size; } }
+
+ public PostalAddressesProperty (Persona? persona) {
+ Object (
+ property_name: "postal-addresses",
+ persona: persona
+ );
+
+ if (persona != null) {
+ unowned PostalAddressDetails? details = persona as PostalAddressDetails;
+ foreach (var address_details in details.postal_addresses) {
+ this.addresses.add (new PostalAddr (address_details));
+ }
+ }
+ }
+
+ public static bool should_show (Persona persona) {
+ unowned PostalAddressDetails? details = persona as PostalAddressDetails;
+ return (details != null && !details.postal_addresses.is_empty);
+ }
+
+ protected override void create_widgets (PropertyWidget prop_widget) {
+ foreach (var addr in this.addresses)
+ add_field (prop_widget, addr);
+ }
+
+ protected virtual void add_field (PropertyWidget prop_widget, PostalAddr addr) {
+ var type_label = prop_widget.create_type_label (addr.type_descr.display_name);
+ var value_label = prop_widget.create_value_label (format_address (addr.address));
+ prop_widget.add_row (type_label, value_label);
+ }
+
+ private static string format_address (PostalAddress addr) {
+ string[] lines = {};
+
+ if (addr.street != "")
+ lines += addr.street;
+ if (addr.extension != "")
+ lines += addr.extension;
+ if (addr.locality != "")
+ lines += addr.locality;
+ if (addr.region != "")
+ lines += addr.region;
+ if (addr.postal_code != "")
+ lines += addr.postal_code;
+ if (addr.po_box != "")
+ lines += addr.po_box;
+ if (addr.country != "")
+ lines += addr.country;
+ if (addr.address_format != "")
+ lines += addr.address_format;
+
+ return string.joinv ("\n", lines);
+ }
+}
+
+public class Contacts.EditablePostalAddressesProperty : PostalAddressesProperty, EditableProperty {
+
+ 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 EditablePostalAddressesProperty (Persona? persona) {
+ base (persona);
+
+ if (persona == null)
+ this.addresses.add (new PostalAddr.dummy ("Home"));
+ }
+
+ protected override void add_field (PropertyWidget prop_widget, PostalAddressesProperty.PostalAddr addr) {
+ var type_combo = prop_widget.create_type_combo (TypeSet.general, addr.type_descr);
+ type_combo.changed.connect ((combo) => {
+ addr.type_descr = type_combo.active_descriptor;
+ });
+
+ var grid = new Grid ();
+ grid.orientation = Orientation.VERTICAL;
+ for (int i = 0; i < POSTAL_ELEMENT_PROPS.length; i++) {
+ unowned string address_part = POSTAL_ELEMENT_PROPS[i];
+ string part;
+ addr.address.get (address_part, out part);
+
+ var part_entry = prop_widget.create_value_entry (part);
+ part_entry.get_style_context ().add_class ("contacts-postal-entry");
+ part_entry.placeholder_text = POSTAL_ELEMENT_NAMES[i];
+ grid.add (part_entry);
+ }
+ grid.show_all ();
+ var delete_button = prop_widget.create_delete_button (_("Remove postal address"));
+
+ prop_widget.add_row (type_combo, grid, delete_button);
+ }
+
+ public Value? create_value () {
+ if (this.addresses.is_empty)
+ return null;
+
+ var new_details = create_new_field_details ();
+ // Check if we only had empty addresses
+ if (new_details.is_empty)
+ return null;
+
+ var result = Value (new_details.get_type ());
+ result.set_object (new_details);
+ return result;
+ }
+
+ public async void save_changes () throws PropertyError {
+ assert (this.persona != null);
+
+ var new_addrs = create_new_field_details ();
+
+ var old_addrs = ((PostalAddressDetails) this.persona).postal_addresses;
+ if (!check_if_equal (old_addrs, new_addrs))
+ yield ((PostalAddressDetails) this.persona).change_postal_addresses (new_addrs);
+ }
+
+ private HashSet<PostalAddressFieldDetails>? create_new_field_details () {
+ var new_details = new HashSet<PostalAddressFieldDetails> ();
+ foreach (var addr in this.addresses) {
+ if (addr.is_empty() || addr.deleted)
+ continue;
+
+ var parameters = addr.type_descr.add_type_to_parameters (addr.parameters);
+ var address = new PostalAddressFieldDetails (addr.address, parameters);
+ new_details.add (address);
+ }
+
+ return new_details;
+ }
+}
diff --git a/src/contacts-type-combo.vala b/src/contacts-type-combo.vala
index db998a4..a75212c 100644
--- a/src/contacts-type-combo.vala
+++ b/src/contacts-type-combo.vala
@@ -51,7 +51,6 @@ public class Contacts.TypeCombo : ComboBox {
construct {
this.valign = Align.START;
this.halign = Align.FILL;
- this.hexpand = true;
this.visible = true;
var renderer = new CellRendererText ();
diff --git a/src/contacts-type-descriptor.vala b/src/contacts-type-descriptor.vala
index efc96ce..a83fad5 100644
--- a/src/contacts-type-descriptor.vala
+++ b/src/contacts-type-descriptor.vala
@@ -89,28 +89,33 @@ public class Contacts.TypeDescriptor : Object {
return this.source == Source.CUSTOM;
}
- public void save_to_field_details (AbstractFieldDetails details) {
+ /**
+ * Saves the type decribed by this object to the given parameters (as found
+ * in the parameters property of a {@link Folks.AbstractFieldDetails} object.
+ *
+ * If old_parameters is specified, it will also copy over all fields (that
+ * not related to the type of the property).
+ *
+ * @param old_parameters: The previous parameters to base on, or null if none.
+ */
+ public MultiMap<string, string> add_type_to_parameters (MultiMap<string, string>? old_parameters) {
debug ("Saving type %s", to_string ());
- var old_parameters = details.parameters;
var new_parameters = new HashMultiMap<string, string> ();
// Check whether PREF VCard "flag" is set
bool has_pref = false;
- foreach (var val in old_parameters["type"]) {
- if (val.ascii_casecmp ("PREF") == 0) {
- has_pref = true;
- break;
+ if (old_parameters != null) {
+ has_pref = TypeDescriptor.parameters_have_type_pref (old_parameters);
+
+ // Copy over all parameters, execept the ones we're going to create ourselves
+ foreach (var param in old_parameters.get_keys ()) {
+ if (param != "type" && param != X_GOOGLE_LABEL)
+ foreach (var val in old_parameters[param])
+ new_parameters[param] = val;
}
}
- // Copy over all parameters, execept the ones we're going to create ourselves
- foreach (var param in old_parameters.get_keys ()) {
- if (param != "type" && param != X_GOOGLE_LABEL)
- foreach (var val in old_parameters[param])
- new_parameters[param] = val;
- }
-
// Set the type based on our Source
switch (this.source) {
case Source.VCARD:
@@ -130,8 +135,58 @@ public class Contacts.TypeDescriptor : Object {
if (has_pref)
new_parameters["type"] = "PREF";
- // We didn't crash 'n burn, so lets
- details.parameters = new_parameters;
+ return new_parameters;
+ }
+
+ public static bool parameters_have_type_pref (MultiMap<string, string> parameters) {
+ foreach (var val in parameters["type"])
+ if (val.ascii_casecmp ("PREF") == 0)
+ return true;
+
+ return false;
+ }
+
+ /**
+ * Checks whether the values related to a {@link TypeDescriptor} in the given
+ * parameters (as one might find in a {@link Folks.AbstractFieldDetails}) are
+ * equal.
+ *
+ * @param parameters_a: The first parameters multimap to compare
+ * @param parameters_b: The second parameters multimap to compare
+ *
+ * @return: Whether the type parameters ("type" and "PREF") are equal
+ */
+ public static bool check_type_parameters_equal (MultiMap<string, string> parameters_a,
+ MultiMap<string, string> parameters_b) {
+ // First check if some "PREF" value changed
+ if (TypeDescriptor.parameters_have_type_pref (parameters_a)
+ != TypeDescriptor.parameters_have_type_pref (parameters_b))
+ return false;
+
+ // Next, check for any custom Google property labels
+ var google_label_a = Utils.get_first<string> (parameters_a[X_GOOGLE_LABEL]);
+ var google_label_b = Utils.get_first<string> (parameters_b[X_GOOGLE_LABEL]);
+ if (google_label_a != null || google_label_b != null) {
+ // Note that we do a case-sensitive comparison for custom labels
+ return google_label_a == google_label_b;
+ }
+
+ // Finally, check the type parameters
+ var types_a = new ArrayList<string>.wrap (parameters_a["type"].to_array ());
+ var types_b = new ArrayList<string>.wrap (parameters_b["type"].to_array ());
+
+ if (types_a.size != types_b.size)
+ return false;
+
+ // Now we check if types are esual. Note that we might be a bit more strict
+ // than truly necessary, but from a UI perspective they are still the same
+ types_a.sort ();
+ types_b.sort ();
+ for (int i = 0; i < types_a.size; i++)
+ if (types_a[i].ascii_casecmp (types_b[i]) != 0)
+ return false;
+
+ return true;
}
/**
diff --git a/src/contacts-utils.vala b/src/contacts-utils.vala
index 5552e8c..e82e8ba 100644
--- a/src/contacts-utils.vala
+++ b/src/contacts-utils.vala
@@ -70,15 +70,6 @@ namespace Contacts {
}
namespace Contacts.Utils {
- public void compose_mail (string email) {
- var mailto_uri = "mailto:" + Uri.escape_string (email, "@" , false);
- try {
- Gtk.show_uri_on_window (null, mailto_uri, 0);
- } catch (Error e) {
- debug ("Couldn't launch URI \"%s\": %s", mailto_uri, e.message);
- }
- }
-
#if HAVE_TELEPATHY
public void start_chat (Contact contact, string protocol, string id) {
var im_persona = contact.find_im_persona (protocol, id);
@@ -173,4 +164,63 @@ namespace Contacts.Utils {
dialog.run();
dialog.destroy();
}
+
+ public static int compare_personas_on_store (Persona? a, Persona? b) {
+ if (a == null || b == null)
+ return (a == null)? -1 : 1;
+
+ var store_a = a.store;
+ var store_b = b.store;
+
+ // In the same store, sort Google 'other' contacts last
+ if (store_a == store_b) {
+ if (!Utils.persona_is_google (a))
+ return 0;
+
+ var a_is_other = Utils.persona_is_google_other (a);
+ if (a_is_other != Utils.persona_is_google_other (b))
+ return a_is_other? 1 : -1;
+ }
+
+ // Sort primary stores before others
+ if (store_a.is_primary_store != store_b.is_primary_store)
+ return (store_a.is_primary_store)? -1 : 1;
+
+ // E-D-S stores get prioritized
+ if ((store_a.type_id == "eds") != (store_b.type_id == "eds"))
+ return (store_a.type_id == "eds")? -1 : 1;
+
+ // Normal case: use alphabetical sorting
+ return strcmp (store_a.id, store_b.id);
+ }
+
+ public bool persona_is_google (Persona persona) {
+ return persona.store.type_id == "eds" && esource_uid_is_google (persona.store.id);
+ }
+
+ /**
+ * Return true only for personas which are in a Google address book, but which
+ * are not in the user's "My Contacts" group in the address book.
+ */
+ public bool persona_is_google_other (Persona persona) {
+ if (!persona_is_google (persona))
+ return false;
+
+ var p = persona as Edsf.Persona;
+ return p != null && !p.in_google_personal_group;
+ }
+
+ public bool persona_is_google_profile (Persona persona) {
+ if (!persona_is_google_other (persona))
+ return false;
+
+ var u = persona as UrlDetails;
+ if (u != null && u.urls.size == 1) {
+ foreach (var url in u.urls) {
+ if (/https?:\/\/www.google.com\/profiles\/[0-9]+$/.match(url.value))
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/src/meson.build b/src/meson.build
index 773c3dc..2d33881 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -90,6 +90,7 @@ contacts_vala_sources = files(
'contacts-linking.vala',
'contacts-list-pane.vala',
'contacts-max-width-bin.vala',
+ 'contacts-persona-property.vala',
'contacts-settings.vala',
'contacts-setup-window.vala',
'contacts-type-combo.vala',