2
* @file ui_itemlist.c item list GUI handling
4
* Copyright (C) 2004-2009 Lars Lindner <lars.lindner@gmail.com>
5
* Copyright (C) 2004-2006 Nathan J. Conrad <t98502@users.sourceforge.net>
7
* This program is free software; you can redistribute it and/or modify
8
* it under the terms of the GNU General Public License as published by
9
* the Free Software Foundation; either version 2 of the License, or
10
* (at your option) any later version.
12
* This program is distributed in the hope that it will be useful,
13
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
* GNU General Public License for more details.
17
* You should have received a copy of the GNU General Public License
18
* along with this program; if not, write to the Free Software
19
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22
#include "ui/ui_itemlist.h"
25
#include <gdk/gdkkeysyms.h>
37
#include "ui/browser_tabs.h"
38
#include "ui/liferea_shell.h"
39
#include "ui/ui_common.h"
40
#include "ui/ui_popup.h"
42
/** Enumeration of the columns in the itemstore. */
44
IS_TIME, /**< Time of item creation */
45
IS_TIME_STR, /**< Time of item creation as a string*/
46
IS_LABEL, /**< Displayed name */
47
IS_STATEICON, /**< Pixbuf reference to the item's state icon */
48
IS_NR, /**< Item id, to lookup item ptr from parent feed */
49
IS_PARENT, /**< Parent node pointer */
50
IS_FAVICON, /**< Pixbuf reference to the item's feed's icon */
51
IS_ENCICON, /**< Pixbuf reference to the item's enclosure icon */
52
IS_ENCLOSURE, /**< Flag wether enclosure is attached or not */
53
IS_SOURCE, /**< Source node pointer */
54
IS_STATE, /**< Original item state (unread, flagged...) for sorting */
55
ITEMSTORE_UNREAD, /**< Flag wether "unread" icon is to be shown */
56
ITEMSTORE_LEN /**< Number of columns in the itemstore */
59
static GHashTable *item_id_to_iter = NULL; /** hash table used for fast item id->tree iter lookup */
61
static GtkTreeView *itemlist_treeview = NULL;
63
/* helper functions for item <-> iter conversion */
66
ui_itemlist_contains_item (gulong id)
68
return (NULL != g_hash_table_lookup (item_id_to_iter, GUINT_TO_POINTER (id)));
72
ui_iter_to_item_id (GtkTreeIter *iter)
76
gtk_tree_model_get (gtk_tree_view_get_model (itemlist_treeview), iter, IS_NR, &id, -1);
81
ui_item_id_to_iter (gulong id, GtkTreeIter *iter)
83
GtkTreeIter *old_iter;
85
old_iter = g_hash_table_lookup (item_id_to_iter, GUINT_TO_POINTER (id));
94
ui_itemlist_date_sort_func (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer user_data)
99
gtk_tree_model_get (model, a, IS_TIME, &timea, -1);
100
gtk_tree_model_get (model, b, IS_TIME, &timeb, -1);
101
diff = difftime ((time_t)timeb, (time_t)timea);
113
ui_itemlist_favicon_sort_func (GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b, gpointer user_data)
115
nodePtr node1, node2;
117
gtk_tree_model_get (model, a, IS_SOURCE, &node1, -1);
118
gtk_tree_model_get (model, b, IS_SOURCE, &node2, -1);
120
if (!node1->id || !node2->id)
123
return strcmp (node1->id, node2->id);
127
ui_itemlist_set_sort_column (nodeViewSortType sortType, gboolean sortReversed)
132
case NODE_VIEW_SORT_BY_TITLE:
133
sortColumn = IS_LABEL;
135
case NODE_VIEW_SORT_BY_PARENT:
136
sortColumn = IS_PARENT;
139
sortColumn = IS_TIME;
143
gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (gtk_tree_view_get_model (itemlist_treeview)),
145
sortReversed?GTK_SORT_DESCENDING:GTK_SORT_ASCENDING);
149
ui_itemlist_reset_tree_store (void)
152
GtkTreeStore *itemstore;
155
ui_itemlist_clear ();
157
/* drop old tree store */
158
model = gtk_tree_view_get_model (itemlist_treeview);
159
gtk_tree_view_set_model (itemlist_treeview, NULL);
161
g_object_unref (model);
163
/* create and assign new one */
164
itemstore = gtk_tree_store_new (ITEMSTORE_LEN,
165
G_TYPE_UINT64, /* IS_TIME */
166
G_TYPE_STRING, /* IS_TIME_STR */
167
G_TYPE_STRING, /* IS_LABEL */
168
GDK_TYPE_PIXBUF, /* IS_STATEICON */
169
G_TYPE_ULONG, /* IS_NR */
170
G_TYPE_POINTER, /* IS_PARENT */
171
GDK_TYPE_PIXBUF, /* IS_FAVICON */
172
GDK_TYPE_PIXBUF, /* IS_ENCICON */
173
G_TYPE_BOOLEAN, /* IS_ENCLOSURE */
174
G_TYPE_POINTER, /* IS_SOURCE */
175
G_TYPE_UINT, /* IS_STATE */
176
G_TYPE_INT /* ITEMSTORE_UNREAD */
178
gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (itemstore), IS_TIME, ui_itemlist_date_sort_func, NULL, NULL);
179
gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (itemstore), IS_SOURCE, ui_itemlist_favicon_sort_func, NULL, NULL);
180
g_signal_connect (G_OBJECT(itemstore), "sort-column-changed", G_CALLBACK (itemlist_sort_column_changed_cb), NULL);
182
gtk_tree_view_set_model (itemlist_treeview, GTK_TREE_MODEL (itemstore));
184
ui_itemlist_prefocus ();
188
ui_itemlist_remove_item (itemPtr item)
192
g_assert (NULL != item);
193
iter = g_hash_table_lookup (item_id_to_iter, GUINT_TO_POINTER (item->id));
195
gtk_tree_store_remove (GTK_TREE_STORE (gtk_tree_view_get_model (itemlist_treeview)), iter);
196
g_hash_table_remove (item_id_to_iter, GUINT_TO_POINTER(item->id));
198
g_warning("Fatal: item to be removed not found in iter lookup hash!");
202
/* cleans up the item list, sets up the iter hash when called for the first time */
204
ui_itemlist_clear (void)
207
GtkTreeStore *itemstore;
209
itemstore = GTK_TREE_STORE (gtk_tree_view_get_model (itemlist_treeview));
211
/* unselecting all items is important to remove items
212
whose removal is deferred until unselecting */
213
gtk_tree_selection_unselect_all (gtk_tree_view_get_selection (itemlist_treeview));
215
adj = gtk_tree_view_get_vadjustment (itemlist_treeview);
216
gtk_adjustment_set_value (adj, 0.0);
217
gtk_tree_view_set_vadjustment (itemlist_treeview, adj);
220
gtk_tree_store_clear (itemstore);
222
g_hash_table_destroy (item_id_to_iter);
224
item_id_to_iter = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_free);
228
ui_itemlist_update_item (itemPtr item)
231
gchar *title, *time_str;
232
const gchar *direction_marker;
235
if (!ui_item_id_to_iter (item->id, &iter))
238
time_str = (0 != item->time) ? itemview_format_date((time_t)item->time) : g_strdup ("");
240
direction_marker = common_get_direction_mark (node_from_id (item->nodeId)->title);
242
title = item->title && strlen (item->title) ? item->title : _("*** No title ***");
243
title = g_strstrip (g_strdup_printf ("%s%s", direction_marker, title));
245
state_icon = item->flagStatus ? ICON_FLAG :
246
!item->readStatus ? ICON_UNREAD :
249
gtk_tree_store_set (GTK_TREE_STORE (gtk_tree_view_get_model (itemlist_treeview)),
252
IS_TIME_STR, time_str,
253
IS_STATEICON, icons[state_icon],
254
ITEMSTORE_UNREAD, item->readStatus ? PANGO_WEIGHT_NORMAL : PANGO_WEIGHT_BOLD,
262
ui_itemlist_update_item_foreach (gpointer key,
268
item = item_load (GPOINTER_TO_UINT (key) /* id */);
272
ui_itemlist_update_item (item);
278
ui_itemlist_update_all_items (void)
280
g_hash_table_foreach (item_id_to_iter, ui_itemlist_update_item_foreach, NULL);
284
ui_itemlist_key_press_cb (GtkWidget *widget,
288
if ((event->type == GDK_KEY_PRESS) && (event->state == 0) && (event->keyval == GDK_Delete))
289
on_remove_item_activate(NULL, NULL);
295
ui_itemlist_new (GtkWidget *mainwindow)
297
GtkCellRenderer *renderer;
298
GtkTreeViewColumn *column;
299
GtkTreeSelection *select;
300
GtkWidget *ilscrolledwindow;
302
ilscrolledwindow = gtk_scrolled_window_new (NULL, NULL);
303
gtk_widget_show (ilscrolledwindow);
304
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (ilscrolledwindow), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
305
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (ilscrolledwindow), GTK_SHADOW_IN);
307
itemlist_treeview = GTK_TREE_VIEW (gtk_tree_view_new ());
308
gtk_container_add (GTK_CONTAINER (ilscrolledwindow), GTK_WIDGET (itemlist_treeview));
309
gtk_widget_show (GTK_WIDGET (itemlist_treeview));
310
gtk_widget_set_name (GTK_WIDGET (itemlist_treeview), "itemlist");
311
gtk_tree_view_set_rules_hint (itemlist_treeview, TRUE);
313
g_object_set_data (G_OBJECT (mainwindow), "itemlist", itemlist_treeview);
315
item_id_to_iter = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, g_free);
317
ui_itemlist_reset_tree_store ();
319
renderer = gtk_cell_renderer_pixbuf_new ();
320
column = gtk_tree_view_column_new_with_attributes ("", renderer, "pixbuf", IS_STATEICON, NULL);
321
gtk_tree_view_append_column (itemlist_treeview, column);
322
gtk_tree_view_column_set_sort_column_id (column, IS_STATE);
324
renderer = gtk_cell_renderer_pixbuf_new ();
325
column = gtk_tree_view_column_new_with_attributes ("", renderer, "pixbuf", IS_ENCICON, NULL);
326
gtk_tree_view_append_column (itemlist_treeview, column);
328
renderer = gtk_cell_renderer_text_new ();
329
column = gtk_tree_view_column_new_with_attributes (_("Date"), renderer,
331
"weight", ITEMSTORE_UNREAD,
333
gtk_tree_view_append_column (itemlist_treeview, column);
334
gtk_tree_view_column_set_sort_column_id(column, IS_TIME);
335
g_object_set (column, "resizable", TRUE, NULL);
337
renderer = gtk_cell_renderer_pixbuf_new ();
338
column = gtk_tree_view_column_new_with_attributes ("", renderer, "pixbuf", IS_FAVICON, NULL);
339
gtk_tree_view_column_set_sort_column_id (column, IS_SOURCE);
340
gtk_tree_view_append_column (itemlist_treeview, column);
342
renderer = gtk_cell_renderer_text_new ();
343
column = gtk_tree_view_column_new_with_attributes (_("Headline"), renderer,
345
"weight", ITEMSTORE_UNREAD,
347
gtk_tree_view_append_column (itemlist_treeview, column);
348
gtk_tree_view_column_set_sort_column_id (column, IS_LABEL);
349
g_object_set (column, "resizable", TRUE, NULL);
350
g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
352
/* And connect signals */
353
g_signal_connect (G_OBJECT (itemlist_treeview), "key-press-event", G_CALLBACK (ui_itemlist_key_press_cb), NULL);
355
/* Setup the selection handler */
356
select = gtk_tree_view_get_selection (itemlist_treeview);
357
gtk_tree_selection_set_mode (select, GTK_SELECTION_SINGLE);
358
g_signal_connect (G_OBJECT (select), "changed",
359
G_CALLBACK (on_itemlist_selection_changed), NULL);
360
g_signal_connect ((gpointer)itemlist_treeview, "button_press_event",
361
G_CALLBACK (on_itemlist_button_press_event), NULL);
362
g_signal_connect ((gpointer)itemlist_treeview, "row_activated",
363
G_CALLBACK (on_Itemlist_row_activated), NULL);
365
return ilscrolledwindow;
369
ui_itemlist_destroy (void)
371
g_hash_table_destroy (item_id_to_iter);
374
/* typically called when filling the item tree view */
376
ui_itemlist_prefocus (void)
378
GtkWidget *focus_widget;
379
GtkTreeSelection *itemselection;
381
/* the following is important to prevent setting the unread
382
flag for the first item in the item list when the user does
383
the first click into the treeview, if we don't do a focus and
384
unselect, GTK would always (exception: clicking on first item)
385
generate two selection-change events (one for the clicked and
386
one for the selected item)!!! */
388
/* we need to restore the focus after we temporarily select the itemlist */
389
focus_widget = gtk_window_get_focus (GTK_WINDOW (liferea_shell_get_window ()));
391
/* prevent marking as unread before focussing, which leads to a selection */
392
gtk_widget_grab_focus (GTK_WIDGET (itemlist_treeview));
394
itemselection = gtk_tree_view_get_selection (itemlist_treeview);
396
gtk_tree_selection_unselect_all (itemselection);
399
gtk_widget_grab_focus (focus_widget);
403
ui_itemlist_add_item (itemPtr item)
405
GtkTreeStore *itemstore;
407
GtkTreeIter old_iter;
412
exists = ui_item_id_to_iter (item->id, &old_iter);
415
itemstore = GTK_TREE_STORE (gtk_tree_view_get_model (itemlist_treeview));
417
node = node_from_id (item->nodeId);
419
return; /* comment items do cause this... maybe filtering them earlier would be a good idea... */
423
iter = g_new0 (GtkTreeIter, 1);
424
gtk_tree_store_prepend (itemstore, iter, NULL);
425
g_hash_table_insert (item_id_to_iter, GUINT_TO_POINTER (item->id), (gpointer)iter);
428
if (item->flagStatus)
430
if (!item->readStatus)
433
gtk_tree_store_set (itemstore, iter,
434
IS_TIME, (guint64)item->time,
437
IS_FAVICON, node->icon,
438
IS_ENCICON, item->hasEnclosure?icons[ICON_ENCLOSURE]:NULL,
439
IS_ENCLOSURE, item->hasEnclosure,
443
ui_itemlist_update_item (item);
447
ui_itemlist_enable_favicon_column (gboolean enabled)
449
/* we depend on the fact that the second column is the favicon column!!!
450
if we are in search mode or have a folder or vfolder we show the favicon
451
column to give a hint where the item comes from ... */
452
gtk_tree_view_column_set_visible (gtk_tree_view_get_column (itemlist_treeview, 3), enabled);
456
ui_itemlist_enable_encicon_column (gboolean enabled)
458
/* we depend on the fact that the third column is the enclosure icon column!!! */
459
gtk_tree_view_column_set_visible (gtk_tree_view_get_column (itemlist_treeview, 1), enabled);
463
on_popup_launchitem_selected (void)
467
item = itemlist_get_selected ();
469
itemview_launch_URL (item_get_source (item), FALSE);
473
liferea_shell_set_important_status_bar (_("No item has been selected"));
478
on_popup_launchitem_in_tab_selected (void)
483
item = itemlist_get_selected ();
485
link = item_get_source (item);
487
browser_tabs_add_new (link, link, FALSE);
489
ui_show_error_box (_("This item has no link specified!"));
493
liferea_shell_set_important_status_bar (_("No item has been selected"));
498
on_Itemlist_row_activated (GtkTreeView *treeview,
500
GtkTreeViewColumn *column,
504
on_popup_launchitem_selected ();
510
on_toggle_item_flag (GtkMenuItem *menuitem,
515
item = itemlist_get_selected ();
517
itemlist_toggle_flag (item);
523
on_popup_toggle_flag (void)
525
on_toggle_item_flag (NULL, NULL);
529
on_toggle_unread_status (GtkMenuItem *menuitem,
534
item = itemlist_get_selected ();
536
itemlist_toggle_read_status (item);
542
on_popup_toggle_read (void)
544
on_toggle_unread_status (NULL, NULL);
548
on_remove_items_activate (GtkMenuItem *menuitem, gpointer user_data)
552
node = feedlist_get_selected ();
553
// FIXME: use node type capability check
554
if (node && (IS_FEED (node) || IS_NEWSBIN (node)))
555
itemlist_remove_all_items (node);
557
ui_show_error_box (_("You must select a feed to delete its items!"));
561
on_remove_item_activate (GtkMenuItem *menuitem, gpointer user_data)
565
item = itemlist_get_selected ();
567
ui_common_treeview_move_cursor (itemlist_treeview, 1);
568
itemlist_remove_item (item);
570
liferea_shell_set_important_status_bar (_("No item has been selected"));
575
on_popup_remove_selected (void)
577
on_remove_item_activate(NULL, NULL);
581
ui_itemlist_select (itemPtr item)
583
GtkTreeSelection *selection;
585
selection = gtk_tree_view_get_selection (itemlist_treeview);
591
if (!ui_item_id_to_iter(item->id, &iter))
592
/* This is an evil hack to fix SF #1870052: crash
593
upon hitting <enter> when no headline selected.
594
FIXME: This code is rotten! Rewrite it! Now! */
595
itemlist_selection_changed (NULL);
597
path = gtk_tree_model_get_path (gtk_tree_view_get_model (itemlist_treeview), &iter);
598
gtk_tree_view_scroll_to_cell (itemlist_treeview, path, NULL, FALSE, 0.0, 0.0);
599
gtk_tree_view_set_cursor (itemlist_treeview, path, NULL, FALSE);
600
gtk_tree_path_free (path);
602
gtk_tree_selection_unselect_all (selection);
607
ui_itemlist_find_unread_item (gulong startId)
611
gboolean valid = TRUE;
613
model = gtk_tree_view_get_model (itemlist_treeview);
616
valid = ui_item_id_to_iter (startId, &iter);
618
valid = gtk_tree_model_get_iter_first (model, &iter);
621
itemPtr item = item_load (ui_iter_to_item_id (&iter));
623
if (!item->readStatus)
627
valid = gtk_tree_model_iter_next(model, &iter);
634
on_next_unread_item_activate (GtkMenuItem *menuitem, gpointer user_data)
636
itemlist_select_next_unread ();
640
on_popup_next_unread_item_selected (gpointer callback_data, guint callback_action, GtkWidget *widget)
642
itemlist_select_next_unread ();
646
on_nextbtn_clicked (GtkButton *button, gpointer user_data)
648
itemlist_select_next_unread ();
652
on_itemlist_button_press_event (GtkWidget *treeview, GdkEventButton *event, gpointer user_data)
654
GtkTreeViewColumn *column;
658
gboolean result = FALSE;
660
if (event->type != GDK_BUTTON_PRESS)
663
/* avoid handling header clicks */
664
if (event->window != gtk_tree_view_get_bin_window (itemlist_treeview))
667
if (!gtk_tree_view_get_path_at_pos (itemlist_treeview, (gint)event->x, (gint)event->y, &path, NULL, NULL, NULL))
670
if (gtk_tree_model_get_iter (gtk_tree_view_get_model (itemlist_treeview), &iter, path))
671
item = item_load (ui_iter_to_item_id (&iter));
673
gtk_tree_path_free (path);
676
GdkEventButton *eb = (GdkEventButton*)event;
677
switch (eb->button) {
679
column = gtk_tree_view_get_column (itemlist_treeview, 0);
681
/* Allow flag toggling when left clicking in the flagging column.
682
We depend on the fact that the state column is the first!!! */
683
if (event->x <= column->width) {
684
itemlist_toggle_flag (item);
690
/* Middle mouse click toggles read status... */
691
itemlist_toggle_read_status (item);
695
ui_itemlist_select (item);
696
ui_popup_item_menu (item, eb->button, eb->time);
707
on_popup_copy_URL_clipboard (void)
711
item = itemlist_get_selected ();
713
gtk_clipboard_set_text (gtk_clipboard_get (GDK_SELECTION_PRIMARY), item_get_source (item), -1);
714
gtk_clipboard_set_text (gtk_clipboard_get (GDK_SELECTION_CLIPBOARD), item_get_source (item), -1);
717
liferea_shell_set_important_status_bar (_("No item has been selected"));
722
ui_itemlist_search_item_link (itemPtr item)
724
gchar *url = social_get_link_search_url (item_get_source (item));
725
itemview_launch_URL (url, FALSE);
730
ui_itemlist_add_item_bookmark (itemPtr item)
732
gchar *url = social_get_bookmark_url (item_get_source (item), item_get_title (item));
733
(void)browser_launch_URL_external (url);
738
on_popup_social_bm_item_selected (void)
742
item = itemlist_get_selected ();
744
ui_itemlist_add_item_bookmark (item);
748
liferea_shell_set_important_status_bar (_("No item has been selected"));
752
on_itemlist_selection_changed (GtkTreeSelection *selection, gpointer data)
758
if (gtk_tree_selection_get_selected (selection, &model, &iter))
759
item = item_load (ui_iter_to_item_id (&iter));
761
liferea_shell_update_item_menu (NULL != item);
763
itemlist_selection_changed (item);
767
itemlist_sort_column_changed_cb (GtkTreeSortable *treesortable, gpointer user_data)
769
gint sortColumn, nodeSort;
770
GtkSortType sortType;
771
gboolean sorted, changed;
773
if (feedlist_get_selected () == NULL)
776
sorted = gtk_tree_sortable_get_sort_column_id (treesortable, &sortColumn, &sortType);
780
switch (sortColumn) {
783
nodeSort = NODE_VIEW_SORT_BY_TIME;
786
nodeSort = NODE_VIEW_SORT_BY_TITLE;
790
nodeSort = NODE_VIEW_SORT_BY_PARENT;
794
changed = node_set_sort_column (feedlist_get_selected (), nodeSort, sortType == GTK_SORT_DESCENDING);
796
feedlist_schedule_save ();
799
/* needed because switching does sometimes returns to the tree
800
view with a very disturbing horizontal scrolling state */
802
ui_itemlist_scroll_left (void)
804
GtkTreeViewColumn *column;
807
if (2 != itemlist_get_view_mode ()) {
808
gtk_tree_view_get_cursor (itemlist_treeview, &path, &column);
810
column = gtk_tree_view_get_column (itemlist_treeview, 1);
811
gtk_tree_view_set_cursor (itemlist_treeview, path, column, FALSE);
812
gtk_tree_path_free (path);