search: Allow searching for people in overview mode

This adds contacts search to shell, powered by libfolks.
Changes:

- Add Folks and Gee to the build system
- ShellContactSystem, a backend in C
- ContactDisplay, search frontend in JS

https://bugzilla.gnome.org/show_bug.cgi?id=643018
This commit is contained in:
Morten Mjelva 2011-08-26 16:15:38 +02:00 committed by Owen W. Taylor
parent 81cee34c17
commit 352fb7b833
9 changed files with 673 additions and 1 deletions

View File

@ -68,6 +68,7 @@ CLUTTER_MIN_VERSION=1.7.5
GOBJECT_INTROSPECTION_MIN_VERSION=0.10.1
GJS_MIN_VERSION=1.29.15
MUTTER_MIN_VERSION=3.0.0
FOLKS_MIN_VERSION=0.5.2
GTK_MIN_VERSION=3.0.0
GIO_MIN_VERSION=2.29.10
LIBECAL_MIN_VERSION=2.32.0
@ -82,6 +83,7 @@ STARTUP_NOTIFICATION_MIN_VERSION=0.11
PKG_CHECK_MODULES(GNOME_SHELL, gio-2.0 >= $GIO_MIN_VERSION
gio-unix-2.0 dbus-glib-1 libxml-2.0
gtk+-3.0 >= $GTK_MIN_VERSION
folks >= $FOLKS_MIN_VERSION
libmutter >= $MUTTER_MIN_VERSION
gjs-internals-1.0 >= $GJS_MIN_VERSION
libgnome-menu-3.0 $recorder_modules gconf-2.0

View File

@ -673,6 +673,11 @@ StTooltip StLabel {
-shell-grid-item-size: 118px;
}
.contact-grid {
spacing: 36px;
-shell-grid-item-size: 272px; /* 2 * -shell-grid-item-size + spacing */
}
.icon-grid .overview-icon {
icon-size: 96px;
}
@ -739,11 +744,57 @@ StTooltip StLabel {
text-align: center;
}
.contact {
width: 272px; /* Same width as two normal results + spacing */
height: 118px; /* Aspect ratio = 1.75. Normal US business card ratio */
border-radius: 4px;
padding: 3px;
border: 1px rgba(0,0,0,0);
transition-duration: 100;
}
.contact-content {
border-radius: 2px;
padding: 8px;
width: 232px;
height: 84px;
background-color: white;
color: black;
text-align: center;
}
.contact-icon {
border-radius: 4px;
}
.contact-details {
padding: 6px 8px 11px 8px;
}
.contact-details-alias {
font-size: 16px;
padding-bottom: 11px;
}
.contact-details-status {
font-size: 11pt;
}
.contact-details-status-icon {
padding-right: 2px;
}
.contact:hover {
background-color: rgba(255,255,255,0.1);
transition-duration: 100;
}
.app-well-app.running > .overview-icon {
text-shadow: black 0px 2px 2px;
background-image: url("running-indicator.svg");
}
.contact:selected,
.app-well-app:selected > .overview-icon,
.search-result-content:selected > .overview-icon {
background-color: rgba(255,255,255,0.33);
@ -757,6 +808,7 @@ StTooltip StLabel {
transition-duration: 100;
}
.contact:focus,
.app-well-app:focus > .overview-icon,
.search-result-content:focus > .overview-icon {
border: 1px solid #cccccc;

View File

@ -22,6 +22,7 @@ nobase_dist_js_DATA = \
ui/autorunManager.js \
ui/boxpointer.js \
ui/calendar.js \
ui/contactDisplay.js \
ui/ctrlAltTab.js \
ui/dash.js \
ui/dateMenu.js \

179
js/ui/contactDisplay.js Normal file
View File

@ -0,0 +1,179 @@
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
const Folks = imports.gi.Folks
const Lang = imports.lang;
const Meta = imports.gi.Meta;
const Shell = imports.gi.Shell;
const St = imports.gi.St;
const Util = imports.misc.util;
const IconGrid = imports.ui.iconGrid;
const Search = imports.ui.search;
const SearchDisplay = imports.ui.searchDisplay;
const MAX_SEARCH_RESULTS_ROWS = 1;
const ICON_SIZE = 81;
function launchContact(id) {
Util.spawn(['gnome-contacts', '-i', id]);
}
/* This class represents a shown contact search result in the overview */
function Contact(id) {
this._init(id);
}
Contact.prototype = {
_init: function(id) {
this.individual = Shell.ContactSystem.get_default().get_individual(id);
this.actor = new St.Bin({ style_class: 'contact',
reactive: true,
track_hover: true });
let content = new St.BoxLayout( { style_class: 'contact-content',
vertical: false });
this.actor.set_child(content);
let icon = new St.Icon({ icon_type: St.IconType.FULLCOLOR,
icon_size: ICON_SIZE,
style_class: 'contact-icon' });
if (this.individual.avatar != null)
icon.gicon = this.individual.avatar;
else
icon.icon_name = 'avatar-default';
content.add(icon, { x_fill: true,
y_fill: false,
x_align: St.Align.START,
y_align: St.Align.MIDDLE });
let details = new St.BoxLayout({ style_class: 'contact-details',
vertical: true });
content.add(details, { x_fill: true,
y_fill: false,
x_align: St.Align.START,
y_align: St.Align.MIDDLE });
let aliasText = this.individual.alias || _("Unknown");
let aliasLabel = new St.Label({ text: aliasText,
style_class: 'contact-details-alias' });
details.add(aliasLabel, { x_fill: true,
y_fill: false,
x_align: St.Align.START,
y_align: St.Align.START });
let presence = this._createPresence(this.individual.presence_type);
details.add(presence, { x_fill: false,
y_fill: true,
x_align: St.Align.START,
y_align: St.Align.END });
},
_createPresence: function(presence) {
let text;
let iconName;
switch(presence) {
case Folks.PresenceType.AVAILABLE:
text = _("Available");
iconName = 'user-available';
break;
case Folks.PresenceType.AWAY:
case Folks.PresenceType.EXTENDED_AWAY:
text = _("Away");
iconName = 'user-away';
break;
case Folks.PresenceType.BUSY:
text = _("Busy");
iconName = 'user-busy';
break;
default:
text = _("Offline");
iconName = 'user-offline';
}
let icon = new St.Icon({ icon_name: iconName,
icon_type: St.IconType.FULLCOLOR,
icon_size: 16,
style_class: 'contact-details-status-icon' });
let label = new St.Label({ text: text });
let box = new St.BoxLayout({ vertical: false,
style_class: 'contact-details-status' });
box.add(icon, { x_fill: true,
y_fill: false,
x_align: St.Align.START,
y_align: St.Align.START });
box.add(label, { x_fill: true,
y_fill: false,
x_align: St.Align.END,
y_align: St.Align.START });
return box;
},
createIcon: function(size) {
let tc = St.TextureCache.get_default();
let icon = this.individual.avatar;
if (icon != null) {
return tc.load_gicon(null, icon, size);
} else {
return tc.load_icon_name(null, 'avatar-default', St.IconType.FULLCOLOR, size);
}
},
};
/* Searches for and returns contacts */
function ContactSearchProvider() {
this._init();
}
ContactSearchProvider.prototype = {
__proto__: Search.SearchProvider.prototype,
_init: function() {
Search.SearchProvider.prototype._init.call(this, _("CONTACTS"));
this._contactSys = Shell.ContactSystem.get_default();
},
getResultMeta: function(id) {
let contact = new Contact(id);
return { 'id': id,
'name': contact.alias,
'createIcon': function(size) {
return contact.createIcon(size);
}
};
},
getInitialResultSet: function(terms) {
return this._contactSys.initial_search(terms);
},
getSubsearchResultSet: function(previousResults, terms) {
return this._contactSys.subsearch(previousResults, terms);
},
createResultActor: function(resultMeta, terms) {
let contact = new Contact(resultMeta.id);
return contact.actor;
},
createResultContainerActor: function() {
let grid = new IconGrid.IconGrid({ rowLimit: MAX_SEARCH_RESULTS_ROWS,
xAlign: St.Align.START });
grid.actor.style_class = 'contact-grid';
let actor = new SearchDisplay.GridSearchResults(this, grid);
return actor;
},
activateResult: function(id, params) {
launchContact(id);
}
};

View File

@ -11,6 +11,7 @@ const Shell = imports.gi.Shell;
const Gdk = imports.gi.Gdk;
const AppDisplay = imports.ui.appDisplay;
const ContactDisplay = imports.ui.contactDisplay;
const Dash = imports.ui.dash;
const DND = imports.ui.dnd;
const DocDisplay = imports.ui.docDisplay;
@ -211,6 +212,7 @@ Overview.prototype = {
this._viewSelector.addSearchProvider(new AppDisplay.SettingsSearchProvider());
this._viewSelector.addSearchProvider(new PlaceDisplay.PlaceSearchProvider());
this._viewSelector.addSearchProvider(new DocDisplay.DocSearchProvider());
this._viewSelector.addSearchProvider(new ContactDisplay.ContactSearchProvider());
// TODO - recalculate everything when desktop size changes
this._dash = new Dash.Dash();

View File

@ -102,6 +102,7 @@ shell_public_headers_h = \
shell-app-system.h \
shell-app-usage.h \
shell-arrow.h \
shell-contact-system.h \
shell-doc-system.h \
shell-embedded-window.h \
shell-generic-container.h \
@ -137,6 +138,7 @@ libgnome_shell_la_SOURCES = \
shell-app-system.c \
shell-app-usage.c \
shell-arrow.c \
shell-contact-system.c \
shell-doc-system.c \
shell-embedded-window.c \
shell-generic-container.c \
@ -269,7 +271,7 @@ libgnome_shell_la_LIBADD = \
libgnome_shell_la_CPPFLAGS = $(gnome_shell_cflags)
Shell-0.1.gir: libgnome-shell.la St-1.0.gir
Shell_0_1_gir_INCLUDES = Clutter-1.0 ClutterX11-1.0 Meta-3.0 TelepathyGLib-0.12 TelepathyLogger-0.2 Soup-2.4 GMenu-3.0 NetworkManager-1.0 NMClient-1.0
Shell_0_1_gir_INCLUDES = Clutter-1.0 ClutterX11-1.0 Meta-3.0 TelepathyGLib-0.12 TelepathyLogger-0.2 Soup-2.4 GMenu-3.0 NetworkManager-1.0 NMClient-1.0 Folks-0.6
Shell_0_1_gir_CFLAGS = $(libgnome_shell_la_CPPFLAGS) -I $(srcdir)
Shell_0_1_gir_LIBS = libgnome-shell.la
Shell_0_1_gir_FILES = $(libgnome_shell_la_gir_sources)

350
src/shell-contact-system.c Normal file
View File

@ -0,0 +1,350 @@
/* This implements a complete suite for caching and searching contacts in the
* Shell. We retrieve contacts from libfolks asynchronously and we search
* these for display to the user. */
#include "shell-contact-system.h"
#include <glib.h>
#include <glib/gprintf.h>
#include <gee.h>
#include <clutter/clutter.h>
#include <folks/folks.h>
#include "shell-global.h"
#include "shell-util.h"
#include "st.h"
G_DEFINE_TYPE (ShellContactSystem, shell_contact_system, G_TYPE_OBJECT);
#define ALIAS_PREFIX_MATCH_WEIGHT 100
#define ALIAS_SUBSTRING_MATCH_WEIGHT 90
#define IM_PREFIX_MATCH_WEIGHT 10
#define IM_SUBSTRING_MATCH_WEIGHT 5
/* Callbacks */
static void
prepare_individual_aggregator_cb (GObject *obj,
GAsyncResult *res,
gpointer user_data)
{
FolksIndividualAggregator *aggregator = FOLKS_INDIVIDUAL_AGGREGATOR (obj);
folks_individual_aggregator_prepare_finish (aggregator, res, NULL);
}
/* Internal stuff */
typedef struct {
gchar *key;
guint weight;
} ContactSearchResult;
struct _ShellContactSystemPrivate {
FolksIndividualAggregator *aggregator;
};
static void
shell_contact_system_constructed (GObject *obj)
{
ShellContactSystem *self = SHELL_CONTACT_SYSTEM (obj);
G_OBJECT_CLASS (shell_contact_system_parent_class)->constructed (obj);
/* We intentionally do not care about the "individuals-changed" signal, as
* we don't intend to update searches after they've been performed.
* Therefore, we will simply retrieve the "individuals" property which
* represents a snapshot of the individuals in the aggregator.
*/
self->priv->aggregator = folks_individual_aggregator_new ();
folks_individual_aggregator_prepare (self->priv->aggregator, prepare_individual_aggregator_cb, NULL);
}
static void
shell_contact_system_finalize (GObject *obj)
{
ShellContactSystem *self = SHELL_CONTACT_SYSTEM (obj);
g_object_unref (self->priv->aggregator);
G_OBJECT_CLASS (shell_contact_system_parent_class)->finalize (obj);
}
static void
shell_contact_system_init (ShellContactSystem *self)
{
self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, SHELL_TYPE_CONTACT_SYSTEM, ShellContactSystemPrivate);
}
static void
shell_contact_system_class_init (ShellContactSystemClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->constructed = shell_contact_system_constructed;
object_class->finalize = shell_contact_system_finalize;
g_type_class_add_private (object_class, sizeof (ShellContactSystemPrivate));
}
/**
* normalize_terms:
* @terms: (element-type utf8): Input search terms
*
* Returns: (element-type utf8) (transfer full): Unicode-normalized and lowercased terms
*/
static GSList *
normalize_terms (GSList *terms)
{
GSList *normalized_terms = NULL;
GSList *iter;
for (iter = terms; iter; iter = iter->next)
{
const char *term = iter->data;
normalized_terms = g_slist_prepend (normalized_terms, shell_util_normalize_and_casefold (term));
}
return normalized_terms;
}
static guint
do_match (ShellContactSystem *self,
FolksIndividual *individual,
GSList *terms)
{
GSList *term_iter;
guint weight = 0;
char *alias = shell_util_normalize_and_casefold (folks_alias_details_get_alias (FOLKS_ALIAS_DETAILS (individual)));
GeeMultiMap *im_addr_map = folks_im_details_get_im_addresses (FOLKS_IM_DETAILS (individual));
GeeCollection *im_addrs = gee_multi_map_get_values (im_addr_map);
GeeIterator *im_addrs_iter;
gboolean have_alias_prefix = FALSE;
gboolean have_alias_substring = FALSE;
gboolean have_im_prefix = FALSE;
gboolean have_im_substring = FALSE;
for (term_iter = terms; term_iter; term_iter = term_iter->next)
{
const char *term = term_iter->data;
const char *p;
/* Match on alias */
p = strstr (alias, term);
if (p == alias)
have_alias_prefix = TRUE;
else if (p != NULL)
have_alias_substring = TRUE;
/* Match on one or more IM addresses */
im_addrs_iter = gee_iterable_iterator (GEE_ITERABLE (im_addrs));
while (gee_iterator_next (im_addrs_iter))
{
const gchar *addr = gee_iterator_get (im_addrs_iter);
p = strstr (addr, term);
if (p == addr)
have_im_prefix = TRUE;
else if (p != NULL)
have_im_substring = TRUE;
}
g_object_unref (im_addrs_iter);
}
if (have_alias_prefix)
weight += ALIAS_PREFIX_MATCH_WEIGHT;
else if (have_alias_substring)
weight += ALIAS_SUBSTRING_MATCH_WEIGHT;
if (have_im_prefix)
weight += IM_PREFIX_MATCH_WEIGHT;
else if (have_im_substring)
weight += IM_SUBSTRING_MATCH_WEIGHT;
g_free (alias);
g_object_unref (im_addrs);
return weight;
}
static gint
compare_results (gconstpointer a,
gconstpointer b)
{
ContactSearchResult *first = (ContactSearchResult *) a;
ContactSearchResult *second = (ContactSearchResult *) b;
if (first->weight > second->weight)
return 1;
else if (first->weight < second->weight)
return -1;
else
return 0;
}
static void
free_result (gpointer data,
gpointer user_data)
{
g_slice_free (ContactSearchResult, data);
}
/* modifies and frees @results */
static GSList *
sort_and_prepare_results (GSList *results)
{
GSList *iter;
GSList *sorted_results = NULL;
results = g_slist_sort (results, compare_results);
for (iter = results; iter; iter = iter->next)
{
ContactSearchResult *result = iter->data;
gchar *id = result->key;
sorted_results = g_slist_prepend (sorted_results, id);
}
g_slist_foreach (results, (GFunc) free_result, NULL);
return sorted_results;
}
/* Methods */
/**
* shell_contact_system_get_default:
*
* Return Value: (transfer none): The global #ShellContactSystem singleton
*/
ShellContactSystem *
shell_contact_system_get_default (void)
{
static ShellContactSystem *instance = NULL;
if (instance == NULL)
instance = g_object_new (SHELL_TYPE_CONTACT_SYSTEM, NULL);
return instance;
}
/**
* shell_contact_system_get_all:
* @self: A #ShellContactSystem
*
* Returns: (transfer none): All individuals
*/
GeeMap *
shell_contact_system_get_all (ShellContactSystem *self)
{
GeeMap *individuals;
g_return_val_if_fail (SHELL_IS_CONTACT_SYSTEM (self), NULL);
individuals = folks_individual_aggregator_get_individuals (self->priv->aggregator);
return individuals;
}
/**
* shell_contact_system_get_individual:
* @self: A #ShellContactSystem
* @id: A #gchar with the ID of the FolksIndividual to be returned.
*
* Returns: (transfer full): A #FolksIndividual or NULL if @id could not be found.
*/
FolksIndividual *
shell_contact_system_get_individual (ShellContactSystem *self,
gchar *id)
{
GeeMap *individuals;
gpointer key, value;
key = (gpointer) id;
g_return_val_if_fail (SHELL_IS_CONTACT_SYSTEM (self), NULL);
individuals = folks_individual_aggregator_get_individuals (self->priv->aggregator);
value = gee_map_get (individuals, key);
return FOLKS_INDIVIDUAL (value);
}
/**
* shell_contact_system_initial_search:
* @shell: A #ShellContactSystem
* @terms: (element-type utf8): List of terms, logical AND
*
* Search through contacts for the given search terms.
*
* Returns: (transfer container) (element-type utf8): List of contact
* identifiers
*/
GSList *
shell_contact_system_initial_search (ShellContactSystem *self,
GSList *terms)
{
FolksIndividual *individual;
GSList *results = NULL;
GeeMap *individuals = NULL;
ContactSearchResult *result;
GeeMapIterator *iter;
gpointer key;
guint weight;
GSList *normalized_terms = normalize_terms (terms);
g_return_val_if_fail (SHELL_IS_CONTACT_SYSTEM (self), NULL);
individuals = folks_individual_aggregator_get_individuals (self->priv->aggregator);
iter = gee_map_map_iterator (individuals);
while (gee_map_iterator_next (iter))
{
individual = gee_map_iterator_get_value (iter);
weight = do_match (self, individual, normalized_terms);
if (weight != 0)
{
key = gee_map_iterator_get_key (iter);
result = g_slice_new (ContactSearchResult);
result->key = (gchar *) key;
result->weight = weight;
results = g_slist_append (results, result);
}
g_object_unref (individual);
}
return sort_and_prepare_results (results);
}
/**
* shell_contact_system_subsearch:
* @shell: A #ShellContactSystem
* @previous_results: (element-type utf8): List of previous results
* @terms: (element-type utf8): List of terms, logical AND
*
* Search through a previous result set; for more information see
* js/ui/search.js.
*
* Returns: (transfer container) (element-type utf8): List of contact
* identifiers
*/
GSList *
shell_contact_system_subsearch (ShellContactSystem *self,
GSList *previous_results,
GSList *terms)
{
return shell_contact_system_initial_search (self, terms);
}

View File

@ -0,0 +1,50 @@
/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
#ifndef __SHELL_CONTACT_SYSTEM_H__
#define __SHELL_CONTACT_SYSTEM_H__
#include <clutter/clutter.h>
#include <gio/gio.h>
#include <folks/folks.h>
#define SHELL_TYPE_CONTACT_SYSTEM (shell_contact_system_get_type ())
#define SHELL_CONTACT_SYSTEM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SHELL_TYPE_CONTACT_SYSTEM, ShellContactSystem))
#define SHELL_CONTACT_SYSTEM_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SHELL_TYPE_CONTACT_SYSTEM, ShellContactSystemClass))
#define SHELL_IS_CONTACT_SYSTEM(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SHELL_TYPE_CONTACT_SYSTEM))
#define SHELL_IS_CONTACT_SYSTEM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SHELL_TYPE_CONTACT_SYSTEM))
#define SHELL_CONTACT_SYSTEM_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SHELL_TYPE_CONTACT_SYSTEM, ShellContactSystemClass))
typedef struct _ShellContactSystem ShellContactSystem;
typedef struct _ShellContactSystemClass ShellContactSystemClass;
typedef struct _ShellContactSystemPrivate ShellContactSystemPrivate;
struct _ShellContactSystem
{
GObject parent;
ShellContactSystemPrivate *priv;
};
struct _ShellContactSystemClass
{
GObjectClass parent_class;
};
GType shell_contact_system_get_type (void) G_GNUC_CONST;
/* Methods */
ShellContactSystem * shell_contact_system_get_default (void);
GeeMap *shell_contact_system_get_all (ShellContactSystem *self);
FolksIndividual *shell_contact_system_get_individual (ShellContactSystem *self,
gchar *id);
GSList * shell_contact_system_initial_search (ShellContactSystem *shell,
GSList *terms);
GSList * shell_contact_system_subsearch (ShellContactSystem *shell,
GSList *previous_results,
GSList *terms);
#endif /* __SHELL_CONTACT_SYSTEM_H__ */

View File

@ -16,6 +16,38 @@
<repository type="git" name="colord.gitorious.org"
href="git://gitorious.org/colord/master.git"/>
<tarball id="libgee" version="0.6.0">
<pkg-config/>
<source hash="sha256:e586678d0a88637abeaaf850b62231000772e79ea6d9c4b45dc3cea99f778a7a" href="http://download.gnome.org/sources/libgee/0.6/libgee-0.6.0.tar.bz2" md5sum="4eb513b23ab6ea78884989518a4acf6f" size="477609"/>
<dependencies>
<dep package="glib"/>
</dependencies>
<suggests>
<dep package="gobject-introspection"/>
</suggests>
</tarball>
<tarball id="folks" version="0.6.0">
<source hash="sha256:50539da0b42564887bbce67d3a4107f3a7dbd8c6ae3b5d3a2d422b4f75a2b519" href="http://ftp.gnome.org/pub/GNOME/sources/folks/0.6/folks-0.6.0.tar.bz2" md5sum="4d5a3619c8b6e0714d02941c92826160" size="1261916"/>
<dependencies>
<dep package="libgee"/>
<dep package="vala"/>
<dep package="gobject-introspection"/>
<dep package="telepathy-glib"/>
<dep package="evolution-data-server"/>
</dependencies>
</tarball>
<tarball id="gnome-contacts">
<source hash="sha256:db52d4521e5d3335b5bf77ce5d1a7ec2cdea9ebf37ce12ddadda0503a2968cf9" href="http://ftp.gnome.org/pub/GNOME/sources/gnome-contacts/0.1/gnome-contacts-0.1.2.tar.bz2" size="377930"/>
<dependencies>
<dep package="folks"/>
<dep package="glib"/>
<dep package="gtk3"/>
<dep package="vala"/>
</dependencies>
</tarball>
<autotools id="gobject-introspection">
<branch repo="git.gnome.org" module="gobject-introspection"/>
<dependencies>
@ -320,8 +352,10 @@
<dep package="gjs"/>
<dep package="caribou"/>
<dep package="dconf"/>
<dep package="folks"/>
<dep package="gconf"/>
<dep package="glib"/>
<dep package="gnome-contacts"/>
<dep package="gnome-menus"/>
<dep package="gnome-desktop-3"/>
<dep package="gsettings-desktop-schemas"/>