Logo Search packages:      
Sourcecode: tasks version File versions

main.c

/*
 * Copyright (C) 2007 OpenedHand Ltd
 *
 * 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., 51
 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include <config.h>
#include <string.h>
#include <libecal/e-cal.h>
#include <glib/gi18n.h>
#include <gtk/gtk.h>

#include <libkoto/ical-util.h>
#include <libkoto/koto-category-group.h>
#include <libkoto/koto-all-group.h>
#include <libkoto/koto-no-category-group.h>
#include <libkoto/koto-meta-group.h>
#include <libkoto/koto-group-store.h>
#include <libkoto/koto-hint-entry.h>
#include <libkoto/koto-task.h>
#include <libkoto/koto-task-store.h>
#include <libkoto/koto-task-view.h>
#include <libkoto/koto-task-editor-dialog.h>
#include <libkoto/koto-group-filter-model.h>
#include <libkoto/koto-group-combo.h>
#include <libkoto/koto-platform.h>
#include <libkoto/koto-utils.h>

#include "window-util.h"

static GtkWidget *window, *combo, *treeview, *new_entry;
static ECal *cal;
static GtkTreeModel *task_store, *filter, *group_filter_store, *group_selector_store;
/* Hash of KotoTask to KotoTaskEditorDialogs, to avoid opening the same editor
   twice */
static GHashTable *edit_dialog_hash;

void
koto_platform_open_url (const char *url)
{
  char cmd[1024];

  g_snprintf (cmd, sizeof (cmd), "gnome-open \"%s\"", url);  
  if (g_spawn_command_line_async (cmd, NULL))
    return;

  g_snprintf (cmd, sizeof (cmd), "xdg-open \"%s\"", url);  
  if (g_spawn_command_line_async (cmd, NULL))
    return;

  g_snprintf (cmd, sizeof (cmd), "firefox \"%s\"", url);  
  if (g_spawn_command_line_async (cmd, NULL))
    return;

  g_warning ("Cannot start gnome-open, xdg-open, or firefox");
}

static gboolean
select_uid (char *uid)
{
  GtkTreeSelection *selection;
  GtkTreeIter iter, real_iter;
  GtkTreePath *path;

  g_assert (uid);
  
  if (koto_task_store_get_iter_for_uid (KOTO_TASK_STORE (task_store), uid, &iter)) {
    selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview));
    
    gtk_tree_model_filter_convert_child_iter_to_iter (GTK_TREE_MODEL_FILTER (filter),
                                                      &real_iter, &iter);
    
    path = gtk_tree_model_get_path (filter, &real_iter);
    gtk_tree_view_set_cursor (GTK_TREE_VIEW (treeview), path, NULL, FALSE);
    gtk_tree_path_free (path);
  }
  
  g_free (uid);
  return FALSE;
}

/*
 * Move a pointer on until it isn't pointing at whitespace.
 */
static void
skip_whitespace (const char **p)
{
  g_assert (p);
  g_assert (*p);

  while (g_unichar_isspace (g_utf8_get_char (*p)))
    *p = g_utf8_next_char (*p);
}

/*
 * Given a summary and a default group, parse as much context (such as group and
 * priority) from the summary, and return a new icalcomponent.
 */
static icalcomponent *
create_component (const char *text, KotoGroup *default_group)
{
  char *group = NULL;
  icalcomponent *comp;

  /* TODO: from here onwards should be moved into libkoto so that other
     frontends can have the same logic. */

  comp = icalcomponent_new (ICAL_VTODO_COMPONENT);
  icalcomponent_add_property (comp, icalproperty_new_class (ICAL_CLASS_PUBLIC));

  /* If the task starts with +, !, or -, set the priority as relevant */
  if (g_utf8_strchr ("+!", -1, g_utf8_get_char (text))) {
    icalcomponent_add_property (comp, icalproperty_new_priority (PRIORITY_HIGH));
    text = g_utf8_next_char (text);
    skip_whitespace (&text);
  } else if (g_utf8_strchr ("-", -1, g_utf8_get_char (text))) {
    icalcomponent_add_property (comp, icalproperty_new_priority (PRIORITY_LOW));
    text = g_utf8_next_char (text);
    skip_whitespace (&text);
  }
  
  /* If the task starts with @foo, put it in the "foo" group. */
  if (text[0] == '@') {
    char *end, *guess;

    text = g_utf8_next_char (text);
    end = g_utf8_strchr (text, -1, ' '); /* TODO: use unicode whitespace check */
    if (end) {
      guess = g_strndup (text, end-text);
      text = end;
      skip_whitespace (&text);

      /* Try and find a matching group */
      group = koto_group_store_match_group
        (KOTO_GROUP_STORE (group_selector_store), guess);
      
      if (group)
        g_free (guess);
      else
        group = guess;
    }
  }

  icalcomponent_add_property (comp, icalproperty_new_summary (text));

  /* If a group wasn't specified in the task, use the current group */
  if (group == NULL) {
    if (default_group &&
        g_type_is_a (G_OBJECT_TYPE (default_group),
                     KOTO_TYPE_CATEGORY_GROUP)) {
      group = g_strdup (koto_group_get_name (default_group));
    }
  }
  
  if (group) {
    icalcomponent_add_property (comp, icalproperty_new_categories (group));
    g_free (group);
  }

  return comp;
}

