Compare commits

...
Sign in to create a new pull request.

51 commits

Author SHA1 Message Date
Alexander Larsson
6f4030530b Remove unused typedef 2012-05-15 11:28:31 +02:00
Alexander Larsson
aa084b0111 Don't use Gee in Sorted 2012-05-15 11:26:09 +02:00
Alexander Larsson
d09f179b9d Don't use deprecated functions in Sorted 2012-05-15 11:04:38 +02:00
Alexander Larsson
dba1129de7 Remove now unused Contacts.CellRendererShape 2012-05-15 11:01:21 +02:00
Alexander Larsson
bc0eaef4f1 Implement text_display 2012-05-15 11:00:04 +02:00
Alexander Larsson
4c1289d637 Implemente all header separators in ContactsView 2012-05-15 10:48:25 +02:00
Alexander Larsson
5acaabc929 Update test 2012-05-15 10:48:13 +02:00
Alexander Larsson
976191f057 Merge need_separator into update_separator 2012-05-15 10:47:54 +02:00
Alexander Larsson
49dfd78298 Remove some now unused vars in Contacts.View 2012-05-14 21:38:06 +02:00
Alexander Larsson
62ce9fc103 Fix up separator handling 2012-05-14 17:19:09 +02:00
Alexander Larsson
629da7939a Should not need extra reseparate as it happens in refilter already 2012-05-14 17:16:02 +02:00
Alexander Larsson
3afc08f557 Update view for separator changes 2012-05-14 17:06:34 +02:00
Alexander Larsson
b7106199d4 update test for separator func changes 2012-05-14 17:06:17 +02:00
Alexander Larsson
5cf772ef71 Switch to only one separator func 2012-05-14 17:05:52 +02:00
Alexander Larsson
bb54cc89a6 Stop in list at ends 2012-05-14 16:30:19 +02:00
Alexander Larsson
2d5e2ad20e Delay showing of link hints to make scrolling faster
Actually finding the matches takes some time which causes keynav
to become pretty slow.
2012-05-14 16:19:57 +02:00
Alexander Larsson
d5a40ab91b Fix up non-match </span> in markup 2012-05-14 15:37:14 +02:00
Alexander Larsson
dba1a863f3 Use BROWSE selection mode 2012-05-14 15:11:50 +02:00
Alexander Larsson
1ab1cf76f9 Support pgup/down 2012-05-14 15:09:57 +02:00
Alexander Larsson
01df16164a Fix up ctrl up/down inside scrolled window 2012-05-14 12:18:40 +02:00
Alexander Larsson
78d218cb2f Test multiline rows 2012-05-14 12:18:22 +02:00
Alexander Larsson
cc51439982 Properly escape strings passed to set_markup 2012-05-14 11:58:45 +02:00
Alexander Larsson
7217975b00 Remove unnecessary workaround define
No need for get_preferred_height_for_width_internal define anymore as vala seems
to have been fixed.
2012-05-14 10:58:17 +02:00
Alexander Larsson
6da20da524 Use get_preferred_width_internal() 2012-05-14 10:57:49 +02:00
Alexander Larsson
6bfb93f1c0 Fix up ContactsView layout 2012-05-11 16:50:03 +02:00
Alexander Larsson
16e60db53e Update ContactPresense when persona set changes
We might have added or removed a telepathy persona
2012-05-11 16:49:21 +02:00
Alexander Larsson
d4958d150d warn, not error on MULTIPLE selections 2012-05-11 16:03:16 +02:00
Alexander Larsson
e6ec0e8b4a Use focus_vadj for list view 2012-05-11 16:00:02 +02:00
Alexander Larsson
7600e8f1d2 Support focus_child 2012-05-11 15:52:02 +02:00
Alexander Larsson
3ecb5b4bc4 Don't remove data.grid twice 2012-05-11 15:39:39 +02:00
Alexander Larsson
b56d38b78d Remove iter with on container.remove 2012-05-11 15:38:56 +02:00
Alexander Larsson
c46616cdb5 Update todos 2012-05-11 14:40:52 +02:00
Alexander Larsson
a06929cb46 Don't select on button press but on release (like a button) 2012-05-11 14:40:32 +02:00
Alexander Larsson
087200ef2c Support selection_mode 2012-05-11 14:29:47 +02:00
Alexander Larsson
3076139b41 Test child selection and activation signals 2012-05-11 14:29:06 +02:00
Alexander Larsson
b45ddb74b5 Add child_activate signal 2012-05-11 13:19:45 +02:00
Alexander Larsson
67e45d169a Add initial keyboard navigation 2012-05-11 10:43:24 +02:00
Alexander Larsson
3ac7deb791 Add update_focus helper 2012-05-11 10:12:50 +02:00
Alexander Larsson
620bacad69 Make space for focus line and padding 2012-05-10 21:50:53 +02:00
Alexander Larsson
d263103e38 Support focus handling 2012-05-10 21:50:21 +02:00
Alexander Larsson
9c20c4f8bd Redraw on allocate (we paint the bg) 2012-05-10 20:59:04 +02:00
Alexander Larsson
36aefcf17c Track prelighted rows
This needs us to be a widget window, otherwise we can't track prelight
when inside inferior windows.
2012-05-10 20:59:04 +02:00
Alexander Larsson
a5dbd116bc Add more complex child widgets to test 2012-05-10 20:59:00 +02:00
Alexander Larsson
2a3448f204 Add presence in new ContactsView 2012-05-09 12:09:31 +02:00
Alexander Larsson
e31fb4588e sorted: Fix null check in update_separator 2012-05-09 12:09:31 +02:00
Alexander Larsson
bd8316edee Initial use of ContactsSorted for view 2012-05-09 12:09:27 +02:00
Alexander Larsson
02d79e9779 Track selection 2012-05-09 11:39:57 +02:00
Alexander Larsson
e2d418384c sorted: Add input only window so we can get events 2012-05-09 11:39:57 +02:00
Alexander Larsson
e8d9f74a1f Fix up separator handling when things move 2012-05-09 11:39:57 +02:00
Alexander Larsson
0ca78f5ffa Add separator support 2012-05-09 11:39:57 +02:00
Alexander Larsson
ec2939c8a9 Initial version of ContactsSorted 2012-05-09 11:39:57 +02:00
12 changed files with 1326 additions and 746 deletions

