contacts-contact-editor.vala 30 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
/*
 * Copyright (C) 2011 Alexander Larsson <alexl@redhat.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

using Gtk;
using Folks;
using Gee;

22
public class Contacts.AddressEditor : Box {
23
  public Entry? entries[7];  /* must be the number of elements in postal_element_props */
24 25
  public PostalAddressFieldDetails details;

26
  public const string[] postal_element_props = {"street", "extension", "locality", "region", "postal_code", "po_box", "country"};
27 28
  public static string[] postal_element_names = {_("Street"), _("Extension"), _("City"), _("State/Province"), _("Zip/Postal Code"), _("PO box"), _("Country")};

29 30 31 32 33 34 35 36 37 38
  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;
39
      details.value.get (AddressEditor.postal_element_props[i], out postal_part);
40 41 42

      entries[i] = new Entry ();
      entries[i].set_hexpand (true);
43
      entries[i].set ("placeholder-text", AddressEditor.postal_element_names[i]);
44 45 46 47 48 49 50 51 52 53 54 55

      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 ();
	});
    }
  }
56 57 58 59

  public override void grab_focus () {
    entries[0].grab_focus ();
  }
60 61
}

62 63 64
/**
 * A widget that allows the user to edit a given {@link Contact}.
 */
65
[GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-contact-editor.ui")]
66
public class Contacts.ContactEditor : ContactForm {
67 68 69 70 71 72 73

  private const string[] DEFAULT_PROPS_NEW_CONTACT = {
    "email-addresses.personal",
    "phone-numbers.cell",
    "postal-addresses.home"
  };

74
  private weak Widget focus_widget;
75

76 77
  private Entry name_entry;

78
  private Avatar avatar;
79

80 81 82 83
  [GtkChild]
  private MenuButton add_detail_button;

  [GtkChild]
84 85
  public Button linked_button;

86
  [GtkChild]
87 88
  public Button remove_button;

89
  public struct PropertyData {
90
    Persona? persona;
91 92 93 94
    Value value;
  }

  struct RowData {
95
    AbstractFieldDetails details;
96 97 98 99 100 101 102
  }

  struct Field {
    bool changed;
    HashMap<int, RowData?> rows;
  }

103
  /* the key of the hash_map is the uid of the persona */
104
  private HashMap<string, HashMap<string, Field?>> writable_personas;
105

106 107 108 109 110 111 112 113 114 115 116 117
  public bool has_birthday_row {
    get; private set; default = false;
  }

  public bool has_nickname_row {
    get; private set; default = false;
  }

  public bool has_notes_row {
    get; private set; default = false;
  }

118 119
  construct {
    this.writable_personas = new HashMap<string, HashMap<string, Field?>> ();
120
    this.container_grid.size_allocate.connect(on_container_grid_size_allocate);
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
  }

  public ContactEditor (Contact? contact, Store store, GLib.ActionGroup editor_actions) {
    this.store = store;
    this.contact = contact;

    this.add_detail_button.get_popover ().insert_action_group ("edit", editor_actions);

    if (contact != null) {
      this.remove_button.sensitive = contact.can_remove_personas ();
      this.linked_button.sensitive = contact.individual.personas.size > 1;
    } else {
      this.remove_button.hide ();
      this.linked_button.hide ();
    }

    create_avatar_button ();
    create_name_entry ();

    if (contact != null)
      fill_in_contact ();
    else
      fill_in_empty ();

145
    this.container_grid.show_all ();
146 147 148 149 150 151 152 153 154 155
  }

  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) {
156
        this.container_grid.attach (create_persona_store_label (p), 0, i, 2);
157 158 159
        last_store_position = ++i;
      }

160
      var rw_props = sort_persona_properties (p.writeable_properties);
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
      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 ();
      }
    }
  }

  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;
  }

