/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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; static KotoUndoManager *undo_manager; 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) { const char *text; char *uid = NULL; KotoGroup *group; icalcomponent *comp; KotoUndoContext *ctxt; 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); ctxt = koto_undo_manager_context_begin_formatted (undo_manager, _("Add task %s"), icalcomponent_get_summary (comp), NULL); koto_action_create_task (cal, comp, &uid, ctxt); koto_undo_manager_context_end (undo_manager, ctxt); koto_hint_entry_clear (KOTO_HINT_ENTRY (new_entry)); /* * 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, "undo-manager", undo_manager, 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_TASK, &task, -1); edit_task (task); koto_task_unref (task); } /* * Callback when the selection changes, to disable some actions */ static void on_selection_changed (GtkTreeSelection *selection, gpointer user_data) { GtkActionGroup *actions = user_data; gtk_action_group_set_sensitive (actions, gtk_tree_selection_count_selected_rows (selection) != 0); } /* * 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 ("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; KotoUndoContext *undo; if (!koto_task_view_get_selected_iter (KOTO_TASK_VIEW (treeview), &iter)) { g_warning ("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); undo = koto_undo_manager_context_begin (undo_manager, _("Complete Task")); koto_task_store_set_done (KOTO_TASK_STORE (task_store), &real_iter, TRUE, undo); koto_undo_manager_context_end (undo_manager, undo); } /* * Callback from the Delete Task action. */ static void on_delete_task_action (GtkAction *action, gpointer user_data) { KotoTask *task; KotoUndoContext *ctxt; task = koto_task_view_get_selected_task (KOTO_TASK_VIEW (treeview)); if (!task) { g_warning ("No task selected, DeleteTask should be disabled"); return; } ctxt = koto_undo_manager_context_begin_formatted (undo_manager, _("Delete Task %s"), icalcomponent_get_summary (task->comp), NULL); koto_action_delete_task (cal, task, ctxt); koto_undo_manager_context_end (undo_manager, ctxt); 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) { KotoUndoContext *ctxt = data; KotoTask *task; gboolean done; gtk_tree_model_get (model, iter, COLUMN_DONE, &done, COLUMN_TASK, &task, -1); if (done) { koto_action_delete_task (cal, task, ctxt); } koto_task_unref (task); return FALSE; } /* * Callback from the Purge Tasks action. */ static void on_purge_action (GtkAction *action, gpointer user_data) { KotoUndoContext *ctxt; ctxt = koto_undo_manager_context_begin (undo_manager, _("Remove Completed")); gtk_tree_model_foreach (task_store, purge_foreach, ctxt); koto_undo_manager_context_end (undo_manager, ctxt); } /* * Callback from the About action. */ static void on_about_action (GtkAction *action, gpointer user_data) { const char* authors[] = { "Ross Burton ", NULL, }; const char* artists[] = { "Andreas Nilsson ", "Jakub Steiner ", 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) }, { "PurgeTasks", NULL, N_("_Remove Completed"), NULL, NULL, G_CALLBACK (on_purge_action) }, { "Quit", GTK_STOCK_QUIT, NULL, NULL, NULL, G_CALLBACK (gtk_main_quit) }, { "EditMenu", NULL, N_("_Edit") }, { "HelpMenu", NULL, N_("_Help") }, { "About", GTK_STOCK_ABOUT, NULL, NULL, NULL, G_CALLBACK (on_about_action) }, }; static const GtkActionEntry task_actions[] = { { "EditTask", GTK_STOCK_EDIT, N_("Edit..."), "e", NULL, G_CALLBACK (on_edit_task_action) }, /* TODO: turn this action into a toggle action */ { "CompleteTask", NULL, N_("_Mark Complete"), "d", NULL, G_CALLBACK (on_complete_task_action) }, { "DeleteTask", GTK_STOCK_DELETE, NULL, "Delete", NULL, G_CALLBACK (on_delete_task_action) }, }; int main (int argc, char **argv) { GError *error = NULL; ECalView *cal_view; GtkWidget *top_box, *box, *menu, *scrolled, *hbox, *label, *new_button; GtkTreeSelection *selection; GtkActionGroup *action_group, *task_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); undo_manager = koto_undo_manager_new (); 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); gtk_action_group_add_action_with_accel (action_group, koto_undo_action_new ("Undo", undo_manager), "z"); gtk_action_group_add_action_with_accel (action_group, koto_undo_action_new_redo ("Redo", undo_manager), "z"); task_action_group = gtk_action_group_new ("Task Actions"); gtk_action_group_set_translation_domain (task_action_group, GETTEXT_PACKAGE); gtk_action_group_add_actions (task_action_group, task_actions, G_N_ELEMENTS (task_actions), NULL); ui_manager = gtk_ui_manager_new (); gtk_ui_manager_insert_action_group (ui_manager, action_group, 0); gtk_ui_manager_insert_action_group (ui_manager, task_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_set (treeview, "undo-manager", undo_manager, NULL); g_object_unref (filter); g_signal_connect (treeview, "row-activated", G_CALLBACK (on_row_activated), NULL); gtk_container_add (GTK_CONTAINER (scrolled), treeview); selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview)); g_signal_connect (selection, "changed", G_CALLBACK (on_selection_changed), task_action_group); on_selection_changed (selection, task_action_group); 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; }