/* * Copyright (C) 2007 OpenedHand Ltd. * Derived from gtk/main.c. * Authored by: Rob Bradford * * 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 static GtkWidget *window, *combo, *treeview, *new_entry; static ECal *cal; static GtkTreeModel *task_store, *filter, *group_filter_store, *group_selector_store; void koto_platform_open_url (const char *url) { GError *error = NULL; if (!hildon_uri_open (url, NULL, &error)) { g_warning ("Cannot open URL: %s", error->message); g_error_free (error); } } 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_task (KotoTask *task) { GtkWidget *dialog; g_assert (task); dialog = koto_task_editor_dialog_new (); gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (window)); g_object_set (dialog, "cal", cal, "groups", group_selector_store, "task", task, NULL); gtk_dialog_run (GTK_DIALOG (dialog)); gtk_widget_destroy (dialog); } /* * 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 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, NULL); } /* * 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_TASK, &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 ", "Rob Bradford ", 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 */ { "app-menu", NULL, "AppMenu" }, /* dummy */ { "NewTask", NULL, N_("New Task"), NULL, NULL, G_CALLBACK (on_new_task_action) }, { "EditTask", NULL, N_("Edit Task..."), NULL, NULL, G_CALLBACK (on_edit_task_action) }, /* TODO: turn this action into a toggle action */ { "CompleteTask", NULL, N_("_Mark Complete"), NULL, NULL, G_CALLBACK (on_complete_task_action) }, { "DeleteTask", NULL, N_("Delete Task"), NULL, NULL, G_CALLBACK (on_delete_task_action) }, { "PurgeTasks", NULL, N_("_Remove Completed"), NULL, NULL, G_CALLBACK (on_purge_action) }, { "Quit", NULL, N_("Quit"), NULL, NULL, G_CALLBACK (gtk_main_quit) }, { "About", NULL, N_("About"), NULL, NULL, G_CALLBACK (on_about_action) }, }; int main (int argc, char **argv) { GError *error = NULL; ECalView *cal_view; GtkWidget *top_box, *box, *scrolled, *hbox, *label, *new_button; GtkActionGroup *action_group; GtkUIManager *ui_manager; HildonProgram *program; #ifdef ENABLE_NLS /* Initialise i18n*/ bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR); bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); textdomain(GETTEXT_PACKAGE); #endif g_set_application_name (""); gtk_init (&argc, &argv); 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 */ program = hildon_program_get_instance (); /* We generally can hibernate. TODO this should be set to false when editing a task */ hildon_program_set_can_hibernate (program, TRUE); window = hildon_window_new (); hildon_program_add_window (program, HILDON_WINDOW (window)); gtk_window_set_default_icon_name ("tasks"); 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; } gtk_ui_manager_ensure_update (ui_manager); { GtkWidget *app_menu, *menu; app_menu = gtk_ui_manager_get_widget (ui_manager, "/MenuBar/AppMenu"); menu = gtk_menu_item_get_submenu (GTK_MENU_ITEM (app_menu)); g_object_ref (menu); gtk_menu_item_remove_submenu (GTK_MENU_ITEM (app_menu)); hildon_window_set_menu (HILDON_WINDOW (window), GTK_MENU (menu)); g_object_unref (menu); } 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_NEW); 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); /* This won't be required in Maemo 4, thankfully */ g_object_set (gtk_widget_get_settings (window), "gtk-button-images", FALSE, "gtk-menu-images", FALSE, NULL); gtk_widget_show_all (window); gtk_main (); return 0; }