192 193 194 195
  Value get_value_from_emails (HashMap<int, RowData?> rows) {
    var new_details = new HashSet<EmailFieldDetails>();

    foreach (var row_entry in rows.entries) {
196 197
      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;
198 199 200 201 202

      /* Ignore empty entries. */
      if (entry.get_text () == "")
        continue;

203
      combo.active_descriptor.save_to_field_details (row_entry.value.details);
204 205 206 207 208 209 210 211 212 213 214 215 216
      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) {
217 218
      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;
219 220 221 222 223

      /* Ignore empty entries. */
      if (entry.get_text () == "")
        continue;

224
      combo.active_descriptor.save_to_field_details (row_entry.value.details);
225 226 227 228 229 230 231 232 233 234 235 236
      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) {
237
      var entry = container_grid.get_child_at (1, row_entry.key) as Entry;
238 239 240 241 242

      /* Ignore empty entries. */
      if (entry.get_text () == "")
        continue;

243 244 245 246 247 248 249 250 251 252 253
      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) {
254
      var entry = container_grid.get_child_at (1, row_entry.key) as Entry;
255 256 257 258 259

      /* Ignore empty entries. */
      if (entry.get_text () == "")
        continue;

260 261 262 263 264 265 266 267
      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) {
268
      var box = container_grid.get_child_at (1, row_entry.key) as Grid;
269 270
      var day_spin  = box.get_child_at (0, 0) as SpinButton;
      var combo  = box.get_child_at (1, 0) as ComboBoxText;
271
      var year_spin  = box.get_child_at (2, 0) as SpinButton;
272

273
      var bday = new DateTime.local (year_spin.get_value_as_int (),
274
				     combo.get_active () + 1,
275
				     day_spin.get_value_as_int (),
276 277 278 279 280 281 282 283 284 285 286 287
				     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) {
288
      var text = (container_grid.get_child_at (1, row_entry.key) as Bin).get_child () as TextView;
289 290 291 292
      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);
293 294 295 296
      if (value != "") {
        var details = new NoteFieldDetails (value, row_entry.value.details.parameters);
        new_details.add (details);
      }
297 298 299 300 301 302
    }
    var new_value = Value (new_details.get_type ());
    new_value.set_object (new_details);
    return new_value;
  }

303 304 305 306
  Value get_value_from_addresses (HashMap<int, RowData?> rows) {
    var new_details = new HashSet<PostalAddressFieldDetails>();

    foreach (var row_entry in rows.entries) {
307 308
      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;
309
      combo.active_descriptor.save_to_field_details (row_entry.value.details);
310 311 312 313 314 315 316 317 318

      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,
319
					 addr_editor.details.id);
320
      for (int i = 0; i < addr_editor.entries.length; i++)
321
	new_value.set (AddressEditor.postal_element_props[i], addr_editor.entries[i].get_text ());
322 323 324 325 326 327 328 329 330

      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;
  }

331 332 333 334 335 336 337 338 339 340 341 342 343 344
  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;
	}
      }
    }
  }

345
  new void remove_row (int row) {
346 347 348 349
    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) {
350
	    var child = container_grid.get_child_at (0, row);
351
	    child.destroy ();
352
	    child = container_grid.get_child_at (1, row);
353
	    child.destroy ();
354
	    child = container_grid.get_child_at (3, row);
355 356 357 358 359 360 361 362 363 364 365
	    child.destroy ();

	    field_entry.value.changed = true;
	    field_entry.value.rows.unset (row);
	    return;
	  }
	}
      }
    }
  }