static void
on_new_clicked (GtkButton *button)
{
  GError *error = NULL;
  const char *text;
  char *uid = NULL;
  KotoGroup *group;
  icalcomponent *comp;

  text = gtk_entry_get_text (GTK_ENTRY (new_entry));
  
  if (!text || text[0] == '\0') {
    g_warning ("Got clicked with empty text");
    return;
  }

  group = koto_group_combo_get_active_group (KOTO_GROUP_COMBO (combo));
  comp = create_component (text, group);
  g_object_unref (group);

  koto_hint_entry_clear (KOTO_HINT_ENTRY (new_entry));

  if (!e_cal_create_object (cal, comp, &uid, &error)) {
    g_warning (G_STRLOC ": cannot create task: %s", error->message);
    g_error_free (error);
  }

  /*
   * Select the new task in an idle function so that the store can process the
   * signals that are waiting for it (as we did a blocking call to add the
   * task).
   */
  if (uid) {
    g_idle_add ((GSourceFunc)select_uid, uid);
  }
}


/* 
 * Used to enable/disable the new task button depending on the contents of the
 * entry.
 */
static void
on_new_entry_changed (GtkEntry *entry, GtkWidget *button)
{
  gtk_widget_set_sensitive (button, ! koto_hint_entry_is_empty (KOTO_HINT_ENTRY (entry)));
}

static void
edit_dialog_weak_notify (gpointer data, GObject *dead)
{
  /* Removal automatically unrefs */
  g_hash_table_remove (edit_dialog_hash, data);
}

static void
edit_task (KotoTask *task)
{
  GtkWidget *dialog;

  g_assert (task);

  dialog = g_hash_table_lookup (edit_dialog_hash, task);
  if (dialog) {
    gtk_window_present (GTK_WINDOW (dialog));
  } else {
    dialog = koto_task_editor_dialog_new ();
    gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (window));
    g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL);
    
    g_object_set (dialog,
                  "cal", cal,
                  "groups", group_selector_store,
                  "task", task,
                  NULL);
    
    gtk_widget_show (dialog);
    
    g_hash_table_insert (edit_dialog_hash, task, dialog);
    g_object_weak_ref (G_OBJECT (dialog), edit_dialog_weak_notify, koto_task_ref (task));
  }
}

/*
 * Callback when a row in the tree view is activated, which edits the task.
 */
static void
on_row_activated  (GtkTreeView *tree_view, GtkTreePath *path, GtkTreeViewColumn *column, gpointer user_data)
{
  GtkTreeModel *model;
  GtkTreeIter iter;
  KotoTask *task;

  model = gtk_tree_view_get_model (tree_view);
  if (!gtk_tree_model_get_iter (model, &iter, path)) {
    g_warning (G_STRLOC ": cannot get iterator for path");
    return;
  }
  gtk_tree_model_get (model, &iter, COLUMN_ICAL, &task, -1);

  edit_task (task);

  koto_task_unref (task);
}

/*
 * Callback from the New Task action.
 */
static void
on_new_task_action (GtkAction *action, gpointer user_data)
{
  gtk_widget_grab_focus (new_entry);
}

/*
 * Callback from the Edit Task action.
 */
static void
on_edit_task_action (GtkAction *action, gpointer user_data)
{
  KotoTask *task;

  task = koto_task_view_get_selected_task (KOTO_TASK_VIEW (treeview));
  if (!task) {
    g_warning ("TODO: No task selected, EditTask should be disabled");
    return;
  }
  
  edit_task (task);

  koto_task_unref (task);
}

