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