View file

@ -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

View file

@ -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) {

View file

@ -266,7 +266,7 @@ public class Contacts.AvatarDialog : Dialog {
grid.attach (main_frame, 0, 0, 1, 1);
var label = new Label ("");
label.set_markup ("<span font='16'>" + contact.display_name + "</span>");
label.set_markup (Markup.printf_escaped ("<span font='16'>%s</span>", contact.display_name));
label.set_valign (Align.START);
label.set_halign (Align.START);
label.set_hexpand (true);

View file

@ -1,330 +0,0 @@
/* -*- Mode: vala; indent-tabs-mode: t; c-basic-offset: 2; tab-width: 8 -*- */
/*
* Copyright (C) 2011 Alexander Larsson <alexl@redhat.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using Gtk;
using Folks;
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<IconShape?>.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<IconShape?>.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<IconShape?> sattr = (Pango.AttrShape<IconShape?>)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();
}
}
}

View file

@ -204,7 +204,7 @@ public class Contacts.FieldRow : Contacts.Row {
public void pack_header (string s) {
var l = new Label (s);
l.set_markup (
"<span font='24px'>%s</span>".printf (s));
Markup.printf_escaped ("<span font='24px'>%s</span>", 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 (
"<span font='24px'>%s</span>".printf (s));
Markup.printf_escaped ("<span font='24px'>%s</span>", 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 ("<span font='16'>" + contact.display_name + "</span>");
(w as Label).set_markup (Markup.printf_escaped ("<span font='16'>%s</span>", 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 ("<span font='16'>" + entry.get_text () + "</span>");
l.set_markup (Markup.printf_escaped ("<span font='16'>%s</span>", 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 ("<span font='16'>" + contact.display_name + "</span>");
l.set_markup (Markup.printf_escaped ("<span font='16'>%s</span>", 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;

View file

@ -65,7 +65,7 @@ public class Contacts.ContactPresence : Grid {
if (message.length == 0)
message = Contact.presence_to_string (type);
label.set_markup ("<span font='11px'>" + message + "</span>");
label.set_markup (Markup.printf_escaped ("<span font='11px'>%s</span>", 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);
});
}
}

View file

@ -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 ("<span font='13'>" + selected_contact.display_name + "</span>");
label.set_markup (Markup.printf_escaped ("<span font='13'>%s</span>", 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 ("<span font='9'>" +selected_contact.format_persona_stores () + "</span>");
label.set_markup (Markup.printf_escaped ("<span font='9'>%s</span>", 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 (_("<span weight='bold'>Link contacts to %s</span>").printf (contact.display_name));
label.set_markup (Markup.printf_escaped (_("<span weight='bold'>Link contacts to %s</span>"), contact.display_name));
else
label.set_markup (_("<span weight='bold'>Select contact to link to</span>"));
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;

View file

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

View file

@ -147,7 +147,7 @@ public class Contacts.SetupWindow : Gtk.Window {
var item = new ToolItem ();
title_label = new Label ("");
title_label.set_markup ("<b>%s</b>".printf (_("Contacts Setup")));
title_label.set_markup (Markup.printf_escaped ("<b>%s</b>",_("Contacts Setup")));
title_label.set_no_show_all (true);
item.add (title_label);
item.set_expand (true);

917
src/contacts-sorted.vala Normal file
View file

@ -0,0 +1,917 @@
/* -*- Mode: vala; indent-tabs-mode: t; c-basic-offset: 2; tab-width: 8 -*- */
/*
* Copyright (C) 2011 Alexander Larsson <alexl@redhat.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using Gtk;
/* 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<ChildInfo?> iter;
int y;
int height;
}
Sequence<ChildInfo?> children;
HashTable<unowned Widget, unowned ChildInfo?> child_hash;
CompareDataFunc<Widget>? 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<ChildInfo?>();
child_hash = new HashTable<unowned Widget, unowned ChildInfo?> (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<ChildInfo?>? 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<ChildInfo?>? 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<ChildInfo?>? get_previous_visible (SequenceIter<ChildInfo?> _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<ChildInfo?>? get_next_visible (SequenceIter<ChildInfo?> _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<ChildInfo?>? 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<Widget>? 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<ChildInfo?> 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;
}
}
}

View file

@ -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<Contact,ContactData> contacts;
HashSet<Contact> 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<Contact>();
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<Contact,ContactData> ();
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<ContactData> ("data");
var b = widget_b.get_data<ContactData> ("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 ("<span font='16px'>%s</span>", 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<ContactData> (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 ("<span font='12px'>%s</span>",
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<ContactData> ("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<ContactData> (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<ContactData> ("data");
selection_changed (data != null ? data.contact : null);
}
private bool filter (Widget child) {
var data = child.get_data<ContactData> ("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<ContactData> ("data");
ContactData? before_data = null;
if (before_widget != null)
before_data = before_widget.get_data<ContactData> ("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<bool> ("contacts-suggestions-header"))) {
var l = new Label ("");
l.set_data ("contacts-suggestions-header", true);
l.set_markup (Markup.printf_escaped ("<b>%s</b>", _("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<bool> ("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<bool> ("contacts-other-header"))) {
var l = new Label ("");
l.set_data ("contacts-other-header", true);
l.set_markup (Markup.printf_escaped ("<b>%s</b>", _("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;
}
}

189
src/test-sorted.vala Normal file
View file

@ -0,0 +1,189 @@
/* -*- Mode: vala; indent-tabs-mode: t; c-basic-offset: 2; tab-width: 8 -*- */
/*
* Copyright (C) 2011 Alexander Larsson <alexl@redhat.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using Gtk;
using 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<int>("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<int>("sort_id");
var bb = b.get_data<int>("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;
}