static void
on_complete_task_action (GtkAction *action, gpointer user_data)
{
  GtkTreeIter real_iter, iter;

  if (!koto_task_view_get_selected_iter (KOTO_TASK_VIEW (treeview), &iter)) {
    g_warning ("TODO: No task selected, CompleteTask should be disabled");
    return;
  }

  gtk_tree_model_filter_convert_iter_to_child_iter (GTK_TREE_MODEL_FILTER (filter), &real_iter, &iter);
  
  koto_task_store_set_done (KOTO_TASK_STORE (task_store), &real_iter, TRUE);
}

/*
 * Callback from the Delete Task action.
 */
static void
on_delete_task_action (GtkAction *action, gpointer user_data)
{
  GError *error = NULL;
  GtkWidget *dialog;
  KotoTask *task;

  task = koto_task_view_get_selected_task (KOTO_TASK_VIEW (treeview));
  if (!task) {
    g_warning ("TODO: No task selected, DeleteTask should be disabled");
    return;
  }
  
  dialog = gtk_message_dialog_new (GTK_WINDOW (window),
                                   GTK_DIALOG_DESTROY_WITH_PARENT,
                                   GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE,
                                   _("Are you sure you want to delete \"%s\"?"),
                                   icalcomponent_get_summary (task->comp));
  gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
                                            _("If you delete an item, it is permanently lost."));
  gtk_dialog_add_buttons (GTK_DIALOG (dialog),
                          GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
                          GTK_STOCK_DELETE, GTK_RESPONSE_ACCEPT,
                          NULL);
  gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
  
  if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT) {
    if (!e_cal_remove_object (cal, icalcomponent_get_uid (task->comp), &error)) {
      g_warning ("Cannot remove object: %s", error->message);
      g_error_free (error);
    }
  }
  
  gtk_widget_destroy (dialog);

  koto_task_unref (task);
}

/*
 * Foreach handler for the Purge Complete action, which removes a task if it is
 * completed.
 */
static gboolean
purge_foreach (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer data)
{
  GError *error = NULL;
  KotoTask *task;
  gboolean done;

  gtk_tree_model_get (model, iter,
                      COLUMN_DONE, &done,
                      COLUMN_ICAL, &task,
                      -1);
  
  if (done) {
    if (!e_cal_remove_object (cal, icalcomponent_get_uid (task->comp), &error)) {
      g_warning ("Cannot remove object: %s", error->message);
      g_error_free (error);
      error = NULL;
    }
  }

  koto_task_unref (task);
  
  return FALSE;
}

/*
 * Callback from the Purge Tasks action.
 */
static void
on_purge_action (GtkAction *action, gpointer user_data)
{
  GtkWidget *dialog;

  dialog = gtk_message_dialog_new (GTK_WINDOW (window),
                                   GTK_DIALOG_DESTROY_WITH_PARENT,
                                   GTK_MESSAGE_QUESTION, GTK_BUTTONS_NONE,
                                   _("Are you sure you want to delete all completed tasks?"));
  gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
                                            _("Deleting completed tasks means they are permanently lost."));
  gtk_dialog_add_buttons (GTK_DIALOG (dialog),
                          GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
                          GTK_STOCK_DELETE, GTK_RESPONSE_ACCEPT,
                          NULL);
  gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
  
  if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT) {
    gtk_tree_model_foreach (task_store, purge_foreach, NULL);
  }
  
  gtk_widget_destroy (dialog);
}

/*
 * Callback from the About action.
 */
static void
on_about_action (GtkAction *action, gpointer user_data)
{
  const char* authors[] = {
    "Ross Burton <ross@openedhand.com>", NULL,
  };
  const char* artists[] = {
    "Andreas Nilsson <andreas@andreasn.se>",
    "Jakub Steiner <jimmac@ximian.com>",
    NULL,
  };
  const char *license = {
    N_(
       "Tasks 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.\n\n"
       "Tasks 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.\n\n"
       "You should have received a copy of the GNU General Public License "
       "along with Tasks; if not, write to the Free Software Foundation, Inc., "
       "51 Franklin St, Fifth Floor, Boston, MA 0110-1301, USA"
       )
  };

  gtk_show_about_dialog (GTK_WINDOW (window),
                         "name", _("Tasks"),
                         "version", VERSION,
                         "logo-icon-name", "tasks",
                         "copyright", "Copyright \302\251 2007 OpenedHand Ltd",
                         "authors", authors,
                         "artists", artists,
                         "translator-credits", _("translator-credits"),
                         "license", license,
                         "wrap-license", TRUE,
                         "website", "http://pimlico-project.org",
                         "website-label", _("The Pimlico Project"),
                         NULL);
}

