diff --git a/src/Makefile.am b/src/Makefile.am index 3fc3dfd..0d99c5c 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -19,11 +19,10 @@ AM_VALAFLAGS += -D HAVE_GSTREAMER @CONTACTS_GSTREAMER_PACKAGES@ AM_CPPFLAGS += $(CONTACTS_GSTREAMER_CFLAGS) endif -bin_PROGRAMS = gnome-contacts +bin_PROGRAMS = gnome-contacts test-sorted vala_sources = \ contacts-app.vala \ - contacts-cell-renderer-shape.vala \ contacts-contact.vala \ contacts-contact-pane.vala \ contacts-types.vala \ @@ -32,6 +31,7 @@ vala_sources = \ contacts-linking.vala \ contacts-menu-button.vala \ contacts-row.vala \ + contacts-sorted.vala \ contacts-store.vala \ contacts-view.vala \ contacts-utils.vala \ @@ -67,6 +67,13 @@ gnome_contacts_SOURCES = \ $(vala_sources) \ $(NULL) +test_sorted_SOURCES = \ + contacts-sorted.vala \ + test-sorted.vala \ + $(NULL) +test_sorted_LDADD = $(CONTACTS_LIBS) + + gnome_contacts_LDADD = $(CONTACTS_LIBS) -lm if USE_GSTREAMER diff --git a/src/contacts-app.vala b/src/contacts-app.vala index 5771396..ad9ad7d 100644 --- a/src/contacts-app.vala +++ b/src/contacts-app.vala @@ -59,7 +59,7 @@ public class Contacts.App : Gtk.Application { } private void selection_changed (Contact? new_selection) { - contacts_pane.show_contact (new_selection); + contacts_pane.show_contact (new_selection, false, false); } public void show_contact (Contact? contact) { diff --git a/src/contacts-avatar-dialog.vala b/src/contacts-avatar-dialog.vala index dce0f4a..1b46aab 100644 --- a/src/contacts-avatar-dialog.vala +++ b/src/contacts-avatar-dialog.vala @@ -266,7 +266,7 @@ public class Contacts.AvatarDialog : Dialog { grid.attach (main_frame, 0, 0, 1, 1); var label = new Label (""); - label.set_markup ("" + contact.display_name + ""); + label.set_markup (Markup.printf_escaped ("%s", contact.display_name)); label.set_valign (Align.START); label.set_halign (Align.START); label.set_hexpand (true); diff --git a/src/contacts-cell-renderer-shape.vala b/src/contacts-cell-renderer-shape.vala deleted file mode 100644 index 076c997..0000000 --- a/src/contacts-cell-renderer-shape.vala +++ /dev/null @@ -1,330 +0,0 @@ -/* -*- Mode: vala; indent-tabs-mode: t; c-basic-offset: 2; tab-width: 8 -*- */ -/* - * 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; - -public class Contacts.CellRendererShape : Gtk.CellRenderer { - public const int IMAGE_SIZE = 14; - - private Widget current_widget; - - public string name { get; set; } - public PresenceType presence { get; set; } - public string message { get; set; } - public bool is_phone { get; set; } - public bool show_presence { get; set; } - const int default_width = 60; - int renderer_height = Contact.SMALL_AVATAR_SIZE; - - private struct IconShape { - string icon; - } - - Gdk.Pixbuf? create_symbolic_pixbuf (Widget widget, string icon_name, int size) { - var screen = widget. get_screen (); - var icon_theme = Gtk.IconTheme.get_for_screen (screen); - - var info = icon_theme.lookup_icon (icon_name, size, Gtk.IconLookupFlags.USE_BUILTIN); - if (info == null) - return null; - - var context = widget.get_style_context (); - - context.save (); - bool is_symbolic; - Gdk.Pixbuf? pixbuf = null; - try { - pixbuf = info.load_symbolic_for_context (context, - out is_symbolic); - } catch (Error e) { - } - context.restore (); - - return pixbuf; - } - - private Pango.Layout get_name_layout (Widget widget, - Gdk.Rectangle? cell_area, - CellRendererState flags) { - Pango.Layout layout; - int xpad; - - var attr_list = new Pango.AttrList (); - - layout = widget.create_pango_layout (name); - - var attr = new Pango.AttrSize (13 * Pango.SCALE); - attr.absolute = 1; - attr.start_index = 0; - attr.end_index = attr.start_index + name.length; - attr_list.insert ((owned) attr); - - /* Now apply the attributes as they will effect the outcome - * of pango_layout_get_extents() */ - layout.set_attributes (attr_list); - - // We only look at xpad, and only use it for the left side... - get_padding (out xpad, null); - - layout.set_ellipsize (Pango.EllipsizeMode.END); - - Pango.Rectangle rect; - int width, text_width; - - layout.get_extents (null, out rect); - text_width = rect.width; - - if (cell_area != null) - width = (cell_area.width - xpad) * Pango.SCALE; - else - width = default_width * Pango.SCALE; - - width = int.min (width, text_width); - - layout.set_width (width); - - layout.set_wrap (Pango.WrapMode.WORD_CHAR); - layout.set_height (-2); - - Pango.Alignment align; - if (widget.get_direction () == TextDirection.RTL) - align = Pango.Alignment.RIGHT; - else - align = Pango.Alignment.LEFT; - layout.set_alignment (align); - - return layout; - } - - private Pango.Layout get_presence_layout (Widget widget, - Gdk.Rectangle? cell_area, - CellRendererState flags) { - Pango.Layout layout; - int xpad; - Pango.Rectangle r = { 0, -CellRendererShape.IMAGE_SIZE*1024*7/10, - CellRendererShape.IMAGE_SIZE*1024, CellRendererShape.IMAGE_SIZE*1024 }; - - var attr_list = new Pango.AttrList (); - - string? str = ""; - string? iconname = Contact.presence_to_icon (presence); - if (iconname != null && show_presence) { - str += "*"; - IconShape icon_shape = IconShape(); - icon_shape.icon = iconname; - var a = new Pango.AttrShape.with_data (r, r, icon_shape, (s) => { return s;} ); - a.start_index = 0; - a.end_index = 1; - attr_list.insert ((owned) a); - str += " "; - } - if (message != null && (!show_presence || iconname != null)) { - string m = message; - if (m.length == 0) - m = Contact.presence_to_string (presence); - - var attr = new Pango.AttrSize (9 * Pango.SCALE); - attr.absolute = 1; - attr.start_index = str.length; - attr.end_index = attr.start_index + m.length; - attr_list.insert ((owned) attr); - str += m; - - if (is_phone && show_presence) { - var icon_shape = IconShape(); - icon_shape.icon = "phone-symbolic"; - var a = new Pango.AttrShape.with_data (r, r, icon_shape, (s) => { return s;}); - a.start_index = str.length; - str += "*"; - a.end_index = str.length; - attr_list.insert ((owned) a); - } - } - - layout = widget.create_pango_layout (str); - - get_padding (out xpad, null); - - /* Now apply the attributes as they will effect the outcome - * of pango_layout_get_extents() */ - layout.set_attributes (attr_list); - - layout.set_ellipsize (Pango.EllipsizeMode.END); - - Pango.Rectangle rect; - int width, text_width; - - layout.get_extents (null, out rect); - text_width = rect.width; - - if (cell_area != null) - width = (cell_area.width - xpad) * Pango.SCALE; - else - width = default_width * Pango.SCALE; - - width = int.min (width, text_width); - - layout.set_width (width); - - layout.set_wrap (Pango.WrapMode.WORD_CHAR); - - layout.set_height (-1); - - Pango.Alignment align; - if (widget.get_direction () == TextDirection.RTL) - align = Pango.Alignment.RIGHT; - else - align = Pango.Alignment.LEFT; - layout.set_alignment (align); - - return layout; - } - - public override void get_size (Widget widget, - Gdk.Rectangle? cell_area, - out int x_offset, - out int y_offset, - out int width, - out int height) { - x_offset = y_offset = width = height = 0; - // Not used - } - - private void do_get_size (Widget widget, - Gdk.Rectangle? cell_area, - Pango.Layout? layout, - out int x_offset, - out int y_offset) { - Pango.Rectangle rect; - int xpad, ypad; - - get_padding (out xpad, out ypad); - - layout.get_pixel_extents (null, out rect); - - if (cell_area != null) { - rect.width = int.min (rect.width, cell_area.width - 2 * xpad); - - if (widget.get_direction () == TextDirection.RTL) - x_offset = cell_area.width - (rect.width + xpad); - else - x_offset = xpad; - - x_offset = int.max (x_offset, 0); - } else { - x_offset = 0; - } - - y_offset = ypad; - } - - public override void render (Cairo.Context cr, - Widget widget, - Gdk.Rectangle background_area, - Gdk.Rectangle cell_area, - CellRendererState flags) { - StyleContext context; - Pango.Layout name_layout, presence_layout; - int name_x_offset = 0; - int presence_x_offset = 0; - int name_y_offset = 0; - int presence_y_offset = 0; - int xpad; - Pango.Rectangle name_rect; - Pango.Rectangle presence_rect; - - current_widget = widget; - - context = widget.get_style_context (); - get_padding (out xpad, null); - - name_layout = get_name_layout (widget, cell_area, flags); - do_get_size (widget, cell_area, name_layout, out name_x_offset, out name_y_offset); - name_layout.get_pixel_extents (null, out name_rect); - name_x_offset = name_x_offset - name_rect.x; - - presence_layout = null; - if (name_layout.get_lines_readonly ().length () == 1) { - presence_layout = get_presence_layout (widget, cell_area, flags); - do_get_size (widget, cell_area, presence_layout, out presence_x_offset, out presence_y_offset); - presence_layout.get_pixel_extents (null, out presence_rect); - presence_x_offset = presence_x_offset - presence_rect.x; - } - - cr.save (); - - Gdk.cairo_rectangle (cr, cell_area); - cr.clip (); - - context.render_layout (cr, - cell_area.x + name_x_offset, - cell_area.y + name_y_offset, - name_layout); - - if (presence_layout != null) - context.render_layout (cr, - cell_area.x + presence_x_offset, - cell_area.y + presence_y_offset + renderer_height - 11 - presence_layout.get_baseline () / Pango.SCALE, - presence_layout); - - cr.restore (); - } - - public override void get_preferred_width (Widget widget, - out int min_width, - out int nat_width) { - int xpad; - - get_padding (out xpad, null); - - nat_width = min_width = xpad + default_width; - } - - public override void get_preferred_height_for_width (Widget widget, - int width, - out int minimum_height, - out int natural_height) { - int ypad; - - get_padding (null, out ypad); - minimum_height = renderer_height + ypad; - natural_height = renderer_height + ypad; - } - - public override void get_preferred_height (Widget widget, - out int minimum_size, - out int natural_size) { - int min_width; - - get_preferred_width (widget, out min_width, null); - get_preferred_height_for_width (widget, min_width, - out minimum_size, out natural_size); - } - - public void render_shape (Cairo.Context cr, Pango.AttrShape attr, bool do_path) { - unowned Pango.AttrShape sattr = (Pango.AttrShape)attr; - var pixbuf = create_symbolic_pixbuf (current_widget, sattr.data.icon, IMAGE_SIZE); - if (pixbuf != null) { - double x, y; - cr.get_current_point (out x, out y); - Gdk.cairo_set_source_pixbuf (cr, pixbuf, x, y-IMAGE_SIZE*0.7); - cr.paint(); - } - } -} diff --git a/src/contacts-contact-pane.vala b/src/contacts-contact-pane.vala index 8a9a46f..2b7ff6a 100644 --- a/src/contacts-contact-pane.vala +++ b/src/contacts-contact-pane.vala @@ -204,7 +204,7 @@ public class Contacts.FieldRow : Contacts.Row { public void pack_header (string s) { var l = new Label (s); l.set_markup ( - "%s".printf (s)); + Markup.printf_escaped ("%s", s)); l.set_halign (Align.START); pack (l); } @@ -215,7 +215,7 @@ public class Contacts.FieldRow : Contacts.Row { var l = new Label (s); label = l; l.set_markup ( - "%s".printf (s)); + Markup.printf_escaped ("%s", s)); l.set_halign (Align.START); l.set_hexpand (true); @@ -1552,7 +1552,7 @@ public class Contacts.ContactPane : ScrolledWindow { l.xalign = 0.0f; contact.keep_widget_uptodate (l, (w) => { - (w as Label).set_markup ("" + contact.display_name + ""); + (w as Label).set_markup (Markup.printf_escaped ("%s", contact.display_name)); }); var event_box = new EventBox (); @@ -1609,7 +1609,7 @@ public class Contacts.ContactPane : ScrolledWindow { if (save && changed) { // Things look better if we update immediately, rather than after the setting has // been applied - l.set_markup ("" + entry.get_text () + ""); + l.set_markup (Markup.printf_escaped ("%s", entry.get_text ())); Value v = Value (typeof (string)); v.set_string (entry.get_text ()); @@ -1620,7 +1620,7 @@ public class Contacts.ContactPane : ScrolledWindow { set_individual_property.end (result); } catch (Error e) { App.app.show_message (e.message); - l.set_markup ("" + contact.display_name + ""); + l.set_markup (Markup.printf_escaped ("%s", contact.display_name)); } }); } @@ -1763,9 +1763,9 @@ public class Contacts.ContactPane : ScrolledWindow { var label = new Label (""); if (contact.is_main) - label.set_markup (_("Does %s from %s belong here?").printf (c.display_name, c.format_persona_stores ())); + label.set_markup (Markup.printf_escaped (_("Does %s from %s belong here?"), c.display_name, c.format_persona_stores ())); else - label.set_markup (_("Do these details belong to %s?").printf (c.display_name)); + label.set_markup (Markup.printf_escaped (_("Do these details belong to %s?"), c.display_name)); label.set_valign (Align.START); label.set_halign (Align.START); label.set_line_wrap (true); @@ -1802,7 +1802,13 @@ public class Contacts.ContactPane : ScrolledWindow { grid.attach (bbox, 2, 0, 1, 2); } - public void update_personas () { + private uint update_personas_timeout; + public void update_personas (bool show_matches = true) { + if (update_personas_timeout != 0) { + Source.remove (update_personas_timeout); + update_personas_timeout = 0; + } + foreach (var w in personas_grid.get_children ()) { w.destroy (); } @@ -1818,18 +1824,20 @@ public class Contacts.ContactPane : ScrolledWindow { personas_grid.add (sheet); } - var matches = contact.store.aggregator.get_potential_matches (contact.individual, MatchResult.HIGH); - foreach (var ind in matches.keys) { - var c = Contact.from_individual (ind); - if (c != null && contact.suggest_link_to (c)) { - add_suggestion (c); + if (show_matches) { + var matches = contact.store.aggregator.get_potential_matches (contact.individual, MatchResult.HIGH); + foreach (var ind in matches.keys) { + var c = Contact.from_individual (ind); + if (c != null && contact.suggest_link_to (c)) { + add_suggestion (c); + } } } personas_grid.show_all (); } - public void show_contact (Contact? new_contact, bool edit=false) { + public void show_contact (Contact? new_contact, bool edit=false, bool show_matches = true) { if (contact == new_contact) return; @@ -1844,7 +1852,14 @@ public class Contacts.ContactPane : ScrolledWindow { contact = new_contact; update_card (); - update_personas (); + update_personas (show_matches); + + if (!show_matches) { + update_personas_timeout = Gdk.threads_add_timeout (100, () => { + update_personas (); + return false; + }); + } bool can_remove = false; diff --git a/src/contacts-contact.vala b/src/contacts-contact.vala index 27c2bf1..8cb29cc 100644 --- a/src/contacts-contact.vala +++ b/src/contacts-contact.vala @@ -65,7 +65,7 @@ public class Contacts.ContactPresence : Grid { if (message.length == 0) message = Contact.presence_to_string (type); - label.set_markup ("" + message + ""); + label.set_markup (Markup.printf_escaped ("%s", message)); label.set_margin_bottom (3); if (is_phone) @@ -99,8 +99,13 @@ public class Contacts.ContactPresence : Grid { update_presence_widgets (); }); + var id2 = contact.personas_changed.connect ( () => { + update_presence_widgets (); + }); + this.destroy.connect (() => { contact.disconnect (id); + contact.disconnect (id2); }); } } diff --git a/src/contacts-link-dialog.vala b/src/contacts-link-dialog.vala index 03a61d4..cefec4e 100644 --- a/src/contacts-link-dialog.vala +++ b/src/contacts-link-dialog.vala @@ -52,7 +52,7 @@ public class Contacts.LinkDialog : Dialog { persona_grid.attach (image_frame, 0, 0, 1, 2); var label = new Label (""); - label.set_markup ("" + selected_contact.display_name + ""); + label.set_markup (Markup.printf_escaped ("%s", selected_contact.display_name)); label.set_valign (Align.START); label.set_halign (Align.START); label.set_hexpand (false); @@ -61,7 +61,7 @@ public class Contacts.LinkDialog : Dialog { persona_grid.attach (label, 1, 0, 1, 1); label = new Label (""); - label.set_markup ("" +selected_contact.format_persona_stores () + ""); + label.set_markup (Markup.printf_escaped ("%s", selected_contact.format_persona_stores ())); label.set_valign (Align.START); label.set_halign (Align.START); label.set_hexpand (true); @@ -179,7 +179,7 @@ public class Contacts.LinkDialog : Dialog { var label = new Label (""); if (contact.is_main) - label.set_markup (_("Link contacts to %s").printf (contact.display_name)); + label.set_markup (Markup.printf_escaped (_("Link contacts to %s"), contact.display_name)); else label.set_markup (_("Select contact to link to")); label.set_valign (Align.CENTER); @@ -220,8 +220,9 @@ public class Contacts.LinkDialog : Dialog { scrolled.set_vexpand (true); scrolled.set_hexpand (true); scrolled.set_shadow_type (ShadowType.NONE); - scrolled.add (view); + scrolled.add_with_viewport (view); list_grid.add (scrolled); + view.set_focus_vadjustment (scrolled.get_vadjustment ()); view.selection_changed.connect ( (c) => { selected_contact = c; diff --git a/src/contacts-list-pane.vala b/src/contacts-list-pane.vala index 50ae1fe..af4a934 100644 --- a/src/contacts-list-pane.vala +++ b/src/contacts-list-pane.vala @@ -138,12 +138,14 @@ public class Contacts.ListPane : Frame { grid.set_orientation (Orientation.VERTICAL); this.add (grid); + contacts_view.set_focus_vadjustment (scrolled.get_vadjustment ()); + contacts_view.selection_changed.connect( (l, contact) => { if (!ignore_selection_change) selection_changed (contact); }); - scrolled.add (contacts_view); + scrolled.add_with_viewport (contacts_view); contacts_view.show_all (); scrolled.set_no_show_all (true); @@ -160,7 +162,6 @@ public class Contacts.ListPane : Frame { public void select_contact (Contact contact, bool ignore_change = false) { if (ignore_change) ignore_selection_change = true; - contacts_view.select_contact (contact); ignore_selection_change = false; } } diff --git a/src/contacts-setup-window.vala b/src/contacts-setup-window.vala index 0ccd9cb..359b987 100644 --- a/src/contacts-setup-window.vala +++ b/src/contacts-setup-window.vala @@ -147,7 +147,7 @@ public class Contacts.SetupWindow : Gtk.Window { var item = new ToolItem (); title_label = new Label (""); - title_label.set_markup ("%s".printf (_("Contacts Setup"))); + title_label.set_markup (Markup.printf_escaped ("%s",_("Contacts Setup"))); title_label.set_no_show_all (true); item.add (title_label); item.set_expand (true); diff --git a/src/contacts-sorted.vala b/src/contacts-sorted.vala new file mode 100644 index 0000000..71a11b8 --- /dev/null +++ b/src/contacts-sorted.vala @@ -0,0 +1,917 @@ +/* -*- Mode: vala; indent-tabs-mode: t; c-basic-offset: 2; tab-width: 8 -*- */ +/* + * 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; + +/* Requriements: + + sort + + filter + + first char or type custom "separators" + (create, destroy, update) + + Work with largish sets of children + + selection and keynav + + activation (separate from selection) + + select mode (but not multiple) + + settings: + sort function + filter function + update_separator + + ops: + child-changed (resort, refilter, + resort-all + refilter-all + + Impl: + GSequence for children + GHashTable for child to iter mapping +*/ + +public class Contacts.Sorted : Container { + public delegate bool FilterFunc (Widget child); + public delegate void UpdateSeparatorFunc (ref Widget? separator, Widget child, Widget? before); + + struct ChildInfo { + Widget widget; + Widget? separator; + SequenceIter iter; + int y; + int height; + } + + Sequence children; + HashTable child_hash; + CompareDataFunc? sort_func; + FilterFunc? filter_func; + UpdateSeparatorFunc? update_separator_func; + unowned ChildInfo? selected_child; + unowned ChildInfo? prelight_child; + unowned ChildInfo? cursor_child; + private SelectionMode selection_mode; + + private int do_sort (ChildInfo? a, ChildInfo? b) { + return sort_func (a.widget, b.widget); + } + + public Sorted () { + set_can_focus (true); + set_has_window (true); + set_redraw_on_allocate (true); + + selection_mode = SelectionMode.SINGLE; + + children = new Sequence(); + child_hash = new HashTable (GLib.direct_hash, GLib.direct_equal); + } + + [Signal (action=true)] + public virtual signal void activate_row () { + select_and_activate (cursor_child); + } + + [Signal (action=true)] + public virtual signal void modify_selection () { + if (cursor_child == null) + return; + + if (selection_mode == SelectionMode.SINGLE && + selected_child == cursor_child) + update_selected (null); + else + select_and_activate (cursor_child); + } + + + [Signal (action=true)] + public virtual signal void move_cursor (MovementStep step, int count) { + Gdk.ModifierType state; + + bool modify_selection_pressed = false; + + if (Gtk.get_current_event_state (out state)) { + var modify_mod_mask = this.get_modifier_mask (Gdk.ModifierIntent.MODIFY_SELECTION); + if ((state & modify_mod_mask) == modify_mod_mask) + modify_selection_pressed = true; + } + + unowned ChildInfo? child = null; + switch (step) { + case MovementStep.BUFFER_ENDS: + if (count < 0) + child = get_first_visible (); + else + child = get_last_visible (); + break; + case MovementStep.DISPLAY_LINES: + if (cursor_child != null) { + SequenceIter? iter = cursor_child.iter; + + while (count < 0 && iter != null) { + iter = get_previous_visible (iter); + count++; + } + while (count > 0 && iter != null) { + iter = get_next_visible (iter); + count--; + } + if (iter != null && !iter.is_end ()) { + child = iter.get (); + } + } + break; + case MovementStep.PAGES: + int page_size = 100; + var vadj = get_focus_vadjustment (); + if (vadj != null) + page_size = (int) vadj.get_page_increment (); + + if (cursor_child != null) { + int start_y = cursor_child.y; + int end_y = start_y; + SequenceIter? iter = cursor_child.iter; + + child = cursor_child; + if (count < 0) { + /* Up */ + + while (iter != null && !iter.is_begin ()) { + iter = get_previous_visible (iter); + if (iter == null) + break; + unowned ChildInfo? prev = iter.get (); + if (prev.y < start_y - page_size) + break; + child = prev; + } + } else { + /* Down */ + + while (iter != null && !iter.is_end ()) { + iter = get_next_visible (iter); + if (iter.is_end ()) + break; + unowned ChildInfo? next = iter.get (); + if (next.y > start_y + page_size) + break; + child = next; + } + } + end_y = child.y; + if (end_y != start_y && vadj != null) + vadj.value += end_y - start_y; + + } + break; + default: + return; + } + + if (child == null) { + error_bell (); + return; + } + + update_cursor (child); + if (!modify_selection_pressed) + update_selected (child); + } + + private static void add_move_binding (BindingSet binding_set, uint keyval, Gdk.ModifierType modmask, + MovementStep step, int count) { + BindingEntry.add_signal (binding_set, keyval, modmask, + "move-cursor", 2, + typeof (MovementStep), step, + typeof (int), count); + + if ((modmask & Gdk.ModifierType.CONTROL_MASK) == Gdk.ModifierType.CONTROL_MASK) + return; + + BindingEntry.add_signal (binding_set, keyval, Gdk.ModifierType.CONTROL_MASK, + "move-cursor", 2, + typeof (MovementStep), step, + typeof (int), count); + } + + [CCode (cname = "klass")] + private static extern void *workaround_for_local_var_klass; + static construct { + unowned BindingSet binding_set = BindingSet.by_class (workaround_for_local_var_klass); + + add_move_binding (binding_set, Gdk.Key.Home, 0, + MovementStep.BUFFER_ENDS, -1); + add_move_binding (binding_set, Gdk.Key.KP_Home, 0, + MovementStep.BUFFER_ENDS, -1); + + add_move_binding (binding_set, Gdk.Key.End, 0, + MovementStep.BUFFER_ENDS, 1); + add_move_binding (binding_set, Gdk.Key.KP_End, 0, + MovementStep.BUFFER_ENDS, 1); + + add_move_binding (binding_set, Gdk.Key.Up, Gdk.ModifierType.CONTROL_MASK, + MovementStep.DISPLAY_LINES, -1); + add_move_binding (binding_set, Gdk.Key.KP_Up, Gdk.ModifierType.CONTROL_MASK, + MovementStep.DISPLAY_LINES, -1); + + add_move_binding (binding_set, Gdk.Key.Down, Gdk.ModifierType.CONTROL_MASK, + MovementStep.DISPLAY_LINES, 1); + add_move_binding (binding_set, Gdk.Key.KP_Down, Gdk.ModifierType.CONTROL_MASK, + MovementStep.DISPLAY_LINES, 1); + + add_move_binding (binding_set, Gdk.Key.Page_Up, 0, + MovementStep.PAGES, -1); + add_move_binding (binding_set, Gdk.Key.KP_Page_Up, 0, + MovementStep.PAGES, -1); + + add_move_binding (binding_set, Gdk.Key.Page_Down, 0, + MovementStep.PAGES, 1); + add_move_binding (binding_set, Gdk.Key.KP_Page_Down, 0, + MovementStep.PAGES, 1); + + BindingEntry.add_signal (binding_set, Gdk.Key.space, Gdk.ModifierType.CONTROL_MASK, + "modify-selection", 0); + + activate_signal = GLib.Signal.lookup ("activate-row", typeof (Sorted)); + } + + unowned ChildInfo? find_child_at_y (int y) { + unowned ChildInfo? child_info = null; + for (var iter = children.get_begin_iter (); !iter.is_end (); iter = iter.next ()) { + unowned ChildInfo? info = iter.get (); + if (y >= info.y && y < info.y + info.height) { + child_info = info; + break; + } + } + return child_info; + } + + private void update_cursor (ChildInfo? child) { + cursor_child = child; + this.grab_focus (); + this.queue_draw (); + var vadj = get_focus_vadjustment (); + if (child != null && vadj != null) + vadj.clamp_page (cursor_child.y, + cursor_child.y + cursor_child.height); + } + + private void update_selected (ChildInfo? child) { + if (child != selected_child && + (child == null || selection_mode != SelectionMode.NONE)) { + selected_child = child; + child_selected (selected_child != null ? selected_child.widget : null); + queue_draw (); + } + if (child != null) + update_cursor (child); + } + + private void select_and_activate (ChildInfo? child) { + Widget? w = null; + if (child != null) + w = child.widget; + update_selected (child); + if (w != null) + child_activated (w); + } + + private void update_prelight (ChildInfo? child) { + if (child != prelight_child) { + prelight_child = child; + queue_draw (); + } + } + + public override bool enter_notify_event (Gdk.EventCrossing event) { + if (event.window != get_window ()) + return false; + + unowned ChildInfo? child = find_child_at_y ((int)event.y); + update_prelight (child); + + return false; + } + + public override bool leave_notify_event (Gdk.EventCrossing event) { + if (event.window != get_window ()) + return false; + + if (event.detail != Gdk.NotifyType.INFERIOR) { + update_prelight (null); + } else { + unowned ChildInfo? child = find_child_at_y ((int)event.y); + update_prelight (child); + } + + return false; + } + + public override bool motion_notify_event (Gdk.EventMotion event) { + unowned ChildInfo? child = find_child_at_y ((int)event.y); + update_prelight (child); + return false; + } + + private Widget? button_down_child; + public override bool button_press_event (Gdk.EventButton event) { + if (event.button == 1) { + unowned ChildInfo? child = find_child_at_y ((int)event.y); + if (child != null) + button_down_child = child.widget; + + /* TODO: Should mark as active while down, and handle grab breaks */ + } + return false; + } + + public override bool button_release_event (Gdk.EventButton event) { + if (event.button == 1) { + unowned ChildInfo? child = find_child_at_y ((int)event.y); + if (child != null && child.widget == button_down_child) + select_and_activate (child); + button_down_child = null; + } + return false; + } + + public Widget? get_selected_child (){ + if (selected_child != null) + return selected_child.widget; + + return null; + } + + public virtual signal void child_selected (Widget? child) { + } + + public virtual signal void child_activated (Widget? child) { + } + + public override bool focus (DirectionType direction) { + bool had_focus; + bool focus_into; + Widget recurse_into = null; + + focus_into = true; + had_focus = has_focus; + + unowned ChildInfo? current_focus_child = null; + unowned ChildInfo? next_focus_child = null; + + if (had_focus) { + /* If on row, going right, enter into possible container */ + if (direction == DirectionType.RIGHT || + direction == DirectionType.TAB_FORWARD) { + /* TODO: Handle null cursor child */ + recurse_into = cursor_child.widget; + } + current_focus_child = cursor_child; + /* Unless we're going up/down we're always leaving + the container */ + if (direction != DirectionType.UP && + direction != DirectionType.DOWN) + focus_into = false; + } else if (this.get_focus_child () != null) { + /* There is a focus child, always navigat inside it first */ + recurse_into = this.get_focus_child (); + current_focus_child = lookup_info (recurse_into); + + /* If exiting child container to the right, exit row */ + if (direction == DirectionType.RIGHT || + direction == DirectionType.TAB_FORWARD) + focus_into = false; + + /* If exiting child container to the left, select row or out */ + if (direction == DirectionType.LEFT || + direction == DirectionType.TAB_BACKWARD) { + next_focus_child = current_focus_child; + } + } else { + /* If coming from the left, enter into possible container */ + if (direction == DirectionType.LEFT || + direction == DirectionType.TAB_BACKWARD) { + if (selected_child != null) + recurse_into = selected_child.widget; + } + } + + if (recurse_into != null) { + if (recurse_into.child_focus (direction)) + return true; + } + + if (!focus_into) + return false; // Focus is leaving us + + /* TODO: This doesn't handle up/down going into a focusable separator */ + + if (next_focus_child == null) { + if (current_focus_child != null) { + if (direction == DirectionType.UP) { + var i = get_previous_visible (current_focus_child.iter); + if (i != null) + next_focus_child = i.get (); + } else { + var i = get_next_visible (current_focus_child.iter); + if (!i.is_end ()) + next_focus_child = i.get (); + } + } else { + switch (direction) { + case DirectionType.DOWN: + case DirectionType.TAB_FORWARD: + next_focus_child = get_first_visible (); + break; + case DirectionType.UP: + case DirectionType.TAB_BACKWARD: + next_focus_child = get_last_visible (); + break; + default: + next_focus_child = selected_child; + if (next_focus_child == null) + next_focus_child = get_first_visible (); + break; + } + } + } + + if (next_focus_child == null) { + if (direction == DirectionType.UP || direction == DirectionType.DOWN) { + error_bell (); + return true; + } + + return false; + } + + bool modify_selection_pressed = false; + Gdk.ModifierType state; + + if (Gtk.get_current_event_state (out state)) { + var modify_mod_mask = this.get_modifier_mask (Gdk.ModifierIntent.MODIFY_SELECTION); + if ((state & modify_mod_mask) == modify_mod_mask) + modify_selection_pressed = true; + } + + update_cursor (next_focus_child); + if (!modify_selection_pressed) + update_selected (next_focus_child); + + return true; + } + + public override bool draw (Cairo.Context cr) { + Allocation allocation; + this.get_allocation (out allocation); + + var context = this.get_style_context (); + + context.save (); + context.render_background (cr, + 0, 0, allocation.width, allocation.height); + + if (selected_child != null) { + context.set_state (StateFlags.SELECTED); + context.render_background (cr, + 0, selected_child.y, + allocation.width, selected_child.height); + } + + if (prelight_child != null && prelight_child != selected_child) { + context.set_state (StateFlags.PRELIGHT); + context.render_background (cr, + 0, prelight_child.y, + allocation.width, prelight_child.height); + } + + if (has_visible_focus() && cursor_child != null) { + context.render_focus (cr, 0, cursor_child.y, + allocation.width, cursor_child.height); + } + + context.restore (); + + base.draw (cr); + + return true; + } + + public override void realize () { + Allocation allocation; + get_allocation (out allocation); + set_realized (true); + + Gdk.WindowAttr attributes = { }; + attributes.x = allocation.x; + attributes.y = allocation.y; + attributes.width = allocation.width; + attributes.height = allocation.height; + attributes.window_type = Gdk.WindowType.CHILD; + attributes.event_mask = this.get_events () | + Gdk.EventMask.ENTER_NOTIFY_MASK | + Gdk.EventMask.LEAVE_NOTIFY_MASK | + Gdk.EventMask.POINTER_MOTION_MASK | + Gdk.EventMask.EXPOSURE_MASK | + Gdk.EventMask.BUTTON_PRESS_MASK | + Gdk.EventMask.BUTTON_RELEASE_MASK; + + attributes.wclass = Gdk.WindowWindowClass.INPUT_OUTPUT; + var window = new Gdk.Window (get_parent_window (), attributes, + Gdk.WindowAttributesType.X | + Gdk.WindowAttributesType.Y); + window.set_user_data (this); + this.set_window (window); + } + + private void apply_filter (Widget child) { + bool do_show = true; + if (filter_func != null) + do_show = filter_func (child); + child.set_child_visible (do_show); + } + + private void apply_filter_all () { + for (var iter = children.get_begin_iter (); !iter.is_end (); iter = iter.next ()) { + unowned ChildInfo? child_info = iter.get (); + apply_filter (child_info.widget); + } + } + + public void set_selection_mode (SelectionMode mode) { + if (mode == SelectionMode.MULTIPLE) { + warning ("Multiple selections not supported"); + return; + } + selection_mode = mode; + if (mode == SelectionMode.NONE) + update_selected (null); + } + + public void set_filter_func (owned FilterFunc? f) { + filter_func = (owned)f; + refilter (); + } + + public void set_separator_funcs (owned UpdateSeparatorFunc? update_separator) { + update_separator_func = (owned)update_separator; + reseparate (); + } + + public void refilter () { + apply_filter_all (); + reseparate (); + queue_resize (); + } + + public void resort () { + children.sort (do_sort); + reseparate (); + queue_resize (); + } + + private unowned ChildInfo? get_first_visible () { + for (var iter = children.get_begin_iter (); !iter.is_end (); iter = iter.next ()) { + unowned ChildInfo? child_info = iter.get (); + unowned Widget widget = child_info.widget; + if (widget.get_visible () && widget.get_child_visible ()) + return child_info; + } + return null; + } + + private unowned ChildInfo? get_last_visible () { + var iter = children.get_end_iter (); + while (!iter.is_begin ()) { + iter = iter.prev (); + unowned ChildInfo? child_info = iter.get (); + unowned Widget widget = child_info.widget; + if (widget.get_visible () && widget.get_child_visible ()) + return child_info; + } + return null; + } + + private SequenceIter? get_previous_visible (SequenceIter _iter) { + if (_iter.is_begin()) + return null; + var iter = _iter; + + do { + iter = iter.prev (); + + unowned ChildInfo? child_info = iter.get (); + unowned Widget widget = child_info.widget; + if (widget.get_visible () && widget.get_child_visible ()) + return iter; + } while (!iter.is_begin ()); + + return null; + } + + private SequenceIter? get_next_visible (SequenceIter _iter) { + if (_iter.is_end()) + return _iter; + + var iter = _iter; + do { + iter = iter.next (); + + if (!iter.is_end ()) { + unowned ChildInfo? child_info = iter.get (); + unowned Widget widget = child_info.widget; + if (widget.get_visible () && widget.get_child_visible ()) + return iter; + } + } while (!iter.is_end ()); + + return iter; + } + + private void update_separator (SequenceIter? iter) { + if (iter == null || iter.is_end ()) + return; + + unowned ChildInfo? info = iter.get (); + var before_iter = get_previous_visible (iter); + var widget = info.widget; + Widget? before_widget = null; + if (before_iter != null) { + unowned ChildInfo? before_info = before_iter.get (); + before_widget = before_info.widget; + } + + if (update_separator_func != null && + widget.get_visible () && + widget.get_child_visible ()) { + var old_separator = info.separator; + update_separator_func (ref info.separator, widget, before_widget); + if (old_separator != info.separator) { + if (old_separator != null) + old_separator.unparent (); + if (info.separator != null) { + info.separator.set_parent (this); + info.separator.show (); + } + this.queue_resize (); + } + } else if (info.separator != null) { + info.separator.unparent (); + info.separator = null; + this.queue_resize (); + } + } + + public void reseparate () { + for (var iter = children.get_begin_iter (); !iter.is_end (); iter = iter.next ()) { + update_separator (iter); + } + queue_resize (); + } + + public void set_sort_func (owned CompareDataFunc? f) { + sort_func = (owned)f; + resort (); + } + + private unowned ChildInfo? lookup_info (Widget widget) { + return child_hash.get (widget); + } + + public override void add (Widget widget) { + ChildInfo? the_info = { widget }; + unowned ChildInfo? info = the_info; + SequenceIter iter; + + child_hash.set (widget, info); + + if (sort_func != null) + iter = children.insert_sorted ((owned) the_info, do_sort); + else + iter = children.append ((owned) the_info); + + apply_filter (widget); + + var prev_next = get_next_visible (iter); + update_separator (iter); + update_separator (get_next_visible (iter)); + update_separator (prev_next); + + info.iter = iter; + + widget.set_parent (this); + } + + public void child_changed (Widget widget) { + unowned ChildInfo? info = lookup_info (widget); + if (info == null) + return; + + var prev_next = get_previous_visible (info.iter); + + if (sort_func != null) { + children.sort_changed (info.iter, do_sort); + this.queue_resize (); + } + apply_filter (info.widget); + update_separator (info.iter); + update_separator (get_next_visible (info.iter)); + update_separator (prev_next); + + } + + public override void remove (Widget widget) { + unowned ChildInfo? info = lookup_info (widget); + if (info == null) { + warning ("Tried to remove non-child %p\n", widget); + return; + } + + if (info == selected_child) + update_selected (null); + if (info == prelight_child) + prelight_child = null; + if (info == cursor_child) + cursor_child = null; + + var next = get_next_visible (info.iter); + + bool was_visible = widget.get_visible (); + widget.unparent (); + + child_hash.remove (widget); + children.remove (info.iter); + + update_separator (next); + + if (was_visible && this.get_visible ()) + this.queue_resize (); + } + + public override void forall_internal (bool include_internals, + Gtk.Callback callback) { + for (var iter = children.get_begin_iter (); !iter.is_end (); iter = iter.next ()) { + unowned ChildInfo? child_info = iter.get (); + if (child_info.separator != null && include_internals) + callback (child_info.separator); + callback (child_info.widget); + } + } + + public override void compute_expand_internal (out bool hexpand, out bool vexpand) { + base.compute_expand_internal (out hexpand, out vexpand); + /* We don't expand vertically beyound the minimum size */ + vexpand = false; + } + + public override Type child_type () { + return typeof (Widget); + } + + public override Gtk.SizeRequestMode get_request_mode () { + return SizeRequestMode.HEIGHT_FOR_WIDTH; + } + + public override void get_preferred_height (out int minimum_height, out int natural_height) { + int natural_width; + get_preferred_width_internal (null, out natural_width); + get_preferred_height_for_width_internal (natural_width, out minimum_height, out natural_height); + } + + public override void get_preferred_height_for_width (int width, out int minimum_height, out int natural_height) { + minimum_height = 0; + var context = this.get_style_context (); + int focus_width, focus_pad; + context.get_style ("focus-line-width", out focus_width, + "focus-padding", out focus_pad); + for (var iter = children.get_begin_iter (); !iter.is_end (); iter = iter.next ()) { + unowned ChildInfo? child_info = iter.get (); + unowned Widget widget = child_info.widget; + int child_min; + + if (!widget.get_visible () || !widget.get_child_visible ()) + continue; + + if (child_info.separator != null) { + child_info.separator.get_preferred_height_for_width (width, out child_min, null); + minimum_height += child_min; + } + + widget.get_preferred_height_for_width (width - 2 * (focus_width + focus_pad), + out child_min, null); + minimum_height += child_min + 2 * (focus_width + focus_pad); + } + + /* We always allocate the minimum height, since handling + expanding rows is way too costly, and unlikely to + be used, as lists are generally put inside a scrolling window + anyway. + */ + natural_height = minimum_height; + } + + public override void get_preferred_width (out int minimum_width, out int natural_width) { + var context = this.get_style_context (); + int focus_width, focus_pad; + context.get_style ("focus-line-width", out focus_width, + "focus-padding", out focus_pad); + minimum_width = 0; + natural_width = 0; + for (var iter = children.get_begin_iter (); !iter.is_end (); iter = iter.next ()) { + unowned ChildInfo? child_info = iter.get (); + unowned Widget widget = child_info.widget; + int child_min, child_nat; + + if (!widget.get_visible () || !widget.get_child_visible ()) + continue; + + widget.get_preferred_width (out child_min, out child_nat); + minimum_width = int.max (minimum_width, child_min + 2 * (focus_width + focus_pad)); + natural_width = int.max (natural_width, child_nat + 2 * (focus_width + focus_pad)); + + if (child_info.separator != null) { + child_info.separator.get_preferred_width (out child_min, out child_nat); + minimum_width = int.max (minimum_width, child_min); + natural_width = int.max (natural_width, child_nat); + } + } + } + + public override void get_preferred_width_for_height (int height, out int minimum_width, out int natural_width) { + get_preferred_width_internal (out minimum_width, out natural_width); + } + + public override void size_allocate (Gtk.Allocation allocation) { + Allocation child_allocation = { 0, 0, 0, 0}; + Allocation separator_allocation = { 0, 0, 0, 0}; + + set_allocation (allocation); + + var window = get_window(); + if (window != null) + window.move_resize (allocation.x, + allocation.y, + allocation.width, + allocation.height); + + var context = this.get_style_context (); + int focus_width, focus_pad; + context.get_style ("focus-line-width", out focus_width, + "focus-padding", out focus_pad); + + child_allocation.x = allocation.x + focus_width + focus_pad; + child_allocation.y = allocation.y; + child_allocation.width = allocation.width - 2 * (focus_width + focus_pad); + + separator_allocation.x = allocation.x; + separator_allocation.width = allocation.width; + + for (var iter = children.get_begin_iter (); !iter.is_end (); iter = iter.next ()) { + unowned ChildInfo? child_info = iter.get (); + unowned Widget widget = child_info.widget; + int child_min; + + if (!widget.get_visible () || !widget.get_child_visible ()) { + child_info.y = child_allocation.y; + child_info.height = 0; + continue; + } + + if (child_info.separator != null) { + child_info.separator.get_preferred_height_for_width (allocation.width, out child_min, null); + separator_allocation.height = child_min; + separator_allocation.y = child_allocation.y; + + child_info.separator.size_allocate (separator_allocation); + + child_allocation.y += child_min; + } + + child_info.y = child_allocation.y; + child_allocation.y += focus_width + focus_pad; + + widget.get_preferred_height_for_width (child_allocation.width, out child_min, null); + child_allocation.height = child_min; + + child_info.height = child_allocation.height + 2 * (focus_width + focus_pad); + widget.size_allocate (child_allocation); + + child_allocation.y += child_min + focus_width + focus_pad; + } + } +} diff --git a/src/contacts-view.vala b/src/contacts-view.vala index dc7722e..be3fcd6 100644 --- a/src/contacts-view.vala +++ b/src/contacts-view.vala @@ -20,13 +20,16 @@ using Gtk; using Folks; using Gee; -public class Contacts.View : TreeView { +public class Contacts.View : Contacts.Sorted { private class ContactData { public Contact contact; - public TreeIter iter; - public bool visible; - public bool is_first; + public Grid grid; + public Label label; + public ContactFrame image_frame; public int sort_prio; + public string display_name; + public unichar initial_letter; + public bool filtered; } public enum Subset { @@ -36,46 +39,44 @@ public class Contacts.View : TreeView { ALL } + public enum TextDisplay { + NONE, + PRESENCE, + STORES + } + + public signal void selection_changed (Contact? contact); + Store contacts_store; Subset show_subset; - ListStore list_store; + HashMap contacts; HashSet hidden_contacts; + string []? filter_values; - int custom_visible_count; - ContactData suggestions_header_data; - ContactData padding_data; - ContactData other_header_data; + private TextDisplay text_display; public View (Store store, TextDisplay text_display = TextDisplay.PRESENCE) { + set_selection_mode (SelectionMode.BROWSE); contacts_store = store; hidden_contacts = new HashSet(); show_subset = Subset.ALL; + this.text_display = text_display; - list_store = new ListStore (2, typeof (Contact), typeof (ContactData *)); - suggestions_header_data = new ContactData (); - suggestions_header_data.sort_prio = int.MAX; - padding_data = new ContactData (); - padding_data.sort_prio = 1; + contacts = new HashMap (); - other_header_data = new ContactData (); - other_header_data.sort_prio = -1; - - list_store.set_sort_func (0, (model, iter_a, iter_b) => { - ContactData *aa, bb; - model.get (iter_a, 1, out aa); - model.get (iter_b, 1, out bb); - - return compare_data (aa, bb); + this.set_sort_func ((widget_a, widget_b) => { + var a = widget_a.get_data ("data"); + var b = widget_b.get_data ("data"); + return compare_data (a, b); }); - list_store.set_sort_column_id (0, SortType.ASCENDING); + this.set_filter_func (filter); + this.set_separator_funcs (update_separator); contacts_store.added.connect (contact_added_cb); contacts_store.removed.connect (contact_removed_cb); contacts_store.changed.connect (contact_changed_cb); foreach (var c in store.get_contacts ()) contact_added_cb (store, c); - - init_view (text_display); } private int compare_data (ContactData a_data, ContactData b_data) { @@ -87,16 +88,13 @@ public class Contacts.View : TreeView { if (a_prio < b_prio) return 1; - var a = a_data.contact; - var b = b_data.contact; - - if (is_set (a.display_name) && is_set (b.display_name)) - return a.display_name.collate (b.display_name); + if (is_set (a_data.display_name) && is_set (b_data.display_name)) + return a_data.display_name.collate (b_data.display_name); // Sort empty names last - if (is_set (a.display_name)) + if (is_set (a_data.display_name)) return -1; - if (is_set (b.display_name)) + if (is_set (b_data.display_name)) return 1; return 0; @@ -111,423 +109,200 @@ public class Contacts.View : TreeView { } /* The hardcoded prio if set, otherwise 0 for the - main/combined group, or -2 for the separated other group */ + main/combined group, or -1 for the separated other group */ private int get_sort_prio (ContactData *data) { if (data->sort_prio != 0) return data->sort_prio; if (is_other (data)) - return -2; + return -1; return 0; } - public string get_header_text (TreeIter iter) { - ContactData *data; - list_store.get (iter, 1, out data); - if (data == suggestions_header_data) { - /* Translators: This is the header for the list of suggested contacts to - link to the current contact */ - return ngettext ("Suggestion", "Suggestions", custom_visible_count); - } - if (data == other_header_data) { - /* Translators: This is the header for the list of suggested contacts to - link to the current contact */ - return _("Other Contacts"); - } - return ""; - } - public void set_show_subset (Subset subset) { show_subset = subset; - - bool new_visible = show_subset == Subset.ALL_SEPARATED; - if (new_visible && !other_header_data.visible) { - other_header_data.visible = true; - list_store.append (out other_header_data.iter); - list_store.set (other_header_data.iter, 1, other_header_data); - } - if (!new_visible && other_header_data.visible) { - other_header_data.visible = false; - list_store.remove (other_header_data.iter); - } - + update_all_filtered (); refilter (); + resort (); } public void set_custom_sort_prio (Contact c, int prio) { /* We use negative prios internally */ assert (prio >= 0); - var data = lookup_data (c); - + var data = contacts.get (c); if (data == null) return; - - // We insert a priority between 0 and 1 for the padding - if (prio > 0) - prio += 1; data.sort_prio = prio; - contact_changed_cb (contacts_store, c); - - if (data.visible) { - if (prio > 0) { - if (custom_visible_count++ == 0) - add_custom_headers (); - } else { - if (custom_visible_count-- == 1) - remove_custom_headers (); - } - } - } - - private bool apply_filter (Contact contact) { - if (contact.is_hidden) - return false; - - if (contact in hidden_contacts) - return false; - - if ((show_subset == Subset.MAIN && - !contact.is_main) || - (show_subset == Subset.OTHER && - contact.is_main)) - return false; - - if (filter_values == null || filter_values.length == 0) - return true; - - return contact.contains_strings (filter_values); - } - - public bool is_first (TreeIter iter) { - ContactData *data; - list_store.get (iter, 1, out data); - if (data != null) - return data->is_first; - return false; - } - - private ContactData? get_previous (ContactData data) { - ContactData *previous = null; - TreeIter iter = data.iter; - if (list_store.iter_previous (ref iter)) - list_store.get (iter, 1, out previous); - return previous; - } - - private ContactData? get_next (ContactData data) { - ContactData *next = null; - TreeIter iter = data.iter; - if (list_store.iter_next (ref iter)) - list_store.get (iter, 1, out next); - return next; - } - - private void row_changed_no_resort (ContactData data) { - var path = list_store.get_path (data.iter); - list_store.row_changed (path, data.iter); - } - - private void row_changed_resort (ContactData data) { - list_store.set (data.iter, 0, data.contact); - } - - private bool update_is_first (ContactData data, ContactData? previous) { - bool old_is_first = data.is_first; - - bool is_custom = data.sort_prio != 0; - bool previous_is_custom = previous != null && (previous.sort_prio != 0) ; - - if (is_custom) { - data.is_first = false; - } else if (previous != null && !previous_is_custom) { - unichar previous_initial = previous.contact.initial_letter; - unichar initial = data.contact.initial_letter; - data.is_first = previous_initial != initial; - } else { - data.is_first = true; - } - - if (old_is_first != data.is_first) { - row_changed_no_resort (data); - return true; - } - - return false; - } - - private void add_custom_headers () { - suggestions_header_data.visible = true; - list_store.append (out suggestions_header_data.iter); - list_store.set (suggestions_header_data.iter, 1, suggestions_header_data); - padding_data.visible = true; - list_store.append (out padding_data.iter); - list_store.set (padding_data.iter, 1, padding_data); - } - - private void remove_custom_headers () { - suggestions_header_data.visible = false; - list_store.remove (suggestions_header_data.iter); - padding_data.visible = false; - list_store.remove (padding_data.iter); - } - - private void add_to_model (ContactData data) { - list_store.append (out data.iter); - list_store.set (data.iter, 0, data.contact, 1, data); - - if (data.sort_prio > 0) { - if (custom_visible_count++ == 0) - add_custom_headers (); - } - - if (update_is_first (data, get_previous (data)) && data.is_first) { - /* The newly added row is first, the next one might not be anymore */ - var next = get_next (data); - if (next != null) - update_is_first (next, data); - } - } - - private void remove_from_model (ContactData data) { - if (data.sort_prio > 0) { - if (custom_visible_count-- == 1) - remove_custom_headers (); - } - - ContactData? next = null; - if (data.is_first) - next = get_next (data); - - list_store.remove (data.iter); - data.is_first = false; - - if (next != null) - update_is_first (next, get_previous (next)); - } - - private void update_visible (ContactData data) { - bool was_visible = data.visible; - data.visible = apply_filter (data.contact); - - if (was_visible && !data.visible) - remove_from_model (data); - - if (!was_visible && data.visible) - add_to_model (data); - } - - private void refilter () { - foreach (var c in contacts_store.get_contacts ()) { - update_visible (lookup_data (c)); - } + child_changed (data.grid); } public void hide_contact (Contact contact) { hidden_contacts.add (contact); + update_all_filtered (); refilter (); } public void set_filter_values (string []? values) { filter_values = values; + update_all_filtered (); refilter (); } - private void contact_changed_cb (Store store, Contact c) { - ContactData data = lookup_data (c); + private bool calculate_filtered (Contact c) { + if (c.is_hidden) + return false; - bool was_visible = data.visible; + if (c in hidden_contacts) + return false; - ContactData? next = null; - if (data.visible) - next = get_next (data); + if ((show_subset == Subset.MAIN && + !c.is_main) || + (show_subset == Subset.OTHER && + c.is_main)) + return false; - update_visible (data); + if (filter_values == null || filter_values.length == 0) + return true; - if (was_visible && data.visible) { - /* We just moved position in the list while visible */ + return c.contains_strings (filter_values); + } - row_changed_resort (data); + private void update_data (ContactData data) { + var c = data.contact; + data.display_name = c.display_name; + data.initial_letter = c.initial_letter; + data.filtered = calculate_filtered (c); - /* Update the is_first on the previous next row */ - if (next != null) - update_is_first (next, get_previous (next)); + data.label.set_markup (Markup.printf_escaped ("%s", data.display_name)); + data.image_frame.set_image (c.individual, c); + } - /* Update the is_first on the new next row */ - next = get_next (data); - if (next != null) - update_is_first (next, data); + private void update_all_filtered () { + foreach (var data in contacts.values) { + data.filtered = calculate_filtered (data.contact); } } - private ContactData lookup_data (Contact c) { - return c.lookup (this); + private void contact_changed_cb (Store store, Contact c) { + var data = contacts.get (c); + update_data (data); + child_changed (data.grid); } private void contact_added_cb (Store store, Contact c) { - ContactData data = new ContactData(); + var data = new ContactData(); data.contact = c; - data.visible = false; + data.grid = new Grid (); + data.grid.margin = 12; + data.grid.set_column_spacing (10); + data.image_frame = new ContactFrame (Contact.SMALL_AVATAR_SIZE); + data.label = new Label (""); + data.label.set_ellipsize (Pango.EllipsizeMode.END); + data.label.set_valign (Align.START); + data.label.set_halign (Align.START); - c.set_lookup (this, data); + data.grid.attach (data.image_frame, 0, 0, 1, 2); + data.grid.attach (data.label, 1, 0, 1, 1); - update_visible (data); + if (text_display == TextDisplay.PRESENCE) { + var merged_presence = c.create_merged_presence_widget (); + merged_presence.set_halign (Align.START); + merged_presence.set_valign (Align.END); + merged_presence.set_vexpand (false); + merged_presence.set_margin_bottom (4); + + data.grid.attach (merged_presence, 1, 1, 1, 1); + } + + if (text_display == TextDisplay.STORES) { + var stores = new Label (""); + stores.set_markup (Markup.printf_escaped ("%s", + c.format_persona_stores ())); + + stores.set_ellipsize (Pango.EllipsizeMode.END); + stores.set_halign (Align.START); + data.grid.attach (stores, 1, 1, 1, 1); + } + + update_data (data); + + data.grid.set_data ("data", data); + data.grid.show_all (); + contacts.set (c, data); + this.add (data.grid); } private void contact_removed_cb (Store store, Contact c) { - var data = lookup_data (c); - - if (data.visible) - remove_from_model (data); - - c.remove_lookup (this); + var data = contacts.get (c); + data.grid.destroy (); + data.label.destroy (); + data.image_frame.destroy (); + contacts.unset (c); } - public bool lookup_iter (Contact c, out TreeIter iter) { - var data = lookup_data (c); - iter = data.iter; - return data.visible; + public override void child_selected (Widget? child) { + var data = child.get_data ("data"); + selection_changed (data != null ? data.contact : null); } + private bool filter (Widget child) { + var data = child.get_data ("data"); - - private CellRendererShape shape; - public enum TextDisplay { - NONE, - PRESENCE, - STORES - } - private TextDisplay text_display; - - public signal void selection_changed (Contact? contact); - - private void init_view (TextDisplay text_display) { - this.text_display = text_display; - - set_model (list_store); - set_headers_visible (false); - - var row_padding = 12; - - var selection = get_selection (); - selection.set_mode (SelectionMode.BROWSE); - selection.set_select_function ( (selection, model, path, path_currently_selected) => { - Contact contact; - TreeIter iter; - model.get_iter (out iter, path); - model.get (iter, 0, out contact); - return contact != null; - }); - selection.changed.connect (contacts_selection_changed); - - var column = new TreeViewColumn (); - column.set_spacing (8); - - var icon = new CellRendererPixbuf (); - icon.set_padding (0, row_padding); - icon.xalign = 1.0f; - icon.yalign = 0.0f; - icon.width = Contact.SMALL_AVATAR_SIZE + 12; - column.pack_start (icon, false); - column.set_cell_data_func (icon, (column, cell, model, iter) => { - Contact contact; - - model.get (iter, 0, out contact); - - if (contact == null) { - cell.set ("pixbuf", null); - cell.visible = false; - return; - } - cell.visible = true; - - if (contact != null) - cell.set ("pixbuf", contact.small_avatar); - else - cell.set ("pixbuf", null); - }); - - shape = new CellRendererShape (); - shape.set_padding (0, row_padding); - - Pango.cairo_context_set_shape_renderer (get_pango_context (), shape.render_shape); - - column.pack_start (shape, true); - column.set_cell_data_func (shape, (column, cell, model, iter) => { - Contact contact; - - model.get (iter, 0, out contact); - - if (contact == null) { - cell.visible = false; - return; - } - cell.visible = true; - - var name = contact.display_name; - switch (text_display) { - default: - case TextDisplay.NONE: - cell.set ("name", name, - "show_presence", false, - "message", ""); - break; - case TextDisplay.PRESENCE: - cell.set ("name", name, - "show_presence", true, - "presence", contact.presence_type, - "message", contact.presence_message, - "is_phone", contact.is_phone); - break; - case TextDisplay.STORES: - string stores = contact.format_persona_stores (); - cell.set ("name", name, - "show_presence", false, - "message", stores); - break; - } - }); - - var text = new CellRendererText (); - text.set_alignment (0, 0); - column.pack_start (text, true); - text.set ("weight", Pango.Weight.BOLD); - column.set_cell_data_func (text, (column, cell, model, iter) => { - Contact contact; - - model.get (iter, 0, out contact); - cell.visible = contact == null; - if (cell.visible) { - string header = get_header_text (iter); - cell.set ("text", header); - if (header == "") - cell.height = 6; // PADDING - else - cell.height = -1; - } - }); - - append_column (column); + return data.filtered; } - private void contacts_selection_changed (TreeSelection selection) { - TreeIter iter; - TreeModel model; + private void update_separator (ref Widget? separator, + Widget widget, + Widget? before_widget) { + var w_data = widget.get_data ("data"); + ContactData? before_data = null; + if (before_widget != null) + before_data = before_widget.get_data ("data"); - Contact? contact = null; - if (selection.get_selected (out model, out iter)) { - model.get (iter, 0, out contact); + if (before_data == null && w_data.sort_prio > 0) { + if (separator == null || + !(separator.get_data ("contacts-suggestions-header"))) { + var l = new Label (""); + l.set_data ("contacts-suggestions-header", true); + l.set_markup (Markup.printf_escaped ("%s", _("Suggestions"))); + l.set_halign (Align.START); + separator = l; + } + return; } - selection_changed (contact); - } - - public void select_contact (Contact contact) { - TreeIter iter; - if (lookup_iter (contact, out iter)) { - get_selection ().select_iter (iter); - scroll_to_cell (list_store.get_path (iter), - null, true, 0.0f, 0.0f); + if (before_data != null && before_data.sort_prio > 0 && + w_data.sort_prio == 0) { + if (separator == null || + !(separator.get_data ("contacts-rest-header"))) { + var l = new Label (""); + l.set_data ("contacts-rest-header", true); + l.set_halign (Align.START); + separator = l; + } + return; } + + if (is_other (w_data) && + (before_data == null || !is_other (before_data))) { + if (separator == null || + !(separator.get_data ("contacts-other-header"))) { + var l = new Label (""); + l.set_data ("contacts-other-header", true); + l.set_markup (Markup.printf_escaped ("%s", _("Other Contacts"))); + l.set_halign (Align.START); + separator = l; + } + return; + } + + if (before_data != null && + w_data.initial_letter != before_data.initial_letter) { + if (separator == null || !(separator is Separator)) + separator = new Separator (Orientation.HORIZONTAL); + return; + } + separator = null; } } diff --git a/src/test-sorted.vala b/src/test-sorted.vala new file mode 100644 index 0000000..2af61d5 --- /dev/null +++ b/src/test-sorted.vala @@ -0,0 +1,189 @@ +/* -*- Mode: vala; indent-tabs-mode: t; c-basic-offset: 2; tab-width: 8 -*- */ +/* + * 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 Contacts; + +public void update_separator (ref Widget? separator, + Widget widget, + Widget? before) +{ + if (before == null || + (widget is Label && (widget as Label).get_text () == "blah3")) { + if (separator == null) { + var hbox = new Box(Orientation.HORIZONTAL, 0); + var l = new Label ("Separator"); + hbox.add (l); + var b = new Button.with_label ("button"); + hbox.add (b); + l.show (); + b.show (); + separator = hbox; + } + + var hbox = separator as Box; + var id = widget.get_data("sort_id"); + var l = hbox.get_children ().data as Label; + l.set_text ("Separator %d".printf (id)); + } else { + separator = null; + } + print ("update separator => %p\n", separator); +} + +public static int +compare_label (Widget a, Widget b) { + var aa = a.get_data("sort_id"); + var bb = b.get_data("sort_id"); + return bb - aa; +} + +public static int +compare_label_reverse (Widget a, Widget b) { + return - compare_label (a, b); +} + +public static bool +filter (Widget widget) { + var text = (widget as Label).get_text (); + return strcmp (text, "blah3") != 0; +} + +public static int +main (string[] args) { + + Gtk.init (ref args); + + var w = new Window (); + var hbox = new Box(Orientation.HORIZONTAL, 0); + w.add (hbox); + + var sorted = new Sorted(); + hbox.add (sorted); + + sorted.child_activated.connect ( (child) => { + print ("activated %p\n", child); + }); + + sorted.child_selected.connect ( (child) => { + print ("selected %p\n", child); + }); + + var l = new Label ("blah4"); + l.set_data ("sort_id", 4); + sorted.add (l); + var l3 = new Label ("blah3"); + l3.set_data ("sort_id", 3); + sorted.add (l3); + l = new Label ("blah1"); + l.set_data ("sort_id", 1); + sorted.add (l); + l = new Label ("blah2"); + l.set_data ("sort_id", 2); + sorted.add (l); + + var row_vbox = new Box (Orientation.VERTICAL, 0); + var row_hbox = new Box (Orientation.HORIZONTAL, 0); + row_vbox.set_data ("sort_id", 3); + l = new Label ("da box for da man"); + row_hbox.add (l); + var check = new CheckButton (); + row_hbox.add (check); + var button = new Button.with_label ("ya!"); + row_hbox.add (button); + row_vbox.add (row_hbox); + check = new CheckButton (); + row_vbox.add (check); + sorted.add (row_vbox); + + button = new Button.with_label ("focusable row"); + button.set_hexpand (false); + button.set_halign (Align.START); + sorted.add (button); + + var vbox = new Box(Orientation.VERTICAL, 0); + hbox.add (vbox); + + var b = new Button.with_label ("sort"); + vbox.add (b); + b.clicked.connect ( () => { + sorted.set_sort_func (compare_label); + }); + + b = new Button.with_label ("reverse"); + vbox.add (b); + b.clicked.connect ( () => { + sorted.set_sort_func (compare_label_reverse); + }); + + b = new Button.with_label ("change"); + vbox.add (b); + b.clicked.connect ( () => { + if (l3.get_text () == "blah3") { + l3.set_text ("blah5"); + l3.set_data ("sort_id", 5); + } else { + l3.set_text ("blah3"); + l3.set_data ("sort_id", 3); + } + sorted.child_changed (l3); + }); + + b = new Button.with_label ("filter"); + vbox.add (b); + b.clicked.connect ( () => { + sorted.set_filter_func (filter); + }); + + b = new Button.with_label ("unfilter"); + vbox.add (b); + b.clicked.connect ( () => { + sorted.set_filter_func (null); + }); + + int new_button_nr = 1; + b = new Button.with_label ("add"); + vbox.add (b); + b.clicked.connect ( () => { + var ll = new Label ("blah2 new %d".printf (new_button_nr)); + l.set_data ("sort_id", new_button_nr); + new_button_nr++; + + sorted.add (ll); + l.show (); + }); + + b = new Button.with_label ("separate"); + vbox.add (b); + b.clicked.connect ( () => { + sorted.set_separator_funcs (update_separator); + }); + + b = new Button.with_label ("unseparate"); + vbox.add (b); + b.clicked.connect ( () => { + sorted.set_separator_funcs (null); + }); + + + w.show_all (); + + Gtk.main (); + + return 0; +}