366
  void attach_row_with_entry (int row, TypeSet type_set, AbstractFieldDetails details, string value, string? type = null) {
367 368
    var combo = new TypeCombo (type_set);
    combo.set_hexpand (false);
369
    combo.set_active_from_field_details (details);
370
    if (type != null)
371
      combo.set_active_from_vcard_type (type);
372
    combo.set_valign (Align.CENTER);
373
    container_grid.attach (combo, 0, row, 1, 1);
374 375 376 377

    var value_entry = new Entry ();
    value_entry.set_text (value);
    value_entry.set_hexpand (true);
378
    container_grid.attach (value_entry, 1, row, 1, 1);
379

380 381 382 383 384 385
    if (type_set == TypeSet.email) {
      value_entry.placeholder_text = _("Add email");
    } else if (type_set == TypeSet.phone) {
      value_entry.placeholder_text = _("Add number");
    }

386
    var delete_button = new Button.from_icon_name ("user-trash-symbolic", IconSize.MENU);
387
    delete_button.get_accessible ().set_name (_("Delete field"));
388
    container_grid.attach (delete_button, 3, row, 1, 1);
389 390

    /* Notify change to upper layer */
391
    combo.changed.connect ((c) => {
392
	set_field_changed (get_current_row (combo));
393 394
      });
    value_entry.changed.connect (() => {
395
	set_field_changed (get_current_row (value_entry));
396 397
      });
    delete_button.clicked.connect (() => {
398
	remove_row (get_current_row (delete_button));
399
      });
400

401 402
    if (value == "")
      focus_widget = value_entry;
403 404 405 406 407
  }

  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);
408
    title_label.set_halign (Align.START);
409
    title_label.margin_end = 6;
410
    container_grid.attach (title_label, 0, row, 1, 1);
411 412 413 414

    var value_entry = new Entry ();
    value_entry.set_text (value);
    value_entry.set_hexpand (true);
415
    container_grid.attach (value_entry, 1, row, 1, 1);
416

417
    var delete_button = new Button.from_icon_name ("user-trash-symbolic", IconSize.MENU);
418
    delete_button.get_accessible ().set_name (_("Delete field"));
419
    container_grid.attach (delete_button, 3, row, 1, 1);
420 421 422

    /* Notify change to upper layer */
    value_entry.changed.connect (() => {
423
	set_field_changed (get_current_row (value_entry));
424
      });
425
    delete_button.clicked.connect_after (() => {
426
	remove_row (get_current_row (delete_button));
427
      });
428

429 430
    if (value == "")
      focus_widget = value_entry;
431 432 433 434 435
  }

  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);
436
    title_label.set_halign (Align.START);
437 438
    title_label.set_valign (Align.START);
    title_label.margin_top = 3;
439
    title_label.margin_end = 6;
440
    container_grid.attach (title_label, 0, row, 1, 1);
441 442 443 444 445 446 447 448

    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);
449
    container_grid.attach (sw, 1, row, 1, 1);
450

451
    var delete_button = new Button.from_icon_name ("user-trash-symbolic", IconSize.MENU);
452
    delete_button.get_accessible ().set_name (_("Delete field"));
453
    delete_button.set_valign (Align.START);
454
    container_grid.attach (delete_button, 3, row, 1, 1);
455 456 457

    /* Notify change to upper layer */
    value_text.get_buffer ().changed.connect (() => {
Thiago Mendes's avatar
Thiago Mendes committed
458
	set_field_changed (get_current_row (sw));
459 460
      });
    delete_button.clicked.connect (() => {
461
	remove_row (get_current_row (delete_button));
462 463
	/* eventually will need to check against the details type */
	has_notes_row = false;
464
      });
465

466 467
    if (value == "")
      focus_widget = value_text;
468 469
  }

470 471
  delegate void AdjustingDateFn();

472 473 474
  void attach_row_for_birthday (string title, AbstractFieldDetails? details, DateTime birthday, int row) {
    var title_label = new Label (title);
    title_label.set_hexpand (false);
475
    title_label.set_halign (Align.START);
476
    title_label.margin_end = 6;
477
    container_grid.attach (title_label, 0, row, 1, 1);
478 479 480 481 482 483 484 485

    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 ());

486 487 488 489 490 491 492 493
    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;
494

495 496 497 498 499
    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 ());

500
    box.add (day_spin);
501
    box.add (month_combo);
502
    box.add (year_spin);
503

504
    container_grid.attach (box, 1, row, 1, 1);