/* TODO: split into global actions and actions that require a task to be selected */
static const GtkActionEntry actions[] = 
{
  /* Action name, stock ID, label, accelerator, tooltip, callback */
  { "TasksMenu", NULL, N_("_Task") },
  { "NewTask", GTK_STOCK_NEW, NULL, NULL, NULL, G_CALLBACK (on_new_task_action) },
  { "EditTask", GTK_STOCK_EDIT, NULL, "<control>e", NULL, G_CALLBACK (on_edit_task_action) },
  /* TODO: turn this action into a toggle action */
  { "CompleteTask", NULL, N_("_Mark Complete"), "<control>d", NULL, G_CALLBACK (on_complete_task_action) },
  { "DeleteTask", GTK_STOCK_DELETE, NULL, "<control>Delete", NULL, G_CALLBACK (on_delete_task_action) },
  { "PurgeTasks", NULL, N_("_Remove Completed"), NULL, NULL, G_CALLBACK (on_purge_action) },
  { "Quit", GTK_STOCK_QUIT, NULL, NULL, NULL, G_CALLBACK (gtk_main_quit) },
  
  { "HelpMenu", NULL, N_("_Help") },
  { "About", GTK_STOCK_ABOUT, NULL, NULL, NULL, G_CALLBACK (on_about_action) },
};

int
main (int argc, char **argv)
{
  GError *error = NULL;
  ECalView *cal_view;
  GtkWidget *top_box, *box, *menu, *scrolled, *hbox, *label, *new_button;
  GtkActionGroup *action_group;
  GtkUIManager *ui_manager;

#ifdef ENABLE_NLS
  /* Initialise i18n*/
  bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR);
  bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
  textdomain(GETTEXT_PACKAGE);
