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