/* * Copyright (C) 2011 Alexander Larsson * * 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 . */ using Gtk; using Folks; using Gee; /** * The ContactList is the actual list of {@link Contact}s that the user sees on * the left. It is contained by the {@link ListPane}, which also provides other * functionality, such as an action bar. */ public class Contacts.ContactList : ListBox { private class ContactDataRow : ListBoxRow { private const int LIST_AVATAR_SIZE = 48; public Contact contact; private Label label; private Avatar avatar; public CheckButton selector_button; // Whether the selector should always be visible (or only on hover) private bool checkbox_exposed = false; public ContactDataRow(Contact c) { this.contact = c; this.contact.changed.connect (on_contact_changed); get_style_context (). add_class ("contact-data-row"); Grid grid = new Grid (); grid.margin = 3; grid.margin_start = 9; grid.set_column_spacing (10); this.avatar = new Avatar (LIST_AVATAR_SIZE, this.contact); this.label = new Label (c.individual.display_name); this.label.ellipsize = Pango.EllipsizeMode.END; this.label.valign = Align.CENTER; this.label.halign = Align.START; // Make sure it doesn't "twitch" when the checkbox becomes visible this.label.xalign = 0; this.selector_button = new CheckButton (); this.selector_button.visible = false; this.selector_button.valign = Align.CENTER; this.selector_button.halign = Align.END; this.selector_button.hexpand = true; // Make sure it doesn't overlap with the scrollbar this.selector_button.margin_end = 12; grid.attach (this.avatar, 0, 0); grid.attach (this.label, 1, 0); grid.attach (this.selector_button, 2, 0); this.add (grid); this.show_all (); } private void on_contact_changed () { this.label.set_text (this.contact.individual.display_name); changed (); } // Sets whether the checbox should always be shown (and not only on hover) public void expose_checkbox (bool expose) { this.checkbox_exposed = expose; var hovering = StateFlags.PRELIGHT in get_state_flags (); this.selector_button.visible = expose || hovering; } // Normally, we would use the (enter/leave)_notify_event here, but since ListBoxRow // doesn't have its own Gdk.Window, this won't work (at least in GTK+3). public override void state_flags_changed (StateFlags previous_state) { var hovering_now = StateFlags.PRELIGHT in get_state_flags (); var was_hovering = StateFlags.PRELIGHT in previous_state; if (hovering_now != was_hovering) // If hovering changed this.selector_button.visible = checkbox_exposed || hovering_now; } } public signal void selection_changed (Contact? contact); public signal void contacts_marked (int contacts_marked); int nr_contacts_marked = 0; private Query filter_query; private Store store; private Settings settings; public UiState state { get; set; } public ContactList (Settings settings, Store store, Query query) { this.selection_mode = Gtk.SelectionMode.BROWSE; this.store = store; this.filter_query = query; this.filter_query.notify.connect (() => { invalidate_filter (); }); this.visible = true; this.notify["state"].connect (on_ui_state_changed); this.settings = settings; this.settings.changed["sort-on-surname"].connect(invalidate_sort); this.store.added.connect (contact_added_cb); this.store.removed.connect (contact_removed_cb); foreach (var c in this.store.get_contacts ()) contact_added_cb (this.store, c); get_style_context ().add_class ("contacts-contact-list"); set_sort_func (compare_rows); set_filter_func (filter_row); set_header_func (update_header); } private void on_ui_state_changed (Object obj, ParamSpec pspec) { foreach (var widget in get_children ()) { var row = widget as ContactDataRow; row.expose_checkbox (this.state == UiState.SELECTING); if (this.state != UiState.SELECTING) row.selector_button.active = false; } if (this.state != UiState.SELECTING) this.nr_contacts_marked = 0; } private int compare_rows (ListBoxRow row_a, ListBoxRow row_b) { var a = ((ContactDataRow) row_a).contact.individual; var b = ((ContactDataRow) row_b).contact.individual; // Always prefer favourites over non-favourites. if (a.is_favourite != b.is_favourite) return a.is_favourite? -1 : 1; // Both are (non-)favourites: sort by either first name or surname (user preference) unowned string? a_name = this.settings.sort_on_surname? try_get_surname(a) : a.display_name; unowned string? b_name = this.settings.sort_on_surname? try_get_surname(b) : b.display_name; return a_name.collate (b_name); } private unowned string try_get_surname (Individual indiv) { if (indiv.structured_name != null && indiv.structured_name.family_name != "") return indiv.structured_name.family_name; // Fall back to the display_name return indiv.display_name; } private void update_header (ListBoxRow row, ListBoxRow? before) { var current = ((ContactDataRow) row).contact.individual; if (before == null) { if (current.is_favourite) row.set_header (create_header_label (_("Favorites"))); else row.set_header (create_header_label (_("All Contacts"))); return; } var previous = ((ContactDataRow) before).contact.individual; if (!current.is_favourite && previous.is_favourite) { row.set_header (create_header_label (_("All Contacts"))); } else { row.set_header (null); } } private Label create_header_label (string text) { var label = new Label (text); label.halign = Align.START; label.margin = 3; label.margin_start = 6; label.margin_top = 6; var attrs = new Pango.AttrList (); attrs.insert (Pango.attr_weight_new (Pango.Weight.BOLD)); attrs.insert (Pango.attr_scale_new ((Pango.Scale.SMALL + Pango.Scale.MEDIUM) / 2.0)); attrs.insert (Pango.attr_foreground_alpha_new (30000)); label.attributes = attrs; return label; } private void contact_added_cb (Store store, Contact c) { var row = new ContactDataRow(c); row.selector_button.toggled.connect ( () => { on_row_checkbox_toggled (row); }); row.selector_button.visible = (this.state == UiState.SELECTING); add (row); } private void on_row_checkbox_toggled (ContactDataRow row) { this.nr_contacts_marked += (row.selector_button.active)? 1 : -1; // User selected a first checkbox: enter selection mode if (row.selector_button.active && this.nr_contacts_marked == 1) this.state = UiState.SELECTING; // User deselected the last checkbox: leave selection mode if (!row.selector_button.active && this.nr_contacts_marked == 0) this.state = UiState.SHOWING; contacts_marked (this.nr_contacts_marked); } private void contact_removed_cb (Store store, Contact c) { var row = find_row_for_contact (c); if (row != null) row.destroy (); } public override void row_selected (ListBoxRow? row) { var data = (ContactDataRow?) row as ContactDataRow; var contact = data != null ? data.contact : null; selection_changed (contact); #if HAVE_TELEPATHY if (contact != null) contact.fetch_contact_info (); #endif } private bool filter_row (ListBoxRow row) { var contact = ((ContactDataRow) row).contact; return !contact.is_hidden && this.filter_query.is_match (contact.individual) > 0; } public void select_contact (Contact? contact) { if (contact == null) { /* deselect */ select_row (null); return; } select_row (find_row_for_contact (contact)); } private ContactDataRow? find_row_for_contact (Contact contact) { foreach (var widget in get_children ()) { var row = ((ContactDataRow) widget); if (row.contact == contact) return row; } return null; } public LinkedList get_marked_contacts () { var cs = new LinkedList (); foreach (var widget in get_children ()) { var row = widget as ContactDataRow; if (row.selector_button.active) cs.add (row.contact); } return cs; } public override bool button_press_event (Gdk.EventButton event) { base.button_press_event (event); if (event.button == Gdk.BUTTON_SECONDARY) { var row = (ContactDataRow) get_row_at_y ((int) Math.round (event.y)); select_row (row); row.selector_button.active = (this.state != UiState.SELECTING) || !row.selector_button.active; } return false; } }