#endif

  g_set_application_name (_("Tasks"));
  gtk_init (&argc, &argv);
  
  edit_dialog_hash = g_hash_table_new_full (NULL, NULL, (GDestroyNotify)koto_task_unref, NULL);

  cal = e_cal_new_system_tasks ();
  if (!cal)
    g_error ("Cannot get system tasks");
  
  if (!e_cal_open (cal, FALSE, &error))
    g_error("Cannot open calendar: %s", error->message);

  if (!e_cal_get_query (cal, "#t", &cal_view, &error))
    g_error("Cannot get calendar view: %s", error->message);
  /* TODO: nasty, should pass cal to the stores or add e_cal_view_get_cal() */
  g_object_set_data_full (G_OBJECT (cal_view), "koto-ecal", g_object_ref (cal), g_object_unref);

  /* Create the data stores */
  task_store = koto_task_store_new (cal_view);
  
  group_filter_store = koto_group_store_new (cal_view);
  koto_group_store_add_group (KOTO_GROUP_STORE (group_filter_store), koto_all_group_new ());
  koto_group_store_add_group (KOTO_GROUP_STORE (group_filter_store), koto_meta_group_new (KOTO_META_GROUP_SEPERATOR, -99));
  koto_group_store_add_group (KOTO_GROUP_STORE (group_filter_store), koto_meta_group_new (KOTO_META_GROUP_SEPERATOR, 99));
  koto_group_store_add_group (KOTO_GROUP_STORE (group_filter_store), koto_no_category_group_new ());

  group_selector_store = koto_group_store_new (cal_view);
  koto_group_store_add_group (KOTO_GROUP_STORE (group_selector_store), koto_meta_group_new (KOTO_META_GROUP_NONE, -100));
  koto_group_store_add_group (KOTO_GROUP_STORE (group_selector_store), koto_meta_group_new (KOTO_META_GROUP_SEPERATOR, -99));
  koto_group_store_add_group (KOTO_GROUP_STORE (group_selector_store), koto_meta_group_new (KOTO_META_GROUP_SEPERATOR, 99));
  koto_group_store_add_group (KOTO_GROUP_STORE (group_selector_store), koto_meta_group_new (KOTO_META_GROUP_NEW, 100));
  
  /* Create the UI */
  gtk_window_set_default_icon_name ("tasks");
  window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  gtk_window_set_default_size (GTK_WINDOW (window), 240, 320);
  window_bind_state (GTK_WINDOW (window));
  g_signal_connect (window, "destroy", gtk_main_quit, NULL);

  top_box = gtk_vbox_new (FALSE, 0);
  gtk_container_add (GTK_CONTAINER (window), top_box); 

  action_group = gtk_action_group_new ("Actions");
  gtk_action_group_set_translation_domain (action_group, GETTEXT_PACKAGE);
  gtk_action_group_add_actions (action_group, actions, G_N_ELEMENTS (actions), NULL);

  ui_manager = gtk_ui_manager_new ();
  gtk_ui_manager_insert_action_group (ui_manager, action_group, 0);

  gtk_ui_manager_add_ui_from_file (ui_manager, PKGDATADIR "/tasks-ui.xml", &error);
  if (error) {
    g_warning ("Cannot load UI: %s", error->message);
    g_error_free (error);
    error = NULL;
  }
  /* Bind the accelerators */
  gtk_window_add_accel_group (GTK_WINDOW (window), gtk_ui_manager_get_accel_group (ui_manager));
  gtk_ui_manager_ensure_update (ui_manager);

  menu = gtk_ui_manager_get_widget (ui_manager, "/MenuBar");
  gtk_box_pack_start (GTK_BOX (top_box), menu, FALSE, FALSE, 0);

  box = gtk_vbox_new (FALSE, 4);
  gtk_container_set_border_width (GTK_CONTAINER (box), 4);
  gtk_container_add (GTK_CONTAINER (top_box), box);

  hbox = gtk_hbox_new (FALSE, 4);
  gtk_box_pack_start (GTK_BOX (box), hbox, FALSE, FALSE, 0);
  label = gtk_label_new_with_mnemonic (_("_Category:"));
  gtk_box_pack_start (GTK_BOX (hbox), label, FALSE, FALSE, 0);
  combo = koto_group_combo_new (KOTO_GROUP_STORE (group_filter_store));
  gtk_label_set_mnemonic_widget (GTK_LABEL (label), combo);
  gtk_box_pack_start (GTK_BOX (hbox), combo, TRUE, TRUE, 0);

  scrolled = gtk_scrolled_window_new (NULL, NULL);
  gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled), GTK_SHADOW_IN);
  gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
  gtk_box_pack_start (GTK_BOX (box), scrolled, TRUE, TRUE, 0);

  filter = koto_group_model_filter_new (KOTO_TASK_STORE (task_store));
  g_object_unref (task_store);
  treeview = koto_task_view_new (KOTO_TASK_STORE (task_store), KOTO_GROUP_MODEL_FILTER (filter));
  g_object_unref (filter);

  g_signal_connect (treeview, "row-activated", G_CALLBACK (on_row_activated), NULL);
  gtk_container_add (GTK_CONTAINER (scrolled), treeview);

  hbox = gtk_hbox_new (FALSE, 4);
  new_entry = koto_hint_entry_new (_("New task..."));
  gtk_entry_set_activates_default (GTK_ENTRY (new_entry), TRUE);
  gtk_box_pack_start (GTK_BOX (hbox), new_entry, TRUE, TRUE, 0);

  new_button = gtk_button_new_from_stock (GTK_STOCK_ADD);
  gtk_widget_set_sensitive (new_button, FALSE);
  GTK_WIDGET_SET_FLAGS (new_button, GTK_CAN_DEFAULT);
  gtk_window_set_default (GTK_WINDOW (window), new_button);
  g_signal_connect (new_button, "clicked", G_CALLBACK (on_new_clicked), NULL);
  g_signal_connect (new_entry, "changed", G_CALLBACK (on_new_entry_changed), new_button);
  gtk_box_pack_start (GTK_BOX (hbox), new_button, FALSE, FALSE, 0);

  gtk_box_pack_start (GTK_BOX (box), hbox, FALSE, FALSE, 0);

  gtk_widget_grab_focus (treeview);

  /* Select the first row, the All group. */
  gtk_combo_box_set_active (GTK_COMBO_BOX (combo), 0);
  koto_group_combo_connect_filter (KOTO_GROUP_COMBO (combo),
                                   KOTO_GROUP_MODEL_FILTER (filter));

  /* Connect to the task store change events to update the title bar */
  koto_sync_window_title (GTK_WINDOW (window),
                          GTK_TREE_MODEL (filter),
                          _("Tasks (%d)"));

  e_cal_view_start (cal_view);

  gtk_widget_show_all (window);  
  gtk_main ();

  return 0;
}

Generated by  Doxygen 1.6.0   Back to index