505

506
    var delete_button = new Button.from_icon_name ("user-trash-symbolic", IconSize.MENU);
507
    delete_button.get_accessible ().set_name (_("Delete field"));
508
    container_grid.attach (delete_button, 3, row, 1, 1);
509

510 511
    AdjustingDateFn fn = () => {
      int[] month_of_31 = {3, 5, 8, 10};
512 513 514 515 516 517 518 519 520
      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);
        }
521 522 523
      }
    };

524 525
    /* Notify change to upper layer */
    day_spin.changed.connect (() => {
526
        set_field_changed (get_current_row (day_spin));
527
      });
528 529
    month_combo.changed.connect (() => {
        set_field_changed (get_current_row (month_combo));
530

531 532
        /* adjusting day_spin value using selected month constraints*/
        fn ();
533 534
      });
    year_spin.changed.connect (() => {
535
        set_field_changed (get_current_row (year_spin));
536

537
        fn ();
538 539
      });
    delete_button.clicked.connect (() => {
540 541
        remove_row (get_current_row (delete_button));
        has_birthday_row = false;
542 543 544
      });
  }

545
  void attach_row_for_address (int row, TypeSet type_set, PostalAddressFieldDetails details, string? type = null) {
546 547
    var combo = new TypeCombo (type_set);
    combo.set_hexpand (false);
548
    combo.set_active_from_field_details (details);
549
    if (type != null)
550
      combo.set_active_from_vcard_type (type);
551
    container_grid.attach (combo, 0, row, 1, 1);
552 553

    var value_address = new AddressEditor (details);
554
    container_grid.attach (value_address, 1, row, 1, 1);
555

556
    var delete_button = new Button.from_icon_name ("user-trash-symbolic", IconSize.MENU);
557
    delete_button.get_accessible ().set_name (_("Delete field"));
558
    delete_button.set_valign (Align.START);
559
    container_grid.attach (delete_button, 3, row, 1, 1);
560 561 562

    /* Notify change to upper layer */
    combo.changed.connect (() => {
563
	set_field_changed (get_current_row (combo));
564 565
      });
    value_address.changed.connect (() => {
566
	set_field_changed (get_current_row (value_address));
567 568
      });
    delete_button.clicked.connect (() => {
569
	remove_row (get_current_row (delete_button));
570
      });
571

572
    focus_widget = value_address;
573 574
  }

