[gnome-contacts/wip/nielsdg/plugins-for-form-fields: 10/11] WIP: Rewrite the ContactEditor/ContactSheet
- From: Niels De Graef <nielsdg src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-contacts/wip/nielsdg/plugins-for-form-fields: 10/11] WIP: Rewrite the ContactEditor/ContactSheet
- Date: Wed, 23 Jan 2019 22:53:29 +0000 (UTC)
commit 1082f480f1a5fd143b43f826b69cdc080a44602a
Author: Niels De Graef <nielsdegraef gmail com>
Date: Sat Dec 1 17:42:24 2018 +0100
WIP: Rewrite the ContactEditor/ContactSheet
data/ui/contacts-contact-form.ui | 14 +
data/ui/style.css | 4 +
src/contacts-contact-editor.vala | 889 ++++----------------------------
src/contacts-contact-form-field.vala | 975 +++++++++++++++++++++++++++++++++++
src/contacts-contact-form.vala | 109 +++-
src/contacts-contact-pane.vala | 54 +-
src/contacts-contact-sheet.vala | 242 ++-------
src/contacts-contact.vala | 153 +-----
src/contacts-type-descriptor.vala | 37 +-
src/contacts-utils.vala | 68 ++-
src/meson.build | 1 +
11 files changed, 1334 insertions(+), 1212 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 dc5ad4a..cdd4bd4 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,79 @@ 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 in p.writeable_properties) {
+ var field = add_edit_row (p, prop);
+ if (field != null)
+ add_field (field);
}
}
}
private void fill_in_empty () {
- this.last_row = 2;
-
- this.writable_personas["null-persona.hack"] = new HashMap<string, Field?> ();
foreach (var prop in DEFAULT_PROPS_NEW_CONTACT) {
var tok = prop.split (".");
- add_new_row_for_property (null, tok[0], tok[1].up ());
- }
-
- this.focus_widget = this.name_entry;
- }
-
- Value get_value_from_emails (HashMap<int, RowData?> rows) {
- var new_details = new HashSet<EmailFieldDetails>();
-
- foreach (var row_entry in rows.entries) {
- var combo = container_grid.get_child_at (0, row_entry.key) as TypeCombo;
- var entry = container_grid.get_child_at (1, row_entry.key) as Entry;
-
- /* Ignore empty entries. */
- if (entry.get_text () == "")
- continue;
-
- combo.active_descriptor.save_to_field_details (row_entry.value.details);
- var details = new EmailFieldDetails (entry.get_text (), row_entry.value.details.parameters);
- new_details.add (details);
- }
- var new_value = Value (new_details.get_type ());
- new_value.set_object (new_details);
-
- return new_value;
- }
-
- Value get_value_from_phones (HashMap<int, RowData?> rows) {
- var new_details = new HashSet<PhoneFieldDetails>();
-
- foreach (var row_entry in rows.entries) {
- var combo = container_grid.get_child_at (0, row_entry.key) as TypeCombo;
- var entry = container_grid.get_child_at (1, row_entry.key) as Entry;
-
- /* Ignore empty entries. */
- if (entry.get_text () == "")
- continue;
-
- combo.active_descriptor.save_to_field_details (row_entry.value.details);
- var details = new PhoneFieldDetails (entry.get_text (), row_entry.value.details.parameters);
- new_details.add (details);
- }
- var new_value = Value (new_details.get_type ());
- new_value.set_object (new_details);
- return new_value;
- }
-
- Value get_value_from_urls (HashMap<int, RowData?> rows) {
- var new_details = new HashSet<UrlFieldDetails>();
-
- foreach (var row_entry in rows.entries) {
- var entry = container_grid.get_child_at (1, row_entry.key) as Entry;
-
- /* Ignore empty entries. */
- if (entry.get_text () == "")
- continue;
-
- var details = new UrlFieldDetails (entry.get_text (), row_entry.value.details.parameters);
- new_details.add (details);
- }
- var new_value = Value (new_details.get_type ());
- new_value.set_object (new_details);
- return new_value;
- }
-
- Value get_value_from_nickname (HashMap<int, RowData?> rows) {
- var new_value = Value (typeof (string));
- foreach (var row_entry in rows.entries) {
- var entry = container_grid.get_child_at (1, row_entry.key) as Entry;
-
- /* Ignore empty entries. */
- if (entry.get_text () == "")
- continue;
-
- new_value.set_string (entry.get_text ());
- }
- return new_value;
- }
-
- Value get_value_from_birthday (HashMap<int, RowData?> rows) {
- var new_value = Value (typeof (DateTime));
- foreach (var row_entry in rows.entries) {
- var box = container_grid.get_child_at (1, row_entry.key) as Grid;
- var day_spin = box.get_child_at (0, 0) as SpinButton;
- var combo = box.get_child_at (1, 0) as ComboBoxText;
- var year_spin = box.get_child_at (2, 0) as SpinButton;
-
- var bday = new DateTime.local (year_spin.get_value_as_int (),
- combo.get_active () + 1,
- day_spin.get_value_as_int (),
- 0, 0, 0);
- bday = bday.to_utc ();
-
- new_value.set_boxed (bday);
- }
- return new_value;
- }
-
- Value get_value_from_notes (HashMap<int, RowData?> rows) {
- var new_details = new HashSet<NoteFieldDetails>();
-
- foreach (var row_entry in rows.entries) {
- var text = (container_grid.get_child_at (1, row_entry.key) as Bin).get_child () as TextView;
- TextIter start, end;
- text.get_buffer ().get_start_iter (out start);
- text.get_buffer ().get_end_iter (out end);
- var value = text.get_buffer ().get_text (start, end, true);
- if (value != "") {
- var details = new NoteFieldDetails (value, row_entry.value.details.parameters);
- new_details.add (details);
- }
- }
- var new_value = Value (new_details.get_type ());
- new_value.set_object (new_details);
- return new_value;
- }
-
- Value get_value_from_addresses (HashMap<int, RowData?> rows) {
- var new_details = new HashSet<PostalAddressFieldDetails>();
-
- foreach (var row_entry in rows.entries) {
- var combo = container_grid.get_child_at (0, row_entry.key) as TypeCombo;
- var addr_editor = container_grid.get_child_at (1, row_entry.key) as AddressEditor;
- combo.active_descriptor.save_to_field_details (row_entry.value.details);
-
- var new_value = new PostalAddress (addr_editor.details.value.po_box,
- addr_editor.details.value.extension,
- addr_editor.details.value.street,
- addr_editor.details.value.locality,
- addr_editor.details.value.region,
- addr_editor.details.value.postal_code,
- addr_editor.details.value.country,
- addr_editor.details.value.address_format,
- addr_editor.details.id);
- for (int i = 0; i < addr_editor.entries.length; i++)
- new_value.set (AddressEditor.postal_element_props[i], addr_editor.entries[i].get_text ());
-
- var details = new PostalAddressFieldDetails(new_value, row_entry.value.details.parameters);
- new_details.add (details);
- }
- var new_value = Value (new_details.get_type ());
- new_value.set_object (new_details);
- return new_value;
- }
-
- void set_field_changed (int row) {
- foreach (var fields in writable_personas.values) {
- foreach (var entry in fields.entries) {
- if (row in entry.value.rows.keys) {
- if (entry.value.changed)
- return;
-
- entry.value.changed = true;
- return;
- }
- }
- }
- }
-
- new void remove_row (int row) {
- foreach (var fields in writable_personas.values) {
- foreach (var field_entry in fields.entries) {
- foreach (var idx in field_entry.value.rows.keys) {
- if (idx == row) {
- var child = container_grid.get_child_at (0, row);
- child.destroy ();
- child = container_grid.get_child_at (1, row);
- child.destroy ();
- child = container_grid.get_child_at (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 field = add_edit_row (null, tok[0], true, tok[1].up ());
+ if (field != null)
+ add_field (field);
}
- 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;
+ this.focus_widget = this.name_widget;
}
- 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";
+ PropertyField? 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)
+ return new EditableEmailsField.empty ();
+ if (EmailsField.should_show (p))
+ return new EditableEmailsField (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)
+ return new EditablePhoneNrsField.empty ();
+ if (PhoneNrsField.should_show (p))
+ return new EditablePhoneNrsField (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)
+ return new EditableUrlsField.empty ();
+ if (UrlsField.should_show (p))
+ return new EditableUrlsField (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)
+ return new EditableNicknameField.empty ();
+ if (NicknameField.should_show (p))
+ return new EditableNicknameField (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)
+ return new EditableBirthdayField.empty ();
+ if (BirthdayField.should_show (p))
+ return new EditableBirthdayField (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)
+ return new EditableNotesField.empty ();
+ if (NotesField.should_show (p))
+ return new EditableNotesField (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)
+ return new EditablePostalAddressesField.empty ();
+ if (PostalAddressesField.should_show (p))
+ return new EditablePostalAddressesField (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 +166,67 @@ 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.fields.get_n_items (); i++) {
+ var field = this.fields.get_item (i) as EditableProperty;
+ if (field != null)
+ yield field.save_changes ();
}
- return props_set;
- }
+ if (name_changed ()) {
+ var v = get_full_name_value ();
+ yield this.contact.set_individual_property ("full-name", v);
+ /*XXX*/
+ /* display_name_changed (v.get_string ()); */
+ }
- 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;
- }
+ if (avatar_changed ()) {
+ var v = get_avatar_value ();
+ yield this.contact.set_individual_property ("avatar", v);
}
+ }
- 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;
- }
+ 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.fields.get_n_items (); i++) {
+ var field = this.fields.get_item (i) as EditableProperty;
+
+ if (field != null)
+ details[field.property_name] = field.create_value ();
}
- 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 ();
+
+ return details;
+ }
+
+ public void add_new_row_for_property (Persona? p, string prop_name, string? type = null) {
}
// 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 +235,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 +253,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-field.vala b/src/contacts-contact-form-field.vala
new file mode 100644
index 0000000..6419a21
--- /dev/null
+++ b/src/contacts-contact-form-field.vala
@@ -0,0 +1,975 @@
+/*
+ * 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 PropertyField is an abstraction of the property of a {@link Folks.Persona}.
+ */
+public abstract class Contacts.PropertyField : Object {
+
+ /**
+ * The {@link Folks.Persona} this property belongs to.
+ */
+ public Persona? persona { get; construct set; default = null; }
+
+ /**
+ * The canonical name of this property.
+ */
+ public string property_name { get; construct set; }
+
+// XXX Do we really need to save this?
+ protected PropertyWidget? row { get; private set; }
+
+ /**
+ * Each subclass is responsible for creating the necessary UI and
+ * incorporating it into the given {@link PropertyWidget}.
+ */
+ protected abstract void create_widgets (PropertyWidget widget);
+
+ /**
+ * 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) {
+ this.row = new PropertyWidget (this, label_group, value_group, actions_group);
+ this.row.margin_top = 18;
+ this.row.hexpand = true;
+
+ // The subclass is responsible to make the appropriate widgets
+ create_widgets (row);
+
+ this.row.show_all ();
+
+ return this.row;
+ }
+}
+
+public interface Contacts.EditableProperty : PropertyField {
+
+ /* public bool dirty { get; construct set; } */
+
+ /**
+ * 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`.
+ */
+ public abstract async void save_changes () throws PropertyError;
+}
+
+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 field
+/// XXX maybe only store the persona?
+ public weak PropertyField field { 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 (PropertyField parent, SizeGroup labels, SizeGroup values, SizeGroup actions) {
+ Object (field: 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.NicknameField : PropertyField {
+
+ protected string nickname = "";
+
+ public NicknameField (Persona persona) {
+ Object (
+ property_name: "nickname",
+ persona: persona
+ );
+
+ 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 widget) {
+ var type_label = row.create_type_label (_("Nickname"));
+ var value_label = row.create_value_label (this.nickname);
+ widget.add_row (type_label, value_label);
+ }
+}
+
+public class Contacts.EditableNicknameField : NicknameField, EditableProperty {
+
+ public EditableNicknameField (Persona persona) {
+ base (persona);
+ }
+
+ public EditableNicknameField.empty () {
+ Object (
+ property_name: "nickname",
+ persona: null
+ );
+ }
+
+ protected override void create_widgets (PropertyWidget widget) {
+ var type_label = row.create_type_label (_("Nickname"));
+ var nickname_entry = row.create_value_entry (this.nickname);
+ nickname_entry.changed.connect ((editable) => {
+ this.nickname = editable.get_chars ();
+ });
+
+ var delete_button = row.create_delete_button (_("Remove nickname"));
+ 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);
+
+ yield ((NameDetails) this.persona).change_nickname (this.nickname);
+ }
+}
+
+public class Contacts.BirthdayField : PropertyField {
+
+ // In local timezone
+ protected DateTime birthday;
+
+ public BirthdayField (Persona persona) {
+ Object (
+ property_name: "birthday",
+ persona: persona
+ );
+
+ 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 widget) {
+ var type_label = row.create_type_label (_("Birthday"));
+ var value_label = row.create_value_label (this.birthday.format ("%x"));
+ widget.add_row (type_label, value_label);
+ }
+}
+
+public class Contacts.EditableBirthdayField : BirthdayField, EditableProperty {
+
+ public EditableBirthdayField (Persona persona) {
+ base (persona);
+ }
+
+ public EditableBirthdayField.empty () {
+ Object (
+ property_name: "birthday",
+ persona: null
+ );
+
+ this.birthday = new DateTime.now_local ();
+ }
+
+ protected override void create_widgets (PropertyWidget widget) {
+ var type_label = row.create_type_label (_("Birthday"));
+ var birthday_entry = create_date_widget ();
+ var delete_button = row.create_delete_button (_("Remove birthday"));
+ 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 () {
+ 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);
+
+ yield ((BirthdayDetails) this.persona).change_birthday (this.birthday);
+ }
+}
+
+public class Contacts.PhoneNrsField : PropertyField {
+
+ protected ArrayList<TypeDescriptor> types = new ArrayList<TypeDescriptor> ();
+ protected ArrayList<string> phone_nrs = new ArrayList<string> ();
+ protected ArrayList<unowned MultiMap<string, string>?> parameters
+ = new ArrayList<unowned MultiMap<string, string>?> ();
+
+ public PhoneNrsField (Persona persona) {
+ Object (
+ property_name: "phone-numbers",
+ persona: persona
+ );
+
+ unowned PhoneDetails? details = this.persona as PhoneDetails;
+ foreach (var phone in details.phone_numbers) {
+ var type_descr = TypeSet.phone.lookup_descriptor_for_field_details (phone);
+ this.types.add (type_descr);
+ this.phone_nrs.add (phone.value);
+ this.parameters.add (phone.parameters);
+ }
+ }
+
+ 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 widget) {
+ for (int i = 0; i < this.phone_nrs.size; i++)
+ add_field (this.types[i], this.phone_nrs[i], widget);
+ }
+
+ protected virtual void add_field (TypeDescriptor type, string phone, PropertyWidget widget) {
+ var type_label = row.create_type_label (type.display_name);
+ var value_label = row.create_value_label (phone);
+ widget.add_row (type_label, value_label);
+ }
+}
+
+public class Contacts.EditablePhoneNrsField : PhoneNrsField, EditableProperty {
+
+ public EditablePhoneNrsField (Persona persona) {
+ base (persona);
+ }
+
+ public EditablePhoneNrsField.empty () {
+ Object (
+ property_name: "phone-numbers",
+ persona: null
+ );
+
+ this.types.add (TypeSet.phone.lookup_descriptor_in_store ("Mobile"));
+ this.phone_nrs.add ("");
+ this.parameters.add (null);
+ }
+
+ protected override void add_field (TypeDescriptor type, string phone, PropertyWidget widget) {
+ var type_combo = row.create_type_combo (TypeSet.phone, type);
+ type_combo.changed.connect ((combo) => {
+ var row_nr = row.get_child_row (combo);
+ this.types[row_nr] = type_combo.active_descriptor;
+ });
+
+ var entry = row.create_value_entry (phone);
+ entry.changed.connect ((editable) => {
+ var row_nr = row.get_child_row ((Widget) editable);
+ this.phone_nrs[row_nr] = editable.get_chars ();
+ });
+
+ var delete_button = row.create_delete_button (_("Remove phone number"));
+
+ /* widget.add_row (type_label, entry, delete_button); */
+ widget.add_row (type_combo, entry, delete_button);
+ }
+
+ public Value? create_value () {
+ if (this.phone_nrs.is_empty)
+ return null;
+
+ var new_details = create_set ();
+ // 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_addrs = create_set ();
+ yield ((PhoneDetails) this.persona).change_phone_numbers (new_addrs);
+ }
+
+ private HashSet<PhoneFieldDetails>? create_set () {
+ var new_details = new HashSet<PhoneFieldDetails> ();
+ for (int i = 0; i < this.phone_nrs.size; i++) {
+ if (this.phone_nrs[i] == "")
+ continue;
+
+ var parameters = this.types[i].add_type_to_parameters (this.parameters[i]);
+ var phone = new PhoneFieldDetails (this.phone_nrs[i], parameters);
+ new_details.add (phone);
+ }
+
+ return new_details;
+ }
+}
+
+public class Contacts.EmailsField : PropertyField {
+
+ protected ArrayList<TypeDescriptor> types = new ArrayList<TypeDescriptor> ();
+ protected ArrayList<string> emails = new ArrayList<string> ();
+ protected ArrayList<unowned MultiMap<string, string>?> parameters
+ = new ArrayList<unowned MultiMap<string, string>?> ();
+
+ public EmailsField (Persona persona) {
+ Object (
+ property_name: "email-addresses",
+ persona: persona
+ );
+
+ unowned EmailDetails? details = persona as EmailDetails;
+ foreach (var email in details.email_addresses) {
+ var type_descr = TypeSet.email.lookup_descriptor_for_field_details (email);
+ this.types.add (type_descr);
+ this.emails.add (email.value);
+ this.parameters.add (email.parameters);
+ }
+ }
+
+ 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 widget) {
+ for (int i = 0; i < this.emails.size; i++)
+ add_field (this.types[i], this.emails[i], widget);
+ }
+
+ protected virtual void add_field (TypeDescriptor type, string email, PropertyWidget widget) {
+ var type_label = row.create_type_label (type.display_name);
+ var url = "mailto:" + Uri.escape_string (email, "@", false);
+ var value_label = row.create_value_link (email, url);
+ widget.add_row (type_label, value_label);
+ }
+}
+
+public class Contacts.EditableEmailsField : EmailsField, EditableProperty {
+
+ public EditableEmailsField (Persona persona) {
+ base (persona);
+ }
+
+ public EditableEmailsField.empty () {
+ Object (
+ property_name: "email-addresses",
+ persona: null
+ );
+
+ this.types.add (TypeSet.email.lookup_descriptor_in_store ("Personal"));
+ this.emails.add ("");
+ this.parameters.add (null);
+ }
+
+ protected override void add_field (TypeDescriptor type, string email, PropertyWidget widget) {
+ var type_combo = row.create_type_combo (TypeSet.email, type);
+ type_combo.changed.connect ((combo) => {
+ var row_nr = row.get_child_row (combo);
+ this.types[row_nr] = type_combo.active_descriptor;
+ });
+
+ var entry = row.create_value_entry (email);
+ entry.changed.connect ((editable) => {
+ var row_nr = row.get_child_row ((Widget) editable);
+ this.emails[row_nr] = editable.get_chars ();
+ });
+ var delete_button = row.create_delete_button (_("Remove email address"));
+
+ widget.add_row (type_combo, entry, delete_button);
+ }
+
+ public Value? create_value () {
+ if (this.emails.is_empty)
+ return null;
+
+ var new_details = create_set ();
+ // 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_addrs = create_set ();
+ yield ((EmailDetails) this.persona).change_email_addresses (new_addrs);
+ }
+
+ private HashSet<EmailFieldDetails>? create_set () {
+ var new_details = new HashSet<EmailFieldDetails> ();
+ for (int i = 0; i < this.emails.size; i++) {
+ if (this.emails[i] != "")
+ continue;
+
+ var parameters = this.types[i].add_type_to_parameters (this.parameters[i]);
+ var email = new EmailFieldDetails (this.emails[i], parameters);
+ new_details.add (email);
+ }
+
+ return new_details;
+ }
+}
+
+public class Contacts.UrlsField : PropertyField {
+
+ protected ArrayList<string> urls = new ArrayList<string> ();
+ protected ArrayList<unowned MultiMap<string, string>?> parameters
+ = new ArrayList<unowned MultiMap<string, string>?> ();
+
+ public UrlsField (Persona persona) {
+ Object (
+ property_name: "urls",
+ persona: persona
+ );
+
+ unowned UrlDetails? details = persona as UrlDetails;
+ foreach (var url in details.urls) {
+ this.urls.add (url.value);
+ this.parameters.add (url.parameters);
+ }
+ }
+
+ 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 widget) {
+ for (int i = 0; i < urls.size; i++)
+ add_field (this.urls[i], widget);
+ }
+
+ protected virtual void add_field (string url, PropertyWidget widget) {
+ var type_label = row.create_type_label (_("Website"));
+ var url_link = Uri.escape_string (url, "@", false);
+ var value_label = row.create_value_link (url, url_link);
+
+ widget.add_row (type_label, value_label);
+ }
+}
+
+public class Contacts.EditableUrlsField : UrlsField, EditableProperty {
+
+ public EditableUrlsField (Persona persona) {
+ base (persona);
+ }
+
+ public EditableUrlsField.empty () {
+ Object (
+ property_name: "urls",
+ persona: null
+ );
+
+ this.urls.add ("");
+ this.parameters.add (null);
+ }
+
+ protected override void add_field (string url, PropertyWidget widget) {
+ var type_label = row.create_type_label (_("Website"));
+ var entry = row.create_value_entry (url);
+ entry.changed.connect ((editable) => {
+ var row_nr = row.get_child_row ((Widget) editable);
+ this.urls[row_nr] = editable.get_chars ();
+ });
+ var delete_button = row.create_delete_button (_("Remove website"));
+
+ widget.add_row (type_label, entry, delete_button);
+ }
+
+ public Value? create_value () {
+ if (this.urls.is_empty)
+ return null;
+
+ var new_details = create_set ();
+ // 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_set ();
+ yield ((UrlDetails) this.persona).change_urls (new_urls);
+ }
+
+ private HashSet<UrlFieldDetails>? create_set () {
+ var new_details = new HashSet<UrlFieldDetails> ();
+ for (int i = 0; i < this.urls.size; i++) {
+ if (this.urls[i] == "")
+ continue;
+
+ var url = new UrlFieldDetails (this.urls[i], this.parameters[i]);
+ new_details.add (url);
+ }
+
+ return new_details;
+ }
+}
+
+public class Contacts.NotesField : PropertyField {
+
+ protected ArrayList<string> notes = new ArrayList<string> ();
+ protected ArrayList<unowned MultiMap<string, string>?> parameters
+ = new ArrayList<unowned MultiMap<string, string>?> ();
+
+ public NotesField (Persona persona) {
+ Object (
+ property_name: "notes",
+ persona: persona
+ );
+
+ unowned NoteDetails? details = persona as NoteDetails;
+ foreach (var note in details.notes) {
+ this.notes.add (note.value);
+ this.parameters.add (note.parameters);
+ }
+ }
+
+ 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 widget) {
+ for (int i = 0; i < notes.size; i++)
+ add_field (this.notes[i], widget);
+ }
+
+ protected virtual void add_field (string note, PropertyWidget widget) {
+ var type_label = row.create_type_label (_("Note"));
+ var value_label = row.create_value_label (note);
+ widget.add_row (type_label, value_label);
+ }
+}
+
+public class Contacts.EditableNotesField : NotesField, EditableProperty {
+
+ public EditableNotesField (Persona persona) {
+ base (persona);
+ }
+
+ public EditableNotesField.empty () {
+ Object (
+ property_name: "note-addresses",
+ persona: null
+ );
+
+ this.notes.add ("");
+ this.parameters.add (null);
+ }
+
+ protected override void add_field (string note, PropertyWidget widget) {
+ var type_label = row.create_type_label (_("Note"));
+ var textview_container = row.create_value_textview (note);
+ /* XXX entry.changed.connect ((editable) => { */
+ /* var row_nr = row.get_child_row ((Widget) editable); */
+ /* this.urls[row_nr] = editable.get_chars (); */
+ /* }); */
+ var delete_button = row.create_delete_button (_("Remove note"));
+
+ widget.add_row (type_label, textview_container, delete_button);
+ }
+
+ public Value? create_value () {
+ if (this.notes.is_empty)
+ return null;
+
+ var new_details = create_set ();
+ // 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_set ();
+ yield ((NoteDetails) this.persona).change_notes (new_addrs);
+ }
+
+ private HashSet<NoteFieldDetails>? create_set () {
+ var new_details = new HashSet<NoteFieldDetails> ();
+ for (int i = 0; i < this.notes.size; i++) {
+ if (this.notes[i] == "")
+ continue;
+
+ var note = new NoteFieldDetails (this.notes[i], this.parameters[i]);
+ new_details.add (note);
+ }
+
+ return new_details;
+ }
+}
+
+public class Contacts.PostalAddressesField : PropertyField {
+
+ protected ArrayList<TypeDescriptor> types = new ArrayList<TypeDescriptor> ();
+ protected ArrayList<PostalAddress> addresses = new ArrayList<PostalAddress> ();
+ protected ArrayList<unowned MultiMap<string, string>?> parameters
+ = new ArrayList<unowned MultiMap<string, string>?> ();
+
+ public PostalAddressesField (Persona persona) {
+ Object (
+ property_name: "postal-addresses",
+ persona: persona
+ );
+
+ unowned PostalAddressDetails? details = persona as PostalAddressDetails;
+ foreach (var address in details.postal_addresses) {
+ var type_descr = TypeSet.general.lookup_descriptor_for_field_details (address);
+ this.types.add (type_descr);
+ this.addresses.add (address.value);
+ this.parameters.add (address.parameters);
+ }
+ }
+
+ 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 widget) {
+ for (int i = 0; i < addresses.size; i++)
+ add_field (this.types[i], this.addresses[i], widget);
+ }
+
+ protected virtual void add_field (TypeDescriptor type, PostalAddress address, PropertyWidget widget) {
+ var type_label = row.create_type_label (type.display_name);
+ var value_label = row.create_value_label (format_address (address));
+ 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.EditablePostalAddressesField : PostalAddressesField, 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 EditablePostalAddressesField (Persona persona) {
+ base (persona);
+ }
+
+ public EditablePostalAddressesField.empty () {
+ Object (
+ property_name: "postal-addresses",
+ persona: null
+ );
+
+ this.types.add (TypeSet.general.lookup_descriptor_in_store ("Home"));
+ this.addresses.add (new PostalAddress ("", "", "", "", "", "", "", "", ""));
+ this.parameters.add (null);
+ }
+
+ protected override void add_field (TypeDescriptor type, PostalAddress address, PropertyWidget widget) {
+ var type_combo = row.create_type_combo (TypeSet.general, type);
+ type_combo.changed.connect ((combo) => {
+ var row_nr = row.get_child_row (combo);
+ this.types[row_nr] = 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;
+ address.get (address_part, out part);
+
+ var part_entry = 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 = row.create_delete_button (_("Remove postal address"));
+
+ widget.add_row (type_combo, grid, delete_button);
+ }
+
+ public Value? create_value () {
+ if (this.addresses.is_empty)
+ return null;
+
+ var new_details = create_set ();
+ // 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_set ();
+ yield ((PostalAddressDetails) this.persona).change_postal_addresses (new_addrs);
+ }
+
+ private HashSet<PostalAddressFieldDetails>? create_set () {
+ var new_details = new HashSet<PostalAddressFieldDetails> ();
+ for (int i = 0; i < this.addresses.size; i++) {
+ if (is_empty_postal_address (this.addresses[i]))
+ continue;
+
+ var parameters = this.types[i].add_type_to_parameters (this.parameters[i]);
+ var address = new PostalAddressFieldDetails (this.addresses[i], parameters);
+ new_details.add (address);
+ }
+
+ return new_details;
+ }
+
+ private bool is_empty_postal_address (PostalAddress addr) {
+ return addr.po_box == "" &&
+ addr.extension == "" &&
+ addr.street == "" &&
+ addr.locality == "" &&
+ addr.region == "" &&
+ addr.postal_code == "" &&
+ addr.country == "" &&
+ addr.address_format == "";
+ }
+}
diff --git a/src/contacts-contact-form.vala b/src/contacts-contact-form.vala
index f44a2fb..28dd679 100644
--- a/src/contacts-contact-form.vala
+++ b/src/contacts-contact-form.vala
@@ -49,42 +49,117 @@ public abstract class Contacts.ContactForm : Grid {
[GtkChild]
protected Grid container_grid;
- protected int last_row = 0;
+
+ [GtkChild]
+ protected ListBox form_container;
+ protected GLib.ListStore fields = new GLib.ListStore (typeof (PropertyField));
+
+ 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.fields, create_row);
+ this.form_container.set_header_func (create_persona_store_header);
+ }
+
+ private Gtk.Widget create_row (Object object) {
+ return ((PropertyField) object).create_row (labels_sizegroup, values_sizegroup, 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.field.persona == null || previous.field.persona == null)
+ return;
+ if (current.field.persona == previous.field.persona)
+ return;
+
+ var label = create_persona_store_label (current.field.persona);
+ row.set_header (label);
+ }
- if (b == prop)
- return 1;
- }
+ private static int compare_fields (Object obj_a, Object obj_b) {
+ unowned PropertyField a = (PropertyField) obj_a;
+ unowned PropertyField b = (PropertyField) obj_b;
- return 0;
- };
+ // First compare personas
+ var persona_comparison = Utils.compare_personas_on_store (a.persona, b.persona);
+ if (persona_comparison != 0)
+ return persona_comparison;
- var sorted_props = new ArrayList<string> ();
- foreach (var s in props)
- sorted_props.add (s);
+ // 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;
- sorted_props.sort ((owned) compare_properties);
- return sorted_props.to_array ();
+ // Next, check for the VCard PREF attribute
+ // XXX TODO
+ 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;
+
+ if (b == prop)
+ return 1;
+ }
+
+ 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 PropertyField? get_property_field_for_name (string name) {
+ for (uint i = 0; i < this.fields.get_n_items(); i++) {
+ var field = (PropertyField) this.fields.get_item (i);
+ if (field.property_name == name)
+ return field;
+ }
+
+ return null;
+ }
+
+ protected void add_field (PropertyField field) {
+ this.fields.insert_sorted (field, compare_fields);
+ }
+
+ 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..370cdd9 100644
--- a/src/contacts-contact-pane.vala
+++ b/src/contacts-contact-pane.vala
@@ -221,10 +221,19 @@ 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) {
+ editor.save_changes.begin ((obj, res) => {
+ try {
+ editor.save_changes.end (res);
+ } catch (Error e) {
+ show_message (e.message);
+ }
+ });
+ }
remove_contact_editor ();
@@ -234,35 +243,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 +253,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..7b11ea1 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 in ContactForm.SORTED_PROPERTIES) {
+ var field = add_row_for_property (p, prop);
+ if (field != null)
+ add_field (field);
}
}
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.fields.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 PropertyField? add_row_for_property (Persona persona, string property_name) {
+ switch (property_name) {
case "email-addresses":
- add_emails (persona);
+ if (EmailsField.should_show (persona))
+ return new EmailsField (persona);
break;
case "phone-numbers":
- add_phone_nrs (persona);
- break;
- case "im-addresses":
- add_im_addresses (persona);
+ if (PhoneNrsField.should_show (persona))
+ return new PhoneNrsField (persona);
break;
case "urls":
- add_urls (persona);
+ if (UrlsField.should_show (persona))
+ return new UrlsField (persona);
break;
case "nickname":
- add_nickname (persona);
+ if (NicknameField.should_show (persona))
+ return new NicknameField (persona);
break;
case "birthday":
- add_birthday (persona);
+ if (BirthdayField.should_show (persona))
+ return new BirthdayField (persona);
break;
case "notes":
- add_notes (persona);
+ if (NotesField.should_show (persona))
+ return new NotesField (persona);
break;
case "postal-addresses":
- add_postal_addresses (persona);
+ if (PostalAddressesField.should_show (persona))
+ return new PostalAddressesField (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-type-descriptor.vala b/src/contacts-type-descriptor.vala
index efc96ce..5e1cfcc 100644
--- a/src/contacts-type-descriptor.vala
+++ b/src/contacts-type-descriptor.vala
@@ -89,26 +89,36 @@ 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) {
+ foreach (var val in old_parameters["type"]) {
+ if (val.ascii_casecmp ("PREF") == 0) {
+ has_pref = true;
+ break;
+ }
}
- }
- // 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
@@ -130,8 +140,7 @@ 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;
}
/**
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..5231505 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -80,6 +80,7 @@ contacts_vala_sources = files(
'contacts-avatar-selector.vala',
'contacts-contact-editor.vala',
'contacts-contact-form.vala',
+ 'contacts-contact-form-field.vala',
'contacts-contact-list.vala',
'contacts-contact-pane.vala',
'contacts-contact-sheet.vala',
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]