2001-08-09 Damon Chaplin <damon@ximian.com> * pcs/cal-backend.c (cal_backend_get_object_component): added new backend method to get the component given a UID. * pcs/cal-backend-file.c (cal_backend_file_get_object_component): added implementation of above virtual method. * pcs/query.c (match_component): use the new backend function to get the CalComponent rather than the string. This avoids converting all the calendar components to strings and parsing them back into components for every query! (That wasn't a good idea, was it ;) * gui/e-week-view.c: * gui/e-day-view.c: use a timeout handler to layout the events, to avoid doing a layout for each event we get from a query. * gui/print.c (print_day_add_event): * gui/e-day-view.c (e_day_view_add_event): set start_row_or_col and num_columns to 0. They are guint8's. * gui/e-week-view.c (e_week_view_free_events): hide all the jump buttons. Fixes bug #5946. * gui/calendar-commands.c (calendar_set_folder_bar_label): added the day numbers for the month view. * gui/dialogs/recurrence-page.glade: changed "_Delete" to "_Remove", since it clashed with "_Add". Also added underlined accelerators for the recurrence radio buttons. Note that none of these accelerators actually work at present, due to the way we are using .glade files for each notebook page. I need to add a bug about this. Also, the "_Action" menu doesn't popup when I press Alt+A, even though the "_File" menu does popup when I press Alt+F. Strange. * pcs/cal-backend-file.c (cal_backend_file_get_timezone_object): removed debug msgs. svn path=/trunk/; revision=11866
1251 lines
28 KiB
C
1251 lines
28 KiB
C
/* Evolution calendar - Live search query implementation
|
||
*
|
||
* Copyright (C) 2001 Ximian, Inc.
|
||
*
|
||
* Author: Federico Mena-Quintero <federico@ximian.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, write to the Free Software
|
||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
|
||
*/
|
||
|
||
#ifdef HAVE_CONFIG_H
|
||
#include <config.h>
|
||
#endif
|
||
|
||
#include <string.h>
|
||
#include <glib.h>
|
||
#include <libgnome/gnome-defs.h>
|
||
#include <libgnome/gnome-i18n.h>
|
||
#include <gtk/gtksignal.h>
|
||
#include <gal/widgets/e-unicode.h>
|
||
#include <e-util/e-sexp.h>
|
||
#include <cal-util/cal-recur.h>
|
||
#include <cal-util/timeutil.h>
|
||
#include "query.h"
|
||
|
||
|
||
|
||
/* Private part of the Query structure */
|
||
struct _QueryPrivate {
|
||
/* The backend we are monitoring */
|
||
CalBackend *backend;
|
||
|
||
/* Listener to which we report changes in the live query */
|
||
GNOME_Evolution_Calendar_QueryListener ql;
|
||
|
||
/* Sexp that defines the query */
|
||
char *sexp;
|
||
ESExp *esexp;
|
||
|
||
/* Idle handler ID for asynchronous queries */
|
||
guint idle_id;
|
||
|
||
/* List of UIDs that we still have to process */
|
||
GList *pending_uids;
|
||
int n_pending;
|
||
int pending_total;
|
||
|
||
/* Table of the UIDs we know do match the query */
|
||
GHashTable *uids;
|
||
|
||
/* The next component that will be handled in e_sexp_eval(); put here
|
||
* just because the query object itself is the esexp context.
|
||
*/
|
||
CalComponent *next_comp;
|
||
};
|
||
|
||
|
||
|
||
static void query_class_init (QueryClass *class);
|
||
static void query_init (Query *query);
|
||
static void query_destroy (GtkObject *object);
|
||
|
||
static BonoboXObjectClass *parent_class;
|
||
|
||
|
||
|
||
BONOBO_X_TYPE_FUNC_FULL (Query,
|
||
GNOME_Evolution_Calendar_Query,
|
||
BONOBO_X_OBJECT_TYPE,
|
||
query);
|
||
|
||
/* Class initialization function for the live search query */
|
||
static void
|
||
query_class_init (QueryClass *class)
|
||
{
|
||
GtkObjectClass *object_class;
|
||
|
||
object_class = (GtkObjectClass *) class;
|
||
|
||
parent_class = gtk_type_class (BONOBO_X_OBJECT_TYPE);
|
||
|
||
object_class->destroy = query_destroy;
|
||
|
||
/* The Query interface (ha ha! query interface!) has no methods, so we
|
||
* don't need to fiddle with the epv.
|
||
*/
|
||
}
|
||
|
||
/* Object initialization function for the live search query */
|
||
static void
|
||
query_init (Query *query)
|
||
{
|
||
QueryPrivate *priv;
|
||
|
||
priv = g_new0 (QueryPrivate, 1);
|
||
query->priv = priv;
|
||
|
||
priv->backend = NULL;
|
||
priv->ql = CORBA_OBJECT_NIL;
|
||
priv->sexp = NULL;
|
||
|
||
priv->pending_uids = NULL;
|
||
priv->uids = g_hash_table_new (g_str_hash, g_str_equal);
|
||
|
||
priv->next_comp = NULL;
|
||
}
|
||
|
||
/* Used from g_hash_table_foreach(); frees a UID */
|
||
static void
|
||
free_uid_cb (gpointer key, gpointer value, gpointer data)
|
||
{
|
||
char *uid;
|
||
|
||
uid = key;
|
||
g_free (uid);
|
||
}
|
||
|
||
/* Destroy handler for the live search query */
|
||
static void
|
||
query_destroy (GtkObject *object)
|
||
{
|
||
Query *query;
|
||
QueryPrivate *priv;
|
||
|
||
g_return_if_fail (object != NULL);
|
||
g_return_if_fail (IS_QUERY (object));
|
||
|
||
query = QUERY (object);
|
||
priv = query->priv;
|
||
|
||
if (priv->backend) {
|
||
gtk_signal_disconnect_by_data (GTK_OBJECT (priv->backend), query);
|
||
gtk_object_unref (GTK_OBJECT (priv->backend));
|
||
priv->backend = NULL;
|
||
}
|
||
|
||
if (priv->ql != CORBA_OBJECT_NIL) {
|
||
CORBA_Environment ev;
|
||
|
||
CORBA_exception_init (&ev);
|
||
bonobo_object_release_unref (priv->ql, &ev);
|
||
|
||
if (ev._major != CORBA_NO_EXCEPTION)
|
||
g_message ("query_destroy(): Could not unref the listener\n");
|
||
|
||
CORBA_exception_free (&ev);
|
||
|
||
priv->ql = CORBA_OBJECT_NIL;
|
||
}
|
||
|
||
if (priv->sexp) {
|
||
g_free (priv->sexp);
|
||
priv->sexp = NULL;
|
||
}
|
||
|
||
if (priv->esexp) {
|
||
e_sexp_unref (priv->esexp);
|
||
priv->esexp = NULL;
|
||
}
|
||
|
||
if (priv->idle_id) {
|
||
g_source_remove (priv->idle_id);
|
||
priv->idle_id = 0;
|
||
}
|
||
|
||
if (priv->pending_uids) {
|
||
GList *l;
|
||
|
||
for (l = priv->pending_uids; l; l = l->next) {
|
||
char *uid;
|
||
|
||
uid = l->data;
|
||
g_assert (uid != NULL);
|
||
g_free (uid);
|
||
}
|
||
|
||
g_list_free (priv->pending_uids);
|
||
priv->pending_uids = NULL;
|
||
priv->n_pending = 0;
|
||
}
|
||
|
||
g_hash_table_foreach (priv->uids, free_uid_cb, NULL);
|
||
g_hash_table_destroy (priv->uids);
|
||
priv->uids = NULL;
|
||
|
||
g_free (priv);
|
||
query->priv = NULL;
|
||
|
||
if (GTK_OBJECT_CLASS (parent_class)->destroy)
|
||
(* GTK_OBJECT_CLASS (parent_class)->destroy) (object);
|
||
}
|
||
|
||
|
||
|
||
/* E-Sexp functions */
|
||
|
||
/* (time-now)
|
||
*
|
||
* Returns a time_t of time (NULL).
|
||
*/
|
||
static ESExpResult *
|
||
func_time_now (ESExp *esexp, int argc, ESExpResult **argv, void *data)
|
||
{
|
||
ESExpResult *result;
|
||
|
||
if (argc != 0) {
|
||
e_sexp_fatal_error (esexp, _("time-now expects 0 arguments"));
|
||
return NULL;
|
||
}
|
||
|
||
result = e_sexp_result_new (esexp, ESEXP_RES_TIME);
|
||
result->value.time = time (NULL);
|
||
|
||
return result;
|
||
}
|
||
|
||
/* (make-time ISODATE)
|
||
*
|
||
* ISODATE - string, ISO 8601 date/time representation
|
||
*
|
||
* Constructs a time_t value for the specified date.
|
||
*/
|
||
static ESExpResult *
|
||
func_make_time (ESExp *esexp, int argc, ESExpResult **argv, void *data)
|
||
{
|
||
const char *str;
|
||
time_t t;
|
||
ESExpResult *result;
|
||
|
||
if (argc != 1) {
|
||
e_sexp_fatal_error (esexp, _("make-time expects 1 argument"));
|
||
return NULL;
|
||
}
|
||
|
||
if (argv[0]->type != ESEXP_RES_STRING) {
|
||
e_sexp_fatal_error (esexp, _("make-time expects argument 1 "
|
||
"to be a string"));
|
||
return NULL;
|
||
}
|
||
str = argv[0]->value.string;
|
||
|
||
t = time_from_isodate (str);
|
||
if (t == -1) {
|
||
e_sexp_fatal_error (esexp, _("make-time argument 1 must be an "
|
||
"ISO 8601 date/time string"));
|
||
return NULL;
|
||
}
|
||
|
||
result = e_sexp_result_new (esexp, ESEXP_RES_TIME);
|
||
result->value.time = t;
|
||
|
||
return result;
|
||
}
|
||
|
||
/* (time-add-day TIME N)
|
||
*
|
||
* TIME - time_t, base time
|
||
* N - int, number of days to add
|
||
*
|
||
* Adds the specified number of days to a time value.
|
||
*
|
||
* FIXME: TIMEZONES - need to use a timezone or daylight saving changes will
|
||
* make the result incorrect.
|
||
*/
|
||
static ESExpResult *
|
||
func_time_add_day (ESExp *esexp, int argc, ESExpResult **argv, void *data)
|
||
{
|
||
ESExpResult *result;
|
||
time_t t;
|
||
int n;
|
||
|
||
if (argc != 2) {
|
||
e_sexp_fatal_error (esexp, _("time-add-day expects 2 arguments"));
|
||
return NULL;
|
||
}
|
||
|
||
if (argv[0]->type != ESEXP_RES_TIME) {
|
||
e_sexp_fatal_error (esexp, _("time-add-day expects argument 1 "
|
||
"to be a time_t"));
|
||
return NULL;
|
||
}
|
||
t = argv[0]->value.time;
|
||
|
||
if (argv[1]->type != ESEXP_RES_INT) {
|
||
e_sexp_fatal_error (esexp, _("time-add-day expects argument 2 "
|
||
"to be an integer"));
|
||
return NULL;
|
||
}
|
||
n = argv[1]->value.number;
|
||
|
||
result = e_sexp_result_new (esexp, ESEXP_RES_TIME);
|
||
result->value.time = time_add_day (t, n);
|
||
|
||
return result;
|
||
}
|
||
|
||
/* (time-day-begin TIME)
|
||
*
|
||
* TIME - time_t, base time
|
||
*
|
||
* Returns the start of the day, according to the local time.
|
||
*
|
||
* FIXME: TIMEZONES - this uses the current Unix timezone.
|
||
*/
|
||
static ESExpResult *
|
||
func_time_day_begin (ESExp *esexp, int argc, ESExpResult **argv, void *data)
|
||
{
|
||
time_t t;
|
||
ESExpResult *result;
|
||
|
||
if (argc != 1) {
|
||
e_sexp_fatal_error (esexp, _("time-day-begin expects 1 argument"));
|
||
return NULL;
|
||
}
|
||
|
||
if (argv[0]->type != ESEXP_RES_TIME) {
|
||
e_sexp_fatal_error (esexp, _("time-day-begin expects argument 1 "
|
||
"to be a time_t"));
|
||
return NULL;
|
||
}
|
||
t = argv[0]->value.time;
|
||
|
||
result = e_sexp_result_new (esexp, ESEXP_RES_TIME);
|
||
result->value.time = time_day_begin (t);
|
||
|
||
return result;
|
||
}
|
||
|
||
/* (time-day-end TIME)
|
||
*
|
||
* TIME - time_t, base time
|
||
*
|
||
* Returns the end of the day, according to the local time.
|
||
*
|
||
* FIXME: TIMEZONES - this uses the current Unix timezone.
|
||
*/
|
||
static ESExpResult *
|
||
func_time_day_end (ESExp *esexp, int argc, ESExpResult **argv, void *data)
|
||
{
|
||
time_t t;
|
||
ESExpResult *result;
|
||
|
||
if (argc != 1) {
|
||
e_sexp_fatal_error (esexp, _("time-day-end expects 1 argument"));
|
||
return NULL;
|
||
}
|
||
|
||
if (argv[0]->type != ESEXP_RES_TIME) {
|
||
e_sexp_fatal_error (esexp, _("time-day-end expects argument 1 "
|
||
"to be a time_t"));
|
||
return NULL;
|
||
}
|
||
t = argv[0]->value.time;
|
||
|
||
result = e_sexp_result_new (esexp, ESEXP_RES_TIME);
|
||
result->value.time = time_day_end (t);
|
||
|
||
return result;
|
||
}
|
||
|
||
/* (get-vtype)
|
||
*
|
||
* Returns a string indicating the type of component (VEVENT, VTODO, VJOURNAL,
|
||
* VFREEBUSY, VTIMEZONE, UNKNOWN).
|
||
*/
|
||
static ESExpResult *
|
||
func_get_vtype (ESExp *esexp, int argc, ESExpResult **argv, void *data)
|
||
{
|
||
Query *query;
|
||
QueryPrivate *priv;
|
||
CalComponent *comp;
|
||
CalComponentVType vtype;
|
||
char *str;
|
||
ESExpResult *result;
|
||
|
||
query = QUERY (data);
|
||
priv = query->priv;
|
||
|
||
g_assert (priv->next_comp != NULL);
|
||
comp = priv->next_comp;
|
||
|
||
/* Check argument types */
|
||
|
||
if (argc != 0) {
|
||
e_sexp_fatal_error (esexp, _("get-vtype expects 0 arguments"));
|
||
return NULL;
|
||
}
|
||
|
||
/* Get the type */
|
||
|
||
vtype = cal_component_get_vtype (comp);
|
||
|
||
switch (vtype) {
|
||
case CAL_COMPONENT_EVENT:
|
||
str = g_strdup ("VEVENT");
|
||
break;
|
||
|
||
case CAL_COMPONENT_TODO:
|
||
str = g_strdup ("VTODO");
|
||
break;
|
||
|
||
case CAL_COMPONENT_JOURNAL:
|
||
str = g_strdup ("VJOURNAL");
|
||
break;
|
||
|
||
case CAL_COMPONENT_FREEBUSY:
|
||
str = g_strdup ("VFREEBUSY");
|
||
break;
|
||
|
||
case CAL_COMPONENT_TIMEZONE:
|
||
str = g_strdup ("VTIMEZONE");
|
||
break;
|
||
|
||
default:
|
||
str = g_strdup ("UNKNOWN");
|
||
break;
|
||
}
|
||
|
||
result = e_sexp_result_new (esexp, ESEXP_RES_STRING);
|
||
result->value.string = str;
|
||
|
||
return result;
|
||
}
|
||
|
||
/* Sets a boolean value in the data to TRUE; called from
|
||
* cal_recur_generate_instances() to indicate that at least one instance occurs
|
||
* in the sought time range. We always return FALSE because we want the
|
||
* recurrence engine to finish as soon as possible.
|
||
*/
|
||
static gboolean
|
||
instance_occur_cb (CalComponent *comp, time_t start, time_t end, gpointer data)
|
||
{
|
||
gboolean *occurs;
|
||
|
||
occurs = data;
|
||
*occurs = TRUE;
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
/* Call the backend function to get a timezone from a TZID. */
|
||
static icaltimezone*
|
||
resolve_tzid (const char *tzid, gpointer data)
|
||
{
|
||
Query *query = data;
|
||
|
||
if (!tzid || !tzid[0])
|
||
return NULL;
|
||
else
|
||
return cal_backend_get_timezone (query->priv->backend, tzid);
|
||
}
|
||
|
||
|
||
/* (occur-in-time-range? START END)
|
||
*
|
||
* START - time_t, start of the time range
|
||
* END - time_t, end of the time range
|
||
*
|
||
* Returns a boolean indicating whether the component has any occurrences in the
|
||
* specified time range.
|
||
*/
|
||
static ESExpResult *
|
||
func_occur_in_time_range (ESExp *esexp, int argc, ESExpResult **argv, void *data)
|
||
{
|
||
Query *query;
|
||
QueryPrivate *priv;
|
||
CalComponent *comp;
|
||
time_t start, end;
|
||
gboolean occurs;
|
||
ESExpResult *result;
|
||
|
||
query = QUERY (data);
|
||
priv = query->priv;
|
||
|
||
g_assert (priv->next_comp != NULL);
|
||
comp = priv->next_comp;
|
||
|
||
/* Check argument types */
|
||
|
||
if (argc != 2) {
|
||
e_sexp_fatal_error (esexp, _("occur-in-time-range? expects 2 arguments"));
|
||
return NULL;
|
||
}
|
||
|
||
if (argv[0]->type != ESEXP_RES_TIME) {
|
||
e_sexp_fatal_error (esexp, _("occur-in-time-range? expects argument 1 "
|
||
"to be a time_t"));
|
||
return NULL;
|
||
}
|
||
start = argv[0]->value.time;
|
||
|
||
if (argv[1]->type != ESEXP_RES_TIME) {
|
||
e_sexp_fatal_error (esexp, _("occur-in-time-range? expects argument 2 "
|
||
"to be a time_t"));
|
||
return NULL;
|
||
}
|
||
end = argv[1]->value.time;
|
||
|
||
/* See if there is at least one instance in that range */
|
||
|
||
occurs = FALSE;
|
||
|
||
cal_recur_generate_instances (comp, start, end,
|
||
instance_occur_cb, &occurs,
|
||
resolve_tzid, query);
|
||
|
||
result = e_sexp_result_new (esexp, ESEXP_RES_BOOL);
|
||
result->value.bool = occurs;
|
||
|
||
return result;
|
||
}
|
||
|
||
/* Returns whether a list of CalComponentText items matches the specified string */
|
||
static gboolean
|
||
matches_text_list (GSList *text_list, const char *str)
|
||
{
|
||
GSList *l;
|
||
gboolean matches;
|
||
|
||
matches = FALSE;
|
||
|
||
for (l = text_list; l; l = l->next) {
|
||
CalComponentText *text;
|
||
|
||
text = l->data;
|
||
g_assert (text->value != NULL);
|
||
|
||
if (e_utf8_strstrcasedecomp (text->value, str) != NULL) {
|
||
matches = TRUE;
|
||
break;
|
||
}
|
||
}
|
||
|
||
return matches;
|
||
}
|
||
|
||
/* Returns whether the comments in a component matches the specified string */
|
||
static gboolean
|
||
matches_comment (CalComponent *comp, const char *str)
|
||
{
|
||
GSList *list;
|
||
gboolean matches;
|
||
|
||
cal_component_get_comment_list (comp, &list);
|
||
matches = matches_text_list (list, str);
|
||
cal_component_free_text_list (list);
|
||
|
||
return matches;
|
||
}
|
||
|
||
/* Returns whether the description in a component matches the specified string */
|
||
static gboolean
|
||
matches_description (CalComponent *comp, const char *str)
|
||
{
|
||
GSList *list;
|
||
gboolean matches;
|
||
|
||
cal_component_get_description_list (comp, &list);
|
||
matches = matches_text_list (list, str);
|
||
cal_component_free_text_list (list);
|
||
|
||
return matches;
|
||
}
|
||
|
||
/* Returns whether the summary in a component matches the specified string */
|
||
static gboolean
|
||
matches_summary (CalComponent *comp, const char *str)
|
||
{
|
||
CalComponentText text;
|
||
|
||
cal_component_get_summary (comp, &text);
|
||
|
||
if (!text.value)
|
||
return FALSE;
|
||
|
||
return e_utf8_strstrcasedecomp (text.value, str) != NULL;
|
||
}
|
||
|
||
/* Returns whether any text field in a component matches the specified string */
|
||
static gboolean
|
||
matches_any (CalComponent *comp, const char *str)
|
||
{
|
||
/* As an optimization, and to make life easier for the individual
|
||
* predicate functions, see if we are looking for the empty string right
|
||
* away.
|
||
*/
|
||
if (strlen (str) == 0)
|
||
return TRUE;
|
||
|
||
return (matches_comment (comp, str)
|
||
|| matches_description (comp, str)
|
||
|| matches_summary (comp, str));
|
||
}
|
||
|
||
/* (contains? FIELD STR)
|
||
*
|
||
* FIELD - string, name of field to match (any, comment, description, summary)
|
||
* STR - string, match string
|
||
*
|
||
* Returns a boolean indicating whether the specified field contains the
|
||
* specified string.
|
||
*/
|
||
static ESExpResult *
|
||
func_contains (ESExp *esexp, int argc, ESExpResult **argv, void *data)
|
||
{
|
||
Query *query;
|
||
QueryPrivate *priv;
|
||
CalComponent *comp;
|
||
const char *field;
|
||
const char *str;
|
||
gboolean matches;
|
||
ESExpResult *result;
|
||
|
||
query = QUERY (data);
|
||
priv = query->priv;
|
||
|
||
g_assert (priv->next_comp != NULL);
|
||
comp = priv->next_comp;
|
||
|
||
/* Check argument types */
|
||
|
||
if (argc != 2) {
|
||
e_sexp_fatal_error (esexp, _("contains? expects 2 arguments"));
|
||
return NULL;
|
||
}
|
||
|
||
if (argv[0]->type != ESEXP_RES_STRING) {
|
||
e_sexp_fatal_error (esexp, _("contains? expects argument 1 "
|
||
"to be a string"));
|
||
return NULL;
|
||
}
|
||
field = argv[0]->value.string;
|
||
|
||
if (argv[1]->type != ESEXP_RES_STRING) {
|
||
e_sexp_fatal_error (esexp, _("contains? expects argument 2 "
|
||
"to be a string"));
|
||
return NULL;
|
||
}
|
||
str = argv[1]->value.string;
|
||
|
||
/* See if it matches */
|
||
|
||
if (strcmp (field, "any") == 0)
|
||
matches = matches_any (comp, str);
|
||
else if (strcmp (field, "comment") == 0)
|
||
matches = matches_comment (comp, str);
|
||
else if (strcmp (field, "description") == 0)
|
||
matches = matches_description (comp, str);
|
||
else if (strcmp (field, "summary") == 0)
|
||
matches = matches_summary (comp, str);
|
||
else {
|
||
e_sexp_fatal_error (esexp, _("contains? expects argument 1 to "
|
||
"be one of \"any\", \"summary\", \"description\""));
|
||
return NULL;
|
||
}
|
||
|
||
result = e_sexp_result_new (esexp, ESEXP_RES_BOOL);
|
||
result->value.bool = matches;
|
||
|
||
return result;
|
||
}
|
||
|
||
/* (has-categories? STR+)
|
||
* (has-categories? #f)
|
||
*
|
||
* STR - At least one string specifying a category
|
||
* Or you can specify a single #f (boolean false) value for components
|
||
* that have no categories assigned to them ("unfiled").
|
||
*
|
||
* Returns a boolean indicating whether the component has all the specified
|
||
* categories.
|
||
*/
|
||
static ESExpResult *
|
||
func_has_categories (ESExp *esexp, int argc, ESExpResult **argv, void *data)
|
||
{
|
||
Query *query;
|
||
QueryPrivate *priv;
|
||
CalComponent *comp;
|
||
gboolean unfiled;
|
||
int i;
|
||
GSList *categories;
|
||
gboolean matches;
|
||
ESExpResult *result;
|
||
|
||
query = QUERY (data);
|
||
priv = query->priv;
|
||
|
||
g_assert (priv->next_comp != NULL);
|
||
comp = priv->next_comp;
|
||
|
||
/* Check argument types */
|
||
|
||
if (argc < 1) {
|
||
e_sexp_fatal_error (esexp, _("has-categories? expects at least 1 argument"));
|
||
return NULL;
|
||
}
|
||
|
||
if (argc == 1 && argv[0]->type == ESEXP_RES_BOOL)
|
||
unfiled = TRUE;
|
||
else
|
||
unfiled = FALSE;
|
||
|
||
if (!unfiled)
|
||
for (i = 0; i < argc; i++)
|
||
if (argv[i]->type != ESEXP_RES_STRING) {
|
||
e_sexp_fatal_error (esexp, _("has-categories? expects all arguments "
|
||
"to be strings or one and only one "
|
||
"argument to be a boolean false (#f)"));
|
||
return NULL;
|
||
}
|
||
|
||
/* Search categories. First, if there are no categories we return
|
||
* whether unfiled components are supposed to match.
|
||
*/
|
||
|
||
cal_component_get_categories_list (comp, &categories);
|
||
if (!categories) {
|
||
result = e_sexp_result_new (esexp, ESEXP_RES_BOOL);
|
||
result->value.bool = unfiled;
|
||
|
||
return result;
|
||
}
|
||
|
||
/* Otherwise, we *do* have categories but unfiled components were
|
||
* requested, so this component does not match.
|
||
*/
|
||
if (unfiled) {
|
||
result = e_sexp_result_new (esexp, ESEXP_RES_BOOL);
|
||
result->value.bool = FALSE;
|
||
|
||
return result;
|
||
}
|
||
|
||
matches = TRUE;
|
||
|
||
for (i = 0; i < argc; i++) {
|
||
const char *sought;
|
||
GSList *l;
|
||
gboolean has_category;
|
||
|
||
sought = argv[i]->value.string;
|
||
|
||
has_category = FALSE;
|
||
|
||
for (l = categories; l; l = l->next) {
|
||
const char *category;
|
||
|
||
category = l->data;
|
||
|
||
if (strcmp (category, sought) == 0) {
|
||
has_category = TRUE;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!has_category) {
|
||
matches = FALSE;
|
||
break;
|
||
}
|
||
}
|
||
|
||
cal_component_free_categories_list (categories);
|
||
|
||
result = e_sexp_result_new (esexp, ESEXP_RES_BOOL);
|
||
result->value.bool = matches;
|
||
|
||
return result;
|
||
}
|
||
|
||
|
||
|
||
/* Adds a component to our the UIDs hash table and notifies the client */
|
||
static void
|
||
add_component (Query *query, const char *uid, gboolean query_in_progress, int n_scanned, int total)
|
||
{
|
||
QueryPrivate *priv;
|
||
char *old_uid;
|
||
CORBA_Environment ev;
|
||
|
||
if (query_in_progress)
|
||
g_assert (n_scanned > 0 || n_scanned <= total);
|
||
|
||
priv = query->priv;
|
||
|
||
if (g_hash_table_lookup_extended (priv->uids, uid, (gpointer *) &old_uid, NULL)) {
|
||
g_hash_table_remove (priv->uids, old_uid);
|
||
g_free (old_uid);
|
||
}
|
||
|
||
g_hash_table_insert (priv->uids, g_strdup (uid), NULL);
|
||
|
||
CORBA_exception_init (&ev);
|
||
GNOME_Evolution_Calendar_QueryListener_notifyObjUpdated (
|
||
priv->ql,
|
||
(char *) uid,
|
||
query_in_progress,
|
||
n_scanned,
|
||
total,
|
||
&ev);
|
||
|
||
if (ev._major != CORBA_NO_EXCEPTION)
|
||
g_message ("add_component(): Could not notify the listener of an "
|
||
"updated component");
|
||
|
||
CORBA_exception_free (&ev);
|
||
}
|
||
|
||
/* Removes a component from our the UIDs hash table and notifies the client */
|
||
static void
|
||
remove_component (Query *query, const char *uid)
|
||
{
|
||
QueryPrivate *priv;
|
||
char *old_uid;
|
||
CORBA_Environment ev;
|
||
|
||
priv = query->priv;
|
||
|
||
if (!g_hash_table_lookup_extended (priv->uids, uid, (gpointer *) &old_uid, NULL))
|
||
return;
|
||
|
||
/* The component did match the query before but it no longer does, so we
|
||
* have to notify the client.
|
||
*/
|
||
|
||
g_hash_table_remove (priv->uids, old_uid);
|
||
g_free (old_uid);
|
||
|
||
CORBA_exception_init (&ev);
|
||
GNOME_Evolution_Calendar_QueryListener_notifyObjRemoved (
|
||
priv->ql,
|
||
(char *) uid,
|
||
&ev);
|
||
|
||
if (ev._major != CORBA_NO_EXCEPTION)
|
||
g_message ("remove_component(): Could not notify the listener of a "
|
||
"removed component");
|
||
|
||
CORBA_exception_free (&ev);
|
||
}
|
||
|
||
/* Removes a component from the list of pending UIDs */
|
||
static void
|
||
remove_from_pending (Query *query, const char *remove_uid)
|
||
{
|
||
QueryPrivate *priv;
|
||
GList *l;
|
||
|
||
priv = query->priv;
|
||
|
||
for (l = priv->pending_uids; l; l = l->next) {
|
||
char *uid;
|
||
|
||
g_assert (priv->n_pending > 0);
|
||
|
||
uid = l->data;
|
||
if (strcmp (remove_uid, uid))
|
||
continue;
|
||
|
||
g_free (uid);
|
||
|
||
priv->pending_uids = g_list_remove_link (priv->pending_uids, l);
|
||
g_list_free_1 (l);
|
||
priv->n_pending--;
|
||
|
||
g_assert ((priv->pending_uids && priv->n_pending != 0)
|
||
|| (!priv->pending_uids && priv->n_pending == 0));
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
static struct {
|
||
char *name;
|
||
ESExpFunc *func;
|
||
} functions[] = {
|
||
/* Time-related functions */
|
||
{ "time-now", func_time_now },
|
||
{ "make-time", func_make_time },
|
||
{ "time-add-day", func_time_add_day },
|
||
{ "time-day-begin", func_time_day_begin },
|
||
{ "time-day-end", func_time_day_end },
|
||
|
||
/* Component-related functions */
|
||
{ "get-vtype", func_get_vtype },
|
||
{ "occur-in-time-range?", func_occur_in_time_range },
|
||
{ "contains?", func_contains },
|
||
{ "has-categories?", func_has_categories }
|
||
};
|
||
|
||
/* Initializes a sexp by interning our own symbols */
|
||
static ESExp *
|
||
create_sexp (Query *query)
|
||
{
|
||
ESExp *esexp;
|
||
int i;
|
||
|
||
esexp = e_sexp_new ();
|
||
|
||
for (i = 0; i < (sizeof (functions) / sizeof (functions[0])); i++)
|
||
e_sexp_add_function (esexp, 0, functions[i].name, functions[i].func, query);
|
||
|
||
return esexp;
|
||
}
|
||
|
||
/* Evaluates the query sexp on the specified component and notifies the listener
|
||
* as appropriate.
|
||
*/
|
||
static void
|
||
match_component (Query *query, const char *uid,
|
||
gboolean query_in_progress, int n_scanned, int total)
|
||
{
|
||
QueryPrivate *priv;
|
||
CalComponent *comp;
|
||
ESExpResult *result;
|
||
|
||
priv = query->priv;
|
||
|
||
comp = cal_backend_get_object_component (priv->backend, uid);
|
||
g_assert (comp != NULL);
|
||
gtk_object_ref (GTK_OBJECT (comp));
|
||
|
||
/* Eval the sexp */
|
||
|
||
g_assert (priv->next_comp == NULL);
|
||
|
||
priv->next_comp = comp;
|
||
result = e_sexp_eval (priv->esexp);
|
||
gtk_object_unref (GTK_OBJECT (comp));
|
||
priv->next_comp = NULL;
|
||
|
||
if (!result) {
|
||
const char *error_str;
|
||
CORBA_Environment ev;
|
||
|
||
error_str = e_sexp_error (priv->esexp);
|
||
g_assert (error_str != NULL);
|
||
|
||
CORBA_exception_init (&ev);
|
||
GNOME_Evolution_Calendar_QueryListener_notifyEvalError (
|
||
priv->ql,
|
||
error_str,
|
||
&ev);
|
||
|
||
if (ev._major != CORBA_NO_EXCEPTION)
|
||
g_message ("match_component(): Could not notify the listener of "
|
||
"an evaluation error");
|
||
|
||
CORBA_exception_free (&ev);
|
||
return;
|
||
} else if (result->type != ESEXP_RES_BOOL) {
|
||
CORBA_Environment ev;
|
||
|
||
CORBA_exception_init (&ev);
|
||
GNOME_Evolution_Calendar_QueryListener_notifyEvalError (
|
||
priv->ql,
|
||
_("Evaluation of the search expression did not yield a boolean value"),
|
||
&ev);
|
||
|
||
if (ev._major != CORBA_NO_EXCEPTION)
|
||
g_message ("match_component(): Could not notify the listener of "
|
||
"an unexpected result value type when evaluating the "
|
||
"search expression");
|
||
|
||
CORBA_exception_free (&ev);
|
||
} else {
|
||
/* Success; process the component accordingly */
|
||
|
||
if (result->value.bool)
|
||
add_component (query, uid, query_in_progress, n_scanned, total);
|
||
else
|
||
remove_component (query, uid);
|
||
}
|
||
|
||
e_sexp_result_free (priv->esexp, result);
|
||
}
|
||
|
||
/* Processes a single component that is queued in the list */
|
||
static gboolean
|
||
process_component_cb (gpointer data)
|
||
{
|
||
Query *query;
|
||
QueryPrivate *priv;
|
||
char *uid;
|
||
GList *l;
|
||
|
||
query = QUERY (data);
|
||
priv = query->priv;
|
||
|
||
/* No more components? */
|
||
|
||
if (!priv->pending_uids) {
|
||
g_assert (priv->n_pending == 0);
|
||
|
||
priv->idle_id = 0;
|
||
return FALSE;
|
||
}
|
||
|
||
g_assert (priv->n_pending > 0);
|
||
|
||
/* Fetch the component */
|
||
|
||
l = priv->pending_uids;
|
||
priv->pending_uids = g_list_remove_link (priv->pending_uids, l);
|
||
priv->n_pending--;
|
||
|
||
g_assert ((priv->pending_uids && priv->n_pending != 0)
|
||
|| (!priv->pending_uids && priv->n_pending == 0));
|
||
|
||
uid = l->data;
|
||
g_assert (uid != NULL);
|
||
|
||
g_list_free_1 (l);
|
||
|
||
bonobo_object_ref (BONOBO_OBJECT (query));
|
||
|
||
match_component (query, uid,
|
||
TRUE,
|
||
priv->pending_total - priv->n_pending,
|
||
priv->pending_total);
|
||
|
||
bonobo_object_unref (BONOBO_OBJECT (query));
|
||
|
||
g_free (uid);
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
/* Populates the query with pending UIDs so that they can be processed
|
||
* asynchronously.
|
||
*/
|
||
static void
|
||
populate_query (Query *query)
|
||
{
|
||
QueryPrivate *priv;
|
||
|
||
priv = query->priv;
|
||
g_assert (priv->idle_id == 0);
|
||
|
||
priv->pending_uids = cal_backend_get_uids (priv->backend, CALOBJ_TYPE_ANY);
|
||
priv->pending_total = g_list_length (priv->pending_uids);
|
||
priv->n_pending = priv->pending_total;
|
||
|
||
priv->idle_id = g_idle_add (process_component_cb, query);
|
||
}
|
||
|
||
/* Idle handler for starting a query */
|
||
static gboolean
|
||
start_query_cb (gpointer data)
|
||
{
|
||
Query *query;
|
||
QueryPrivate *priv;
|
||
CORBA_Environment ev;
|
||
|
||
query = QUERY (data);
|
||
priv = query->priv;
|
||
|
||
priv->idle_id = 0;
|
||
|
||
priv->esexp = create_sexp (query);
|
||
|
||
/* Compile the query string */
|
||
|
||
g_assert (priv->sexp != NULL);
|
||
e_sexp_input_text (priv->esexp, priv->sexp, strlen (priv->sexp));
|
||
|
||
if (e_sexp_parse (priv->esexp) == -1) {
|
||
const char *error_str;
|
||
|
||
error_str = e_sexp_error (priv->esexp);
|
||
g_assert (error_str != NULL);
|
||
|
||
CORBA_exception_init (&ev);
|
||
GNOME_Evolution_Calendar_QueryListener_notifyQueryDone (
|
||
priv->ql,
|
||
GNOME_Evolution_Calendar_QueryListener_PARSE_ERROR,
|
||
error_str,
|
||
&ev);
|
||
|
||
if (ev._major != CORBA_NO_EXCEPTION)
|
||
g_message ("start_query_cb(): Could not notify the listener of "
|
||
"a parse error");
|
||
|
||
CORBA_exception_free (&ev);
|
||
return FALSE;
|
||
}
|
||
|
||
/* Populate the query with UIDs so that we can process them asynchronously */
|
||
|
||
populate_query (query);
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
/* Callback used when the backend gets loaded; we just queue the query to be
|
||
* started later.
|
||
*/
|
||
static void
|
||
backend_opened_cb (CalBackend *backend, CalBackendOpenStatus status, gpointer data)
|
||
{
|
||
Query *query;
|
||
QueryPrivate *priv;
|
||
|
||
query = QUERY (data);
|
||
priv = query->priv;
|
||
|
||
if (status == CAL_BACKEND_OPEN_SUCCESS) {
|
||
g_assert (cal_backend_is_loaded (backend));
|
||
g_assert (priv->idle_id == 0);
|
||
|
||
priv->idle_id = g_idle_add (start_query_cb, query);
|
||
}
|
||
}
|
||
|
||
/* Callback used when a component changes in the backend */
|
||
static void
|
||
backend_obj_updated_cb (CalBackend *backend, const char *uid, gpointer data)
|
||
{
|
||
Query *query;
|
||
|
||
query = QUERY (data);
|
||
|
||
bonobo_object_ref (BONOBO_OBJECT (query));
|
||
|
||
match_component (query, uid, FALSE, 0, 0);
|
||
remove_from_pending (query, uid);
|
||
|
||
bonobo_object_unref (BONOBO_OBJECT (query));
|
||
}
|
||
|
||
/* Callback used when a component is removed from the backend */
|
||
static void
|
||
backend_obj_removed_cb (CalBackend *backend, const char *uid, gpointer data)
|
||
{
|
||
Query *query;
|
||
QueryPrivate *priv;
|
||
|
||
query = QUERY (data);
|
||
priv = query->priv;
|
||
|
||
bonobo_object_ref (BONOBO_OBJECT (query));
|
||
|
||
remove_component (query, uid);
|
||
remove_from_pending (query, uid);
|
||
|
||
bonobo_object_unref (BONOBO_OBJECT (query));
|
||
}
|
||
|
||
/**
|
||
* query_construct:
|
||
* @query: A live search query.
|
||
* @backend: Calendar backend that the query object will monitor.
|
||
* @ql: Listener for query results.
|
||
* @sexp: Sexp that defines the query.
|
||
*
|
||
* Constructs a #Query object by binding it to a calendar backend and a query
|
||
* listener. The @query object will start to populate itself asynchronously and
|
||
* call the listener as appropriate.
|
||
*
|
||
* Return value: The same value as @query, or NULL if the query could not
|
||
* be constructed.
|
||
**/
|
||
Query *
|
||
query_construct (Query *query,
|
||
CalBackend *backend,
|
||
GNOME_Evolution_Calendar_QueryListener ql,
|
||
const char *sexp)
|
||
{
|
||
QueryPrivate *priv;
|
||
CORBA_Environment ev;
|
||
|
||
g_return_val_if_fail (query != NULL, NULL);
|
||
g_return_val_if_fail (IS_QUERY (query), NULL);
|
||
g_return_val_if_fail (backend != NULL, NULL);
|
||
g_return_val_if_fail (IS_CAL_BACKEND (backend), NULL);
|
||
g_return_val_if_fail (ql != CORBA_OBJECT_NIL, NULL);
|
||
g_return_val_if_fail (sexp != NULL, NULL);
|
||
|
||
priv = query->priv;
|
||
|
||
CORBA_exception_init (&ev);
|
||
priv->ql = CORBA_Object_duplicate (ql, &ev);
|
||
if (ev._major != CORBA_NO_EXCEPTION) {
|
||
g_message ("query_construct(): Could not duplicate the listener");
|
||
priv->ql = CORBA_OBJECT_NIL;
|
||
CORBA_exception_free (&ev);
|
||
return NULL;
|
||
}
|
||
CORBA_exception_free (&ev);
|
||
|
||
priv->backend = backend;
|
||
gtk_object_ref (GTK_OBJECT (priv->backend));
|
||
|
||
gtk_signal_connect (GTK_OBJECT (priv->backend), "obj_updated",
|
||
GTK_SIGNAL_FUNC (backend_obj_updated_cb),
|
||
query);
|
||
gtk_signal_connect (GTK_OBJECT (priv->backend), "obj_removed",
|
||
GTK_SIGNAL_FUNC (backend_obj_removed_cb),
|
||
query);
|
||
|
||
priv->sexp = g_strdup (sexp);
|
||
|
||
/* Queue the query to be started asynchronously */
|
||
|
||
if (cal_backend_is_loaded (priv->backend)) {
|
||
g_assert (priv->idle_id == 0);
|
||
priv->idle_id = g_idle_add (start_query_cb, query);
|
||
} else
|
||
gtk_signal_connect (GTK_OBJECT (priv->backend), "opened",
|
||
GTK_SIGNAL_FUNC (backend_opened_cb),
|
||
query);
|
||
|
||
return query;
|
||
}
|
||
|
||
/**
|
||
* query_new:
|
||
* @backend: Calendar backend that the query object will monitor.
|
||
* @ql: Listener for query results.
|
||
* @sexp: Sexp that defines the query.
|
||
*
|
||
* Creates a new query engine object that monitors a calendar backend.
|
||
*
|
||
* Return value: A newly-created query object, or NULL on failure.
|
||
**/
|
||
Query *
|
||
query_new (CalBackend *backend,
|
||
GNOME_Evolution_Calendar_QueryListener ql,
|
||
const char *sexp)
|
||
{
|
||
Query *query;
|
||
|
||
query = QUERY (gtk_type_new (QUERY_TYPE));
|
||
if (!query_construct (query, backend, ql, sexp)) {
|
||
bonobo_object_unref (BONOBO_OBJECT (query));
|
||
return NULL;
|
||
}
|
||
|
||
return query;
|
||
}
|