Make priority sorting work
[milk] / src / milk-main-window.c
1 /*
2  * This program is free software; you can redistribute it and/or
3  * modify it under the terms of the GNU General Public License as
4  * published by the Free Software Foundation; either version 2 of the
5  * License, or (at your option) any later version.
6  *
7  * This program is distributed in the hope that it will be useful,
8  * but WITHOUT ANY WARRANTY; without even the implied warranty of
9  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
10  * General Public License for more details.
11  *
12  * You should have received a copy of the GNU General Public
13  * License along with this program; if not, write to the
14  * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
15  * Boston, MA  02110-1301  USA
16  *
17  * Authors: Travis Reitter <treitter@gmail.com>
18  */
19
20 #include <config.h>
21
22 #include <glib.h>
23 #include <glib/gi18n.h>
24 #include <gtk/gtk.h>
25 #include <hildon/hildon.h>
26 #include <rtm-glib/rtm-glib.h>
27
28 #include "milk-main-window.h"
29 #include "milk-cache.h"
30 #include "milk-task-model.h"
31
32 G_DEFINE_TYPE (MilkMainWindow, milk_main_window, HILDON_TYPE_WINDOW)
33
34 /* less expensive than G_TYPE_INSTANCE_GET_PRIVATE */
35 #define MILK_MAIN_WINDOW_PRIVATE(o) ((MILK_MAIN_WINDOW ((o)))->priv)
36
37 #define NEW_TASK_PLACEHOLDER_TEXT "Enter a new task..."
38
39 static GtkWidget *default_window = NULL;
40
41 struct _MilkMainWindowPrivate
42 {
43         MilkCache *cache;
44
45         GtkWidget *app_menu;
46
47         GtkWidget *main_vbox;
48
49         GtkWidget *new_task_entry;
50         GtkWidget *task_view;
51         GtkWidget *task_selector;
52 };
53
54 enum {
55         TASK_VIEW_COLUMN_TITLE,
56         N_VIEW_COLUMNS
57 };
58
59 typedef struct {
60         const char *display_name;
61         const char *id;
62         gpointer    callback;
63 } MenuItem;
64
65 static void
66 milk_main_window_get_property (GObject    *object,
67                                guint       property_id,
68                                GValue     *value,
69                                GParamSpec *pspec)
70 {
71         switch (property_id)
72         {
73                 default:
74                         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id,
75                                         pspec);
76         }
77 }
78
79 static void
80 milk_main_window_set_property (GObject      *object,
81                                guint         property_id,
82                                const GValue *value,
83                                GParamSpec   *pspec)
84 {
85         switch (property_id)
86         {
87                 default:
88                         G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id,
89                                         pspec);
90         }
91 }
92
93 static void
94 milk_main_window_dispose (GObject *object)
95 {
96         G_OBJECT_CLASS (milk_main_window_parent_class)->dispose (object);
97 }
98
99 static void
100 new_task_clicked_cb (GtkButton      *button,
101                      MilkMainWindow *window)
102 {
103         g_debug ("FIXME: implement 'new task' action");
104 }
105
106 static GList*
107 get_selected_tasks (MilkMainWindow *window)
108 {
109         MilkMainWindowPrivate *priv;
110         GList *rows;
111         GtkTreeModel *model;
112         GList *tasks = NULL;
113
114         priv = MILK_MAIN_WINDOW_PRIVATE (window);
115
116         rows = hildon_touch_selector_get_selected_rows (
117                         HILDON_TOUCH_SELECTOR (priv->task_view),
118                         TASK_VIEW_COLUMN_TITLE);
119         model = hildon_touch_selector_get_model (
120                         HILDON_TOUCH_SELECTOR (priv->task_view),
121                         TASK_VIEW_COLUMN_TITLE);
122
123         while (rows) {
124                 GtkTreeIter iter;
125                 RtmTask *task;
126
127                 gtk_tree_model_get_iter (model, &iter, rows->data);
128                 gtk_tree_model_get (model, &iter,
129                                 MILK_TASK_MODEL_COLUMN_TASK, &task,
130                                 -1);
131
132                 tasks = g_list_prepend (tasks, task);
133                 rows = g_list_delete_link (rows, rows);
134         }
135
136         return tasks;
137 }
138
139 static void
140 complete_clicked_cb (GtkButton      *button,
141                      MilkMainWindow *window)
142 {
143         MilkMainWindowPrivate *priv;
144         GList *tasks;
145         char *timeline;
146         GError *error = NULL;
147
148         priv = MILK_MAIN_WINDOW_PRIVATE (window);
149
150         tasks = get_selected_tasks (window);
151         timeline = milk_cache_timeline_create (priv->cache, &error);
152
153         if (error) {
154                 g_warning (G_STRLOC ": failed to create a timeline: %s",
155                            error->message);
156                 g_clear_error (&error);
157         } else {
158                 while (tasks) {
159                         milk_cache_task_complete (priv->cache, timeline,
160                                         tasks->data, &error);
161                         tasks = g_list_delete_link (tasks, tasks);
162                 }
163         }
164 }
165
166 static void
167 delete_clicked_cb (GtkButton      *button,
168                    MilkMainWindow *window)
169 {
170         MilkMainWindowPrivate *priv;
171         GList *tasks;
172         char *timeline;
173         GError *error = NULL;
174
175         priv = MILK_MAIN_WINDOW_PRIVATE (window);
176
177         tasks = get_selected_tasks (window);
178         timeline = milk_cache_timeline_create (priv->cache, &error);
179
180         if (error) {
181                 g_warning (G_STRLOC ": failed to create a timeline: %s",
182                            error->message);
183                 g_clear_error (&error);
184         } else {
185                 while (tasks) {
186                         milk_cache_task_delete (priv->cache, timeline,
187                                         tasks->data, &error);
188                         tasks = g_list_delete_link (tasks, tasks);
189                 }
190         }
191 }
192
193 static void
194 priority_plus_clicked_cb (GtkButton      *button,
195                           MilkMainWindow *window)
196 {
197         g_debug ("FIXME: implement 'priority plus' action");
198 }
199
200 static void
201 priority_minus_clicked_cb (GtkButton      *button,
202                            MilkMainWindow *window)
203 {
204         g_debug ("FIXME: implement 'priority minus' action");
205 }
206
207 static void
208 edit_clicked_cb (GtkButton      *button,
209                  MilkMainWindow *window)
210 {
211         g_debug ("FIXME: implement 'edit' action");
212 }
213
214 static MenuItem menu_items_always_shown[] = {
215         {"New Task",   "menu-item-new-task",       new_task_clicked_cb},
216 };
217
218 static MenuItem menu_items_selection_required[] = {
219         {"Edit",       "menu-item-edit",           edit_clicked_cb},
220         {"Priority +", "menu-item-priority_plus",  priority_plus_clicked_cb},
221         {"Priority -", "menu-item-priority_minus", priority_minus_clicked_cb},
222         {"Complete",   "menu-item-complete",       complete_clicked_cb},
223         {"Delete",     "menu-item-delete",         delete_clicked_cb},
224 };
225
226 static void
227 new_task_entry_activated_cb (GtkEntry       *entry,
228                              MilkMainWindow *window)
229 {
230         MilkMainWindowPrivate *priv;
231         char *name;
232
233         priv = MILK_MAIN_WINDOW_PRIVATE (window);
234
235         name = g_strdup (gtk_entry_get_text (entry));
236
237         /* Strip the contents of leading and trailing whitespace, and add as a
238          * new task if the result is non-empty */
239         if (g_strcmp0 (g_strstrip (name), "")) {
240                 char *timeline;
241                 GError *error = NULL;
242
243                 timeline = milk_cache_timeline_create (priv->cache, &error);
244
245                 if (error) {
246                         g_warning (G_STRLOC ": failed to create a timeline: %s",
247                                 error->message);
248                         g_clear_error (&error);
249                 } else {
250                         RtmTask *task;
251
252                         task = milk_cache_task_add (priv->cache, timeline, name,
253                                         &error);
254                         if (task) {
255                                 /* empty out the entry and show its placeholder
256                                  * text */
257                                 gtk_entry_set_text (entry, "");
258                                 gtk_widget_grab_focus (priv->task_view);
259
260                                 /* FIXME: we should probably scroll to this new
261                                  * task in the model view, if it's not currently
262                                  * visible (and highlight only it in any case */
263                         } else {
264                                 g_warning (G_STRLOC ": failed to add task: %s",
265                                                 error->message);
266                                 g_clear_error (&error);
267                         }
268                 }
269         }
270
271         g_free (name);
272 }
273
274 static gboolean
275 new_task_entry_key_press_event_cb (GtkEntry       *entry,
276                                    GdkEventKey    *event,
277                                    MilkMainWindow *window)
278 {
279         MilkMainWindowPrivate *priv;
280
281         priv = MILK_MAIN_WINDOW_PRIVATE (window);
282
283         if (!event || event->type != GDK_KEY_PRESS) {
284                 return FALSE;
285         }
286
287         switch (event->keyval) {
288                 case GDK_KP_Enter:
289                 case GDK_Return:
290                         new_task_entry_activated_cb (entry, window);
291                         return TRUE;
292         }
293
294         return FALSE;
295 }
296
297 static void
298 task_view_selection_changed_cb (HildonTouchSelector *view,
299                                 gint                 column,
300                                 MilkMainWindow      *window)
301 {
302         MilkMainWindowPrivate *priv;
303         GList *rows;
304         gboolean show = FALSE;
305         gint i;
306
307         priv = MILK_MAIN_WINDOW_PRIVATE (window);
308
309         rows = hildon_touch_selector_get_selected_rows (view, column);
310         show = (g_list_length (rows) > 0);
311
312         for (i = 0; i < G_N_ELEMENTS (menu_items_selection_required); i++) {
313                 GtkWidget *w;
314
315                 w = g_object_get_data (
316                                 G_OBJECT (priv->app_menu),
317                                 menu_items_selection_required[i].id);
318
319                 if (show)
320                         gtk_widget_show (w);
321                 else
322                         gtk_widget_hide (w);
323         }
324
325         g_list_free (rows);
326 }
327
328 static GtkWidget*
329 create_menu (gpointer user_data)
330 {
331         HildonAppMenu *menu;
332         MenuItem *menu_array;
333         gint i, length;
334         GtkWidget *w;
335
336         menu = HILDON_APP_MENU (hildon_app_menu_new ());
337
338         menu_array = menu_items_always_shown;
339         length = G_N_ELEMENTS (menu_items_always_shown);
340         for (i = 0; i < length; i++) {
341                 w = hildon_button_new_with_text (
342                                 HILDON_SIZE_FINGER_HEIGHT | HILDON_SIZE_AUTO_WIDTH,
343                                 HILDON_BUTTON_ARRANGEMENT_VERTICAL,
344                                 _(menu_array[i].display_name), "");
345                 g_signal_connect (w, "clicked",
346                                 G_CALLBACK (menu_array[i].callback), user_data);
347                 g_object_set_data (G_OBJECT (menu), menu_array[i].id, w);
348                 hildon_app_menu_append (menu, GTK_BUTTON (w));
349                 gtk_widget_show (w);
350         }
351
352         menu_array = menu_items_selection_required;
353         length = G_N_ELEMENTS (menu_items_selection_required);
354         for (i = 0; i < length; i++) {
355                 w = hildon_button_new_with_text (
356                                 HILDON_SIZE_FINGER_HEIGHT | HILDON_SIZE_AUTO_WIDTH,
357                                 HILDON_BUTTON_ARRANGEMENT_VERTICAL,
358                                 menu_array[i].display_name, "");
359                 g_signal_connect (w, "clicked",
360                                 G_CALLBACK (menu_array[i].callback), user_data);
361                 g_object_set_data (G_OBJECT (menu), menu_array[i].id, w);
362                 hildon_app_menu_append (menu, GTK_BUTTON (w));
363                 gtk_widget_hide (w);
364         }
365
366         gtk_widget_show (GTK_WIDGET (menu));
367
368         return GTK_WIDGET (menu);
369 }
370
371 static void
372 priority_column_render_func (GtkCellLayout   *cell_layout,
373                              GtkCellRenderer *renderer,
374                              GtkTreeModel    *model,
375                              GtkTreeIter     *iter,
376                              gpointer         user_data)
377 {
378         RtmTask *task;
379         const char *priority;
380         GdkColor color = {};
381         gboolean color_set = TRUE;
382
383         gtk_tree_model_get (
384                         model, iter, MILK_TASK_MODEL_COLUMN_TASK, &task, -1);
385
386         priority = rtm_task_get_priority (task);
387
388         if (FALSE) {
389         } else if (!g_strcmp0 (priority, "1")) {
390                 gdk_color_parse ("#ea5200", &color);
391         } else if (!g_strcmp0 (priority, "2")) {
392                 gdk_color_parse ("#0060bf", &color);
393         } else if (!g_strcmp0 (priority, "3")) {
394                 gdk_color_parse ("#359aff", &color);
395         } else {
396                 color_set = FALSE;
397         }
398
399         g_object_set (renderer,
400                         "cell-background-gdk", color_set ? &color : NULL,
401                         "cell-background-set", color_set,
402                         NULL);
403
404         g_object_unref (task);
405 }
406
407 static void
408 name_column_render_func (GtkCellLayout   *cell_layout,
409                          GtkCellRenderer *renderer,
410                          GtkTreeModel    *model,
411                          GtkTreeIter     *iter,
412                          gpointer         user_data)
413 {
414         RtmTask *task;
415
416         gtk_tree_model_get (
417                         model, iter, MILK_TASK_MODEL_COLUMN_TASK, &task, -1);
418         g_object_set (renderer, "text", rtm_task_get_name (task), NULL);
419
420         g_object_unref (task);
421 }
422
423 static gboolean
424 begin_cache_idle (MilkMainWindow *window)
425 {
426         MilkMainWindowPrivate *priv;
427
428         priv = MILK_MAIN_WINDOW_PRIVATE (window);
429
430         milk_cache_authenticate (priv->cache);
431
432         return FALSE;
433 }
434
435 static void
436 milk_main_window_constructed (GObject* object)
437 {
438         MilkMainWindow *self = MILK_MAIN_WINDOW (object);
439         MilkMainWindowPrivate *priv = MILK_MAIN_WINDOW_PRIVATE (object);
440         GtkWidget *w;
441         GtkTreeModel *model;
442         GtkCellRenderer *renderer;
443         HildonTouchSelectorColumn *col;
444
445         w = gtk_vbox_new (FALSE, HILDON_MARGIN_DEFAULT);
446         gtk_container_add (GTK_CONTAINER (self), w);
447         priv->main_vbox = w;
448
449         /*
450          * New Task entry
451          */
452         w = hildon_entry_new (HILDON_SIZE_FINGER_HEIGHT);
453         gtk_box_pack_start (GTK_BOX (priv->main_vbox), w, FALSE, FALSE, 0);
454
455         /* FIXME: change this to hildon_gtk_entry_set_placeholder_text() is
456          * fixed, since this is deprecated */
457         hildon_entry_set_placeholder (HILDON_ENTRY (w),
458                         _(NEW_TASK_PLACEHOLDER_TEXT));
459         priv->new_task_entry = w;
460         g_signal_connect (G_OBJECT (w), "activate",
461                         G_CALLBACK (new_task_entry_activated_cb), self);
462         g_signal_connect (G_OBJECT (w), "key-press-event",
463                         G_CALLBACK (new_task_entry_key_press_event_cb), self);
464
465         /*
466          * Task List
467          */
468         model = GTK_TREE_MODEL (milk_task_model_new ());
469         w = hildon_touch_selector_new ();
470
471         col = hildon_touch_selector_append_column (
472                         HILDON_TOUCH_SELECTOR (w), model, NULL, NULL);
473
474         renderer = gtk_cell_renderer_text_new ();
475         g_object_set (renderer,
476                         "width", HILDON_ICON_PIXEL_SIZE_XSMALL,
477                         "width-chars", 1,
478                         NULL);
479         gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (col), renderer, FALSE);
480         gtk_cell_layout_set_cell_data_func (
481                         GTK_CELL_LAYOUT (col), renderer,
482                         (GtkCellLayoutDataFunc) priority_column_render_func,
483                         self, NULL);
484
485         renderer = gtk_cell_renderer_text_new ();
486         g_object_set (renderer,
487                         "ellipsize", PANGO_ELLIPSIZE_END,
488                         "alignment", PANGO_ALIGN_LEFT,
489                         "xpad", HILDON_MARGIN_DEFAULT,
490                         NULL);
491         gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (col), renderer, TRUE);
492         gtk_cell_layout_set_cell_data_func (
493                         GTK_CELL_LAYOUT (col), renderer,
494                         (GtkCellLayoutDataFunc) name_column_render_func,
495                         self, NULL);
496
497         g_object_unref (model);
498
499         hildon_touch_selector_set_column_selection_mode (
500                         HILDON_TOUCH_SELECTOR (w),
501                         HILDON_TOUCH_SELECTOR_SELECTION_MODE_MULTIPLE);
502         hildon_touch_selector_set_hildon_ui_mode (
503                         HILDON_TOUCH_SELECTOR (w), HILDON_UI_MODE_EDIT);
504         hildon_touch_selector_unselect_all (
505                         HILDON_TOUCH_SELECTOR (w), TASK_VIEW_COLUMN_TITLE);
506
507         gtk_box_pack_start (GTK_BOX (priv->main_vbox), w, TRUE, TRUE, 0);
508         g_object_set (w, "can-focus", TRUE, NULL);
509         gtk_widget_grab_focus (w);
510
511         g_signal_connect (
512                         G_OBJECT (w), "changed",
513                         G_CALLBACK (task_view_selection_changed_cb), self);
514         priv->task_view = w;
515
516         priv->app_menu = create_menu (self);
517         hildon_window_set_app_menu (
518                         HILDON_WINDOW (self), HILDON_APP_MENU (priv->app_menu));
519
520         /* set up the cache */
521         priv->cache = milk_cache_get_default ();
522
523         /* break a cyclical dependency by doing this after the window is
524          * constructed */
525         g_idle_add ((GSourceFunc) begin_cache_idle, self);
526 }
527
528 static void
529 milk_main_window_class_init (MilkMainWindowClass *klass)
530 {
531         GObjectClass *object_class = G_OBJECT_CLASS (klass);
532
533         g_type_class_add_private (klass, sizeof (MilkMainWindowPrivate));
534
535         object_class->get_property = milk_main_window_get_property;
536         object_class->set_property = milk_main_window_set_property;
537         object_class->constructed = milk_main_window_constructed;
538         object_class->dispose = milk_main_window_dispose;
539 }
540
541 static void
542 milk_main_window_init (MilkMainWindow *self)
543 {
544         self->priv = G_TYPE_INSTANCE_GET_PRIVATE (
545                         self, MILK_TYPE_MAIN_WINDOW, MilkMainWindowPrivate);
546 }
547
548 GtkWidget*
549 milk_main_window_get_default ()
550 {
551         if (!default_window) {
552                 default_window = g_object_new (MILK_TYPE_MAIN_WINDOW, NULL);
553         }
554
555         return default_window;
556 }