1 /**
2 * @file feed_list_view.c the feed list in a GtkTreeView
3 *
4 * Copyright (C) 2004-2013 Lars Windolf <lars.windolf@gmx.de>
5 * Copyright (C) 2004-2006 Nathan J. Conrad <t98502@users.sourceforge.net>
6 * Copyright (C) 2005 Raphael Slinckx <raphael@slinckx.net>
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21 */
22
23 #include "ui/feed_list_view.h"
24
25 #include <gtk/gtk.h>
26 #include <gdk/gdkkeysyms.h>
27
28 #include "common.h"
29 #include "conf.h"
30 #include "debug.h"
31 #include "feed.h"
32 #include "feedlist.h"
33 #include "folder.h"
34 #include "net_monitor.h"
35 #include "newsbin.h"
36 #include "vfolder.h"
37 #include "ui/browser_tabs.h"
38 #include "ui/liferea_dialog.h"
39 #include "ui/liferea_shell.h"
40 #include "ui/subscription_dialog.h"
41 #include "ui/ui_dnd.h"
42 #include "ui/feed_list_node.h"
43 #include "fl_sources/node_source.h"
44
45 GtkTreeModel *filter = NULL;
46 GtkTreeStore *feedstore = NULL;
47
48 gboolean feedlist_reduced_unread = FALSE;
49
50 static void
feed_list_view_row_changed_cb(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter)51 feed_list_view_row_changed_cb (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter)
52 {
53 nodePtr node;
54
55 gtk_tree_model_get (model, iter, FS_PTR, &node, -1);
56 if (node)
57 feed_list_node_update_iter (node->id, iter);
58 }
59
60 static void
feed_list_view_selection_changed_cb(GtkTreeSelection * selection,gpointer data)61 feed_list_view_selection_changed_cb (GtkTreeSelection *selection, gpointer data)
62 {
63 GtkTreeIter iter;
64 GtkTreeModel *model;
65 nodePtr node;
66
67 if (gtk_tree_selection_get_selected (selection, &model, &iter)) {
68 gtk_tree_model_get (model, &iter, FS_PTR, &node, -1);
69
70 debug1 (DEBUG_GUI, "feed list selection changed to \"%s\"", node?node_get_title (node):"Empty node");
71
72 if (!node) {
73 /* The selected iter is an "empty" node added to an empty folder. We get the parent's node
74 * to set it as the selected node. This is useful if the user adds a feed, the folder will
75 * be used as location for the new node. */
76 GtkTreeIter parent;
77 if (gtk_tree_model_iter_parent (model, &parent, &iter))
78 gtk_tree_model_get (model, &parent, FS_PTR, &node, -1);
79 else {
80 debug0 (DEBUG_GUI, "A selected null node has no parent. This should not happen.");
81 return;
82 }
83 liferea_shell_update_feed_menu (TRUE, FALSE, FALSE);
84 } else {
85 gboolean allowModify = (NODE_SOURCE_TYPE (node->source->root)->capabilities & NODE_SOURCE_CAPABILITY_WRITABLE_FEEDLIST);
86 liferea_shell_update_update_menu ((NODE_TYPE (node)->capabilities & NODE_CAPABILITY_UPDATE) ||
87 (NODE_TYPE (node)->capabilities & NODE_CAPABILITY_UPDATE_CHILDS));
88 liferea_shell_update_feed_menu (allowModify, TRUE, allowModify);
89 }
90
91 browser_tabs_show_headlines (); // FIXME: emit signal to item list instead of bother the tabs manager
92
93 /* 1.) update feed list and item list states */
94 feedlist_selection_changed (node);
95
96 /* 2.) Refilter the GtkTreeView to get rid of nodes with 0 unread
97 messages when in reduced mode. */
98 gtk_tree_model_filter_refilter (GTK_TREE_MODEL_FILTER (filter));
99
100 } else {
101 /* If we cannot get the new selection we keep the old one
102 this happens when we're doing drag&drop for example. */
103 }
104 }
105
106 static void
feed_list_view_row_activated_cb(GtkTreeView * tv,GtkTreePath * path,GtkTreeViewColumn * col,gpointer data)107 feed_list_view_row_activated_cb (GtkTreeView *tv, GtkTreePath *path, GtkTreeViewColumn *col, gpointer data)
108 {
109 GtkTreeIter iter;
110 nodePtr node;
111
112 gtk_tree_model_get_iter (gtk_tree_view_get_model (tv), &iter, path);
113 gtk_tree_model_get (gtk_tree_view_get_model (tv), &iter, FS_PTR, &node, -1);
114 if (node && IS_FOLDER (node)) {
115 if (gtk_tree_view_row_expanded (tv, path))
116 gtk_tree_view_collapse_row (tv, path);
117 else
118 gtk_tree_view_expand_row (tv, path, FALSE);
119 }
120
121 }
122
123 static gboolean
feed_list_view_key_press_cb(GtkWidget * widget,GdkEventKey * event,gpointer data)124 feed_list_view_key_press_cb (GtkWidget *widget, GdkEventKey *event, gpointer data)
125 {
126 if ((event->type == GDK_KEY_PRESS) &&
127 (event->state == 0) &&
128 (event->keyval == GDK_KEY_Delete)) {
129 nodePtr node = feedlist_get_selected ();
130
131 if(node) {
132 if (event->state & GDK_SHIFT_MASK)
133 feedlist_remove_node (node);
134 else
135 feed_list_node_remove (node);
136 return TRUE;
137 }
138 }
139 return FALSE;
140 }
141
142 static gboolean
feed_list_view_filter_visible_function(GtkTreeModel * model,GtkTreeIter * iter,gpointer data)143 feed_list_view_filter_visible_function (GtkTreeModel *model, GtkTreeIter *iter, gpointer data)
144 {
145 gint count;
146 nodePtr node;
147
148 if (!feedlist_reduced_unread)
149 return TRUE;
150
151 gtk_tree_model_get (model, iter, FS_PTR, &node, FS_UNREAD, &count, -1);
152 if (!node)
153 return FALSE;
154
155 if (IS_FOLDER (node) || IS_NODE_SOURCE (node))
156 return FALSE;
157
158 if (IS_VFOLDER (node))
159 return TRUE;
160
161 /* Do not hide in any case if the node is selected, otherwise
162 the last unread item of a feed causes the feed to vanish
163 when clicking it */
164 if (feedlist_get_selected () == node)
165 return TRUE;
166
167 if (count > 0)
168 return TRUE;
169
170 return FALSE;
171 }
172
173 static void
feed_list_view_expand(nodePtr node)174 feed_list_view_expand (nodePtr node)
175 {
176 if (node->parent)
177 feed_list_view_expand (node->parent);
178
179 feed_list_node_set_expansion (node, TRUE);
180 }
181
182 static void
feed_list_view_restore_folder_expansion(nodePtr node)183 feed_list_view_restore_folder_expansion (nodePtr node)
184 {
185 if (node->expanded)
186 feed_list_view_expand (node);
187
188 node_foreach_child (node, feed_list_view_restore_folder_expansion);
189 }
190
191 static void
feed_list_view_reduce_mode_changed()192 feed_list_view_reduce_mode_changed ()
193 {
194 GtkTreeView *treeview;
195
196 treeview = GTK_TREE_VIEW (liferea_shell_lookup ("feedlist"));
197
198 if (feedlist_reduced_unread) {
199 gtk_tree_view_set_reorderable (treeview, FALSE);
200 gtk_tree_view_set_model (treeview, GTK_TREE_MODEL (filter));
201 gtk_tree_model_filter_refilter (GTK_TREE_MODEL_FILTER (filter));
202 } else {
203 gtk_tree_view_set_reorderable (treeview, TRUE);
204 gtk_tree_model_filter_refilter (GTK_TREE_MODEL_FILTER (filter));
205 gtk_tree_view_set_model (treeview, GTK_TREE_MODEL (feedstore));
206
207 feedlist_foreach (feed_list_view_restore_folder_expansion);
208 }
209 }
210
211 static void
feed_list_view_set_reduce_mode(gboolean newReduceMode)212 feed_list_view_set_reduce_mode (gboolean newReduceMode)
213 {
214 feedlist_reduced_unread = newReduceMode;
215 conf_set_bool_value (REDUCED_FEEDLIST, feedlist_reduced_unread);
216 feed_list_view_reduce_mode_changed ();
217 feed_list_node_reload_feedlist ();
218 }
219
220 static gint
feed_list_view_sort_folder_compare(gconstpointer a,gconstpointer b)221 feed_list_view_sort_folder_compare (gconstpointer a, gconstpointer b)
222 {
223 nodePtr n1 = (nodePtr)a;
224 nodePtr n2 = (nodePtr)b;
225
226 gchar *s1 = g_utf8_casefold (n1->title, -1);
227 gchar *s2 = g_utf8_casefold (n2->title, -1);
228
229 gint result = strcmp (s1, s2);
230
231 g_free (s1);
232 g_free (s2);
233
234 return result;
235 }
236
237 void
feed_list_view_sort_folder(nodePtr folder)238 feed_list_view_sort_folder (nodePtr folder)
239 {
240 GtkTreeView *treeview;
241
242 treeview = GTK_TREE_VIEW (liferea_shell_lookup ("feedlist"));
243 /* Unset the model from the view before clearing it and rebuilding it.*/
244 gtk_tree_view_set_model (treeview, NULL);
245 folder->children = g_slist_sort (folder->children, feed_list_view_sort_folder_compare);
246 feed_list_node_reload_feedlist ();
247 /* Reduce mode didn't actually change but we need to set the
248 * correct model according to the setting in the same way : */
249 feed_list_view_reduce_mode_changed ();
250 feedlist_foreach (feed_list_view_restore_folder_expansion);
251 feedlist_schedule_save ();
252 }
253
254 /* sets up the entry list store and connects it to the entry list
255 view in the main window */
256 void
feed_list_view_init(GtkTreeView * treeview)257 feed_list_view_init (GtkTreeView *treeview)
258 {
259 GtkCellRenderer *titleRenderer, *countRenderer;
260 GtkCellRenderer *iconRenderer;
261 GtkTreeViewColumn *column, *column2;
262 GtkTreeSelection *select;
263
264 debug_enter ("feed_list_view_init");
265
266 /* Set up store */
267 feedstore = gtk_tree_store_new (FS_LEN,
268 G_TYPE_STRING,
269 G_TYPE_ICON,
270 G_TYPE_POINTER,
271 G_TYPE_UINT,
272 G_TYPE_STRING);
273
274 gtk_tree_view_set_model (GTK_TREE_VIEW (treeview), GTK_TREE_MODEL (feedstore));
275
276 /* Prepare filter */
277 filter = gtk_tree_model_filter_new (GTK_TREE_MODEL(feedstore), NULL);
278 gtk_tree_model_filter_set_visible_func (GTK_TREE_MODEL_FILTER (filter),
279 feed_list_view_filter_visible_function,
280 NULL,
281 NULL);
282
283 g_signal_connect (G_OBJECT (feedstore), "row-changed", G_CALLBACK (feed_list_view_row_changed_cb), NULL);
284
285 /* we render the icon/state, the feed title and the unread count */
286 iconRenderer = gtk_cell_renderer_pixbuf_new ();
287 titleRenderer = gtk_cell_renderer_text_new ();
288 countRenderer = gtk_cell_renderer_text_new ();
289
290 gtk_cell_renderer_set_alignment (countRenderer, 1.0, 0);
291
292 column = gtk_tree_view_column_new ();
293 column2 = gtk_tree_view_column_new ();
294
295 gtk_tree_view_column_pack_start (column, iconRenderer, FALSE);
296 gtk_tree_view_column_pack_start (column, titleRenderer, TRUE);
297 gtk_tree_view_column_pack_end (column2, countRenderer, FALSE);
298
299 gtk_tree_view_column_add_attribute (column, iconRenderer, "gicon", FS_ICON);
300 gtk_tree_view_column_add_attribute (column, titleRenderer, "markup", FS_LABEL);
301 gtk_tree_view_column_add_attribute (column2, countRenderer, "markup", FS_COUNT);
302
303 gtk_tree_view_column_set_expand (column, TRUE);
304 gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_GROW_ONLY);
305 gtk_tree_view_column_set_sizing (column2, GTK_TREE_VIEW_COLUMN_GROW_ONLY);
306 gtk_tree_view_append_column (treeview, column);
307 gtk_tree_view_append_column (treeview, column2);
308
309 g_object_set (titleRenderer, "ellipsize", PANGO_ELLIPSIZE_END, NULL);
310
311 g_signal_connect (G_OBJECT (treeview), "row-activated", G_CALLBACK (feed_list_view_row_activated_cb), NULL);
312 g_signal_connect (G_OBJECT (treeview), "key-press-event", G_CALLBACK (feed_list_view_key_press_cb), NULL);
313
314 select = gtk_tree_view_get_selection (treeview);
315 gtk_tree_selection_set_mode (select, GTK_SELECTION_SINGLE);
316
317 g_signal_connect (G_OBJECT (select), "changed",
318 G_CALLBACK (feed_list_view_selection_changed_cb),
319 liferea_shell_lookup ("feedlist"));
320
321 conf_get_bool_value (REDUCED_FEEDLIST, &feedlist_reduced_unread);
322 if (feedlist_reduced_unread)
323 feed_list_view_reduce_mode_changed (); /* before menu setup for reduced mode check box to be correct */
324
325 ui_dnd_setup_feedlist (feedstore);
326 liferea_shell_update_feed_menu (TRUE, FALSE, FALSE);
327 liferea_shell_update_allitems_actions (FALSE, FALSE);
328
329 debug_exit ("feed_list_view_init");
330 }
331
332 void
feed_list_view_select(nodePtr node)333 feed_list_view_select (nodePtr node)
334 {
335 GtkTreeView *treeview;
336 GtkTreeModel *model;
337
338 treeview = GTK_TREE_VIEW (liferea_shell_lookup ("feedlist"));
339 model = gtk_tree_view_get_model (treeview);
340
341 if (node && node != feedlist_get_root ()) {
342 GtkTreePath *path;
343
344 /* in filtered mode we need to convert the iterator */
345 if (feedlist_reduced_unread) {
346 GtkTreeIter iter;
347 gtk_tree_model_filter_convert_child_iter_to_iter (GTK_TREE_MODEL_FILTER (filter), &iter, feed_list_node_to_iter (node->id));
348 path = gtk_tree_model_get_path (model, &iter);
349 } else {
350 path = gtk_tree_model_get_path (model, feed_list_node_to_iter (node->id));
351 }
352
353 if (node->parent)
354 feed_list_view_expand (node->parent);
355
356 if (path) {
357 gtk_tree_view_scroll_to_cell (treeview, path, NULL, FALSE, 0.0, 0.0);
358 gtk_tree_view_set_cursor (treeview, path, NULL, FALSE);
359 gtk_tree_path_free (path);
360 }
361 } else {
362 GtkTreeSelection *selection = gtk_tree_view_get_selection (treeview);
363 gtk_tree_selection_unselect_all (selection);
364 }
365 }
366
367 void
on_menu_properties(GSimpleAction * action,GVariant * parameter,gpointer user_data)368 on_menu_properties (GSimpleAction *action, GVariant *parameter, gpointer user_data)
369 {
370 nodePtr node = feedlist_get_selected ();
371
372 NODE_TYPE (node)->request_properties (node);
373 }
374
375 void
on_menu_delete(GSimpleAction * action,GVariant * parameter,gpointer user_data)376 on_menu_delete(GSimpleAction *action, GVariant *parameter, gpointer user_data)
377 {
378 feed_list_node_remove (feedlist_get_selected ());
379 }
380
381 static void
do_menu_update(nodePtr node)382 do_menu_update (nodePtr node)
383 {
384 if (network_monitor_is_online ())
385 node_update_subscription (node, GUINT_TO_POINTER (FEED_REQ_PRIORITY_HIGH));
386 else
387 liferea_shell_set_status_bar (_("Liferea is in offline mode. No update possible."));
388
389 }
390
391 void
on_menu_update(GSimpleAction * action,GVariant * parameter,gpointer user_data)392 on_menu_update (GSimpleAction *action, GVariant *parameter, gpointer user_data)
393 {
394 nodePtr node = NULL;
395
396 if (user_data)
397 node = (nodePtr) user_data;
398 else
399 node = feedlist_get_selected ();
400
401 if (node)
402 do_menu_update (node);
403 else
404 g_warning ("on_menu_update: no feedlist selected");
405 }
406
407 void
on_menu_update_all(GSimpleAction * action,GVariant * parameter,gpointer user_data)408 on_menu_update_all(GSimpleAction *action, GVariant *parameter, gpointer user_data)
409 {
410 do_menu_update (feedlist_get_root ());
411 }
412
413 void
on_action_mark_all_read(GSimpleAction * action,GVariant * parameter,gpointer user_data)414 on_action_mark_all_read (GSimpleAction *action, GVariant *parameter, gpointer user_data)
415 {
416 nodePtr feedlist;
417 gboolean confirm_mark_read;
418 gboolean do_mark_read = TRUE;
419
420 if (!g_strcmp0 (g_action_get_name (G_ACTION (action)), "mark-all-feeds-read"))
421 feedlist = feedlist_get_root ();
422 else if (user_data)
423 feedlist = (nodePtr) user_data;
424 else
425 feedlist = feedlist_get_selected ();
426
427 conf_get_bool_value (CONFIRM_MARK_ALL_READ, &confirm_mark_read);
428
429 if (confirm_mark_read) {
430 gint result;
431 GtkMessageDialog *confirm_dialog = GTK_MESSAGE_DIALOG (liferea_dialog_new ("mark_read_dialog"));
432 GtkWidget *dont_ask_toggle = liferea_dialog_lookup (GTK_WIDGET (confirm_dialog), "dontAskAgainToggle");
433 const gchar *feed_title = (feedlist_get_root () == feedlist) ? _("all feeds"):node_get_title (feedlist);
434 gchar *primary_message = g_strdup_printf (_("Mark %s as read ?"), feed_title);
435
436 g_object_set (confirm_dialog, "text", primary_message, NULL);
437 g_free (primary_message);
438 gtk_message_dialog_format_secondary_text (confirm_dialog, _("Are you sure you want to mark all items in %s as read ?"), feed_title);
439
440 conf_bind (CONFIRM_MARK_ALL_READ, dont_ask_toggle, "active", G_SETTINGS_BIND_DEFAULT | G_SETTINGS_BIND_INVERT_BOOLEAN);
441
442 result = gtk_dialog_run (GTK_DIALOG (confirm_dialog));
443 if (result != GTK_RESPONSE_OK)
444 do_mark_read = FALSE;
445 gtk_widget_destroy (GTK_WIDGET (confirm_dialog));
446 }
447
448 if (do_mark_read)
449 feedlist_mark_all_read (feedlist);
450 }
451
452 void
on_menu_feed_new(GSimpleAction * menuitem,GVariant * parameter,gpointer user_data)453 on_menu_feed_new (GSimpleAction *menuitem, GVariant *parameter, gpointer user_data)
454 {
455 node_type_request_interactive_add (feed_get_node_type ());
456 }
457
458 void
on_new_plugin_activate(GSimpleAction * menuitem,GVariant * parameter,gpointer user_data)459 on_new_plugin_activate (GSimpleAction *menuitem, GVariant *parameter, gpointer user_data)
460 {
461 node_type_request_interactive_add (node_source_get_node_type ());
462 }
463
464 void
on_new_newsbin_activate(GSimpleAction * menuitem,GVariant * parameter,gpointer user_data)465 on_new_newsbin_activate (GSimpleAction *menuitem, GVariant *parameter, gpointer user_data)
466 {
467 node_type_request_interactive_add (newsbin_get_node_type ());
468 }
469
470 void
on_menu_folder_new(GSimpleAction * menuitem,GVariant * parameter,gpointer user_data)471 on_menu_folder_new (GSimpleAction *menuitem, GVariant *parameter, gpointer user_data)
472 {
473 node_type_request_interactive_add (folder_get_node_type ());
474 }
475
476 void
on_new_vfolder_activate(GSimpleAction * menuitem,GVariant * parameter,gpointer user_data)477 on_new_vfolder_activate (GSimpleAction *menuitem, GVariant *parameter, gpointer user_data)
478 {
479 node_type_request_interactive_add (vfolder_get_node_type ());
480 }
481
482 void
on_feedlist_reduced_activate(GSimpleAction * action,GVariant * parameter,gpointer user_data)483 on_feedlist_reduced_activate (GSimpleAction *action, GVariant *parameter, gpointer user_data)
484 {
485 GVariant *state = g_action_get_state (action);
486 gboolean val = !g_variant_get_boolean (state);
487 feed_list_view_set_reduce_mode (val);
488 g_simple_action_set_state (action, g_variant_new_boolean (val));
489 g_object_unref (state);
490 }
491