575
  void add_edit_row (Persona? p, string prop_name, ref int row, bool add_empty = false, string? type = null) {
576 577
    /* Here, we will need to add manually every type of field,
     * we're planning to allow editing on */
578
    string persona_uid = p != null ? p.uid : "null-persona.hack";
579 580 581 582 583
    switch (prop_name) {
    case "email-addresses":
      var rows = new HashMap<int, RowData?> ();
      if (add_empty) {
	var detail_field = new EmailFieldDetails ("");
584
	attach_row_with_entry (row, TypeSet.email, detail_field, "", type);
585 586 587 588 589 590 591
	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) {
592
	    attach_row_with_entry (row, TypeSet.email, email, email.value);
593 594 595 596 597 598
	    rows.set (row, { email });
	    row++;
	  }
	}
      }
      if (! rows.is_empty) {
599
	if (writable_personas[persona_uid].has_key (prop_name)) {
600
	  foreach (var entry in rows.entries) {
601
	    writable_personas[persona_uid][prop_name].rows.set (entry.key, entry.value);
602 603
	  }
	} else {
604
	  writable_personas[persona_uid].set (prop_name, { false, rows });
605 606 607 608 609 610 611
	}
      }
      break;
    case "phone-numbers":
      var rows = new HashMap<int, RowData?> ();
      if (add_empty) {
	var detail_field = new PhoneFieldDetails ("");
612
	attach_row_with_entry (row, TypeSet.phone, detail_field, "", type);
613 614 615 616 617 618 619
	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) {
620
	    attach_row_with_entry (row, TypeSet.phone, phone, phone.value, type);
621 622 623 624 625 626
	    rows.set (row, { phone });
	    row++;
	  }
	}
      }
      if (! rows.is_empty) {
627
	if (writable_personas[persona_uid].has_key (prop_name)) {
628
	  foreach (var entry in rows.entries) {
629
	    writable_personas[persona_uid][prop_name].rows.set (entry.key, entry.value);
630 631
	  }
	} else {
632
	  writable_personas[persona_uid].set (prop_name, { false, rows });
633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653
	}
      }
      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) {
654
	if (writable_personas[persona_uid].has_key (prop_name)) {
655
	  foreach (var entry in rows.entries) {
656
	    writable_personas[persona_uid][prop_name].rows.set (entry.key, entry.value);
657 658
	  }
	} else {
659
	  writable_personas[persona_uid].set (prop_name, { false, rows });
660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679
	}
      }
      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) {
680
	has_nickname_row = true;
681
	var delete_button = container_grid.get_child_at (3, row - 1) as Button;
682 683 684 685
	delete_button.clicked.connect (() => {
	    has_nickname_row = false;
	  });

686
	if (writable_personas[persona_uid].has_key (prop_name)) {
687
	  foreach (var entry in rows.entries) {
688
	    writable_personas[persona_uid][prop_name].rows.set (entry.key, entry.value);
689 690
	  }
	} else {
691
	  writable_personas[persona_uid].set (prop_name, { false, rows });
692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712
	}
      }
      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) {
713
	has_birthday_row = true;
714
	writable_personas[persona_uid].set (prop_name, { add_empty, rows });
715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734
      }
      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) {
735
	has_notes_row = true;
736
	if (writable_personas[persona_uid].has_key (prop_name)) {
737
	  foreach (var entry in rows.entries) {
738
	    writable_personas[persona_uid][prop_name].rows.set (entry.key, entry.value);
739 740
	  }
	} else {
741
	  writable_personas[persona_uid].set (prop_name, { false, rows });
742 743 744
	}
      }
      break;
745 746 747 748 749 750 751 752 753 754 755 756 757
    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));
758
	attach_row_for_address (row, TypeSet.general, detail_field, type);
759 760 761 762 763 764
	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) {
765
	    attach_row_for_address (row, TypeSet.general, addr, type);
766 767 768 769 770 771
	    rows.set (row, { addr });
	    row++;
	  }
	}
      }
      if (! rows.is_empty) {
772
	if (writable_personas[persona_uid].has_key (prop_name)) {
773
	  foreach (var entry in rows.entries) {
774
	    writable_personas[persona_uid][prop_name].rows.set (entry.key, entry.value);
775 776
	  }
	} else {
777
	  writable_personas[persona_uid].set (prop_name, { false, rows });
778 779 780
	}
      }
      break;
781 782 783
    }
  }

784 785 786 787 788 789 790
  int get_current_row (Widget child) {
    int row;

    container_grid.child_get (child, "top-attach", out row);
    return row;
  }

791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806
  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;
	  }
	}
      }
    }
807 808 809 810 811 812 813 814 815 816 817 818 819 820
    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;
	  }
	}
      }
    }
821
    container_grid.insert_row (idx);
822 823
  }

824
  private void on_container_grid_size_allocate (Allocation alloc) {
825 826 827
    if (this.focus_widget != null && this.focus_widget is Widget) {
      this.focus_widget.grab_focus ();
      this.focus_widget = null;
828 829 830
    }
  }

831 832 833 834 835
  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) {
836
	if (field_entry.value.changed && !props_set.has_key (field_entry.key)) {
837
	  PropertyData p = PropertyData ();
838 839 840 841
	  p.persona = null;
	  if (contact != null) {
	    p.persona = contact.find_persona_from_uid (entry.key);
	  }
842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861

	  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;
862 863 864
            case "postal-addresses":
	      p.value = get_value_from_addresses (field_entry.value.rows);
	      break;
865 866 867 868 869 870 871 872 873 874
	  }

	  props_set.set (field_entry.key, p);
	}
      }
    }

    return props_set;
  }

875
  public void add_new_row_for_property (Persona? p, string prop_name, string? type = null) {
876
    /* Somehow, I need to ensure that p is the main/default/first persona */
877 878 879
    Persona persona = null;
    if (contact != null) {
      if (p == null) {
880 881
        persona = new FakePersona (this.store, contact);
        writable_personas[persona.uid] = new HashMap<string, Field?> ();
882
      } else {
883
        persona = p;
884
      }
885 886
    }

887 888 889 890 891 892 893 894 895 896 897 898
    int next_idx = 0;
    foreach (var fields in writable_personas.values) {
      if (fields.has_key (prop_name)) {
	  foreach (var idx in fields[prop_name].rows.keys) {
	    if (idx < last_row)
	      next_idx = idx > next_idx ? idx : next_idx;
	  }
	  break;
      }
    }
    next_idx = (next_idx == 0 ? last_row : next_idx) + 1;
    insert_row_at (next_idx);
899
    add_edit_row (persona, prop_name, ref next_idx, true, type);
900
    last_row++;
901
    container_grid.show_all ();
902
  }
903

904 905
  // Creates the contact's current avatar in a big button on top of the Editor
  private void create_avatar_button () {
906
    this.avatar = new Avatar (PROFILE_SIZE, this.contact);
907 908 909 910 911

    var button = new Button ();
    button.get_accessible ().set_name (_("Change avatar"));
    button.image = this.avatar;
    button.clicked.connect (on_avatar_button_clicked);
912

913
    this.container_grid.attach (button, 0, 0, 1, 3);
914
  }
915

916
  // Show the avatar popover when the avatar is clicked
917
  private void on_avatar_button_clicked (Button avatar_button) {
918 919
    var popover = new AvatarSelector (avatar_button, this.contact);
    popover.set_avatar.connect ( (icon) =>  {
920 921
        this.avatar.set_data ("value", icon);
        this.avatar.set_data ("changed", true);
922 923 924 925 926 927 928 929

        Gdk.Pixbuf? a_pixbuf = null;
        try {
          var stream = (icon as LoadableIcon).load (PROFILE_SIZE, null);
          a_pixbuf = new Gdk.Pixbuf.from_stream_at_scale (stream, PROFILE_SIZE, PROFILE_SIZE, true);
        } catch {
        }

930
        this.avatar.set_pixbuf (a_pixbuf);
931
      });
932
    popover.show();
933 934 935
  }

  public bool avatar_changed () {
936
    return this.avatar.get_data<bool> ("changed");
937
  }
938

939
  public Value get_avatar_value () {
940
    GLib.Icon icon = this.avatar.get_data<GLib.Icon> ("value");
941 942 943 944 945 946 947 948 949 950 951 952 953 954
    Value v = Value (icon.get_type ());
    v.set_object (icon);
    return v;
  }

  // Creates the big name entry on the top
  private void create_name_entry () {
    this.name_entry = new Entry ();
    this.name_entry.hexpand = true;
    this.name_entry.valign = Align.CENTER;
    this.name_entry.placeholder_text = _("Add name");
    this.name_entry.set_data ("changed", false);

    if (this.contact != null)
955
        this.name_entry.text = this.contact.individual.display_name;
956 957

    /* structured name change */
958 959
    this.name_entry.changed.connect (() => {
        this.name_entry.set_data ("changed", true);
960 961
      });

962 963
    this.container_grid.attach (this.name_entry, 1, 0, 3, 3);
  }
964

965 966 967 968 969 970 971 972
  public bool name_changed () {
    return this.name_entry.get_data<bool> ("changed");
  }

  public Value get_full_name_value () {
    Value v = Value (typeof (string));
    v.set_string (this.name_entry.get_text ());
    return v;
973
  }
974
}