1 /**
2  * @file feed_list_node.c  Handling feed list nodes
3  *
4  * Copyright (C) 2004-2006 Nathan J. Conrad <t98502@users.sourceforge.net>
5  * Copyright (C) 2004-2016 Lars Windolf <lars.windolf@gmx.de>
6  *
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.
11  *
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.
16  *
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
20  */
21 
22 #include "ui/feed_list_node.h"
23 
24 #include "common.h"
25 #include "debug.h"
26 #include "feedlist.h"
27 #include "fl_sources/node_source.h"
28 #include "folder.h"
29 #include "render.h"
30 #include "vfolder.h"
31 #include "ui/icons.h"
32 #include "ui/liferea_dialog.h"
33 #include "ui/liferea_shell.h"
34 #include "ui/feed_list_view.h"
35 
36 static GHashTable	*flIterHash = NULL;	/**< hash table used for fast node id <-> tree iter lookup */
37 static GtkWidget	*nodenamedialog = NULL;
38 
39 GtkTreeIter *
feed_list_node_to_iter(const gchar * nodeId)40 feed_list_node_to_iter (const gchar *nodeId)
41 {
42 	if (!flIterHash)
43 		return NULL;
44 
45 	return (GtkTreeIter *)g_hash_table_lookup (flIterHash, (gpointer)nodeId);
46 }
47 
48 void
feed_list_node_update_iter(const gchar * nodeId,GtkTreeIter * iter)49 feed_list_node_update_iter (const gchar *nodeId, GtkTreeIter *iter)
50 {
51 	GtkTreeIter *old;
52 
53 	if (!flIterHash)
54 		return;
55 
56 	old = (GtkTreeIter *)g_hash_table_lookup (flIterHash, (gpointer)nodeId);
57 	if (old)
58 		*old = *iter;
59 }
60 
61 static void
feed_list_node_add_iter(const gchar * nodeId,GtkTreeIter * iter)62 feed_list_node_add_iter (const gchar *nodeId, GtkTreeIter *iter)
63 {
64 	if (!flIterHash)
65 		flIterHash = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_free);
66 
67 	g_hash_table_insert (flIterHash, (gpointer)nodeId, (gpointer)iter);
68 }
69 
70 /* Expansion & Collapsing */
71 
72 gboolean
feed_list_node_is_expanded(const gchar * nodeId)73 feed_list_node_is_expanded (const gchar *nodeId)
74 {
75 	GtkTreeIter	*iter;
76 	gboolean 	expanded = FALSE;
77 
78 	if (feedlist_reduced_unread)
79 		return FALSE;
80 
81 	iter = feed_list_node_to_iter (nodeId);
82 	if (iter) {
83 		GtkTreeView *treeview = GTK_TREE_VIEW (liferea_shell_lookup ("feedlist"));
84 		GtkTreePath *path = gtk_tree_model_get_path (gtk_tree_view_get_model (treeview), iter);
85 		expanded = gtk_tree_view_row_expanded (treeview, path);
86 		gtk_tree_path_free (path);
87 	}
88 
89 	return expanded;
90 }
91 
92 void
feed_list_node_set_expansion(nodePtr folder,gboolean expanded)93 feed_list_node_set_expansion (nodePtr folder, gboolean expanded)
94 {
95 	GtkTreeIter		*iter;
96 	GtkTreePath		*path;
97 	GtkTreeView		*treeview;
98 
99 	if (feedlist_reduced_unread)
100 		return;
101 
102 	iter = feed_list_node_to_iter (folder->id);
103 	if (!iter)
104 		return;
105 
106 	treeview = GTK_TREE_VIEW (liferea_shell_lookup ("feedlist"));
107 	path = gtk_tree_model_get_path (gtk_tree_view_get_model (treeview), iter);
108 	if (expanded)
109 		gtk_tree_view_expand_row (treeview, path, FALSE);
110 	else
111 		gtk_tree_view_collapse_row (treeview, path);
112 	gtk_tree_path_free (path);
113 }
114 
115 /* Folder expansion workaround using "empty" nodes */
116 
117 void
feed_list_node_add_empty_node(GtkTreeIter * parent)118 feed_list_node_add_empty_node (GtkTreeIter *parent)
119 {
120 	GtkTreeIter	iter;
121 
122 	gtk_tree_store_append (feedstore, &iter, parent);
123 	gtk_tree_store_set (feedstore, &iter,
124 	                    FS_LABEL, _("(Empty)"),
125 	                    FS_PTR, NULL,
126 	                    FS_UNREAD, 0,
127 			    FS_COUNT, "",
128 	                    -1);
129 }
130 
131 void
feed_list_node_remove_empty_node(GtkTreeIter * parent)132 feed_list_node_remove_empty_node (GtkTreeIter *parent)
133 {
134 	GtkTreeIter	iter;
135 	nodePtr		node;
136 	gboolean	valid;
137 
138 	gtk_tree_model_iter_children (GTK_TREE_MODEL (feedstore), &iter, parent);
139 	do {
140 		gtk_tree_model_get (GTK_TREE_MODEL (feedstore), &iter, FS_PTR, &node, -1);
141 
142 		if (!node) {
143 			gtk_tree_store_remove (feedstore, &iter);
144 			return;
145 		}
146 
147 		valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (feedstore), &iter);
148 	} while (valid);
149 }
150 
151 /* this function is a workaround to the cant-drop-rows-into-emtpy-
152    folders-problem, so we simply pack an "(empty)" entry into each
153    empty folder like Nautilus does... */
154 
155 static void
feed_list_node_check_if_folder_is_empty(const gchar * nodeId)156 feed_list_node_check_if_folder_is_empty (const gchar *nodeId)
157 {
158 	GtkTreeIter	*iter;
159 	int		count;
160 
161 	debug1 (DEBUG_GUI, "folder empty check for node id \"%s\"", nodeId);
162 
163 	/* this function does two things:
164 
165 	1. add "(empty)" entry to an empty folder
166 	2. remove an "(empty)" entry from a non empty folder
167 	(this state is possible after a drag&drop action) */
168 
169 	iter = feed_list_node_to_iter (nodeId);
170 	if (!iter)
171 		return;
172 
173 	count = gtk_tree_model_iter_n_children (GTK_TREE_MODEL (feedstore), iter);
174 
175 	/* case 1 */
176 	if (0 == count) {
177 		feed_list_node_add_empty_node (iter);
178 		return;
179 	}
180 
181 	if (1 == count)
182 		return;
183 
184 	/* else we could have case 2 */
185 	feed_list_node_remove_empty_node (iter);
186 }
187 
188 void
feed_list_node_add(nodePtr node)189 feed_list_node_add (nodePtr node)
190 {
191 	gint		position;
192 	GtkTreeIter	*iter, *parentIter = NULL;
193 
194 	debug2 (DEBUG_GUI, "adding node \"%s\" as child of parent=\"%s\"", node_get_title(node), (NULL != node->parent)?node_get_title(node->parent):"feed list root");
195 
196 	g_assert (NULL != node->parent);
197 	g_assert (NULL == feed_list_node_to_iter (node->id));
198 
199 	/* if parent is NULL we have the root folder and don't create a new row! */
200 	iter = g_new0 (GtkTreeIter, 1);
201 
202 	/* if reduced feedlist, show flat treeview */
203 	if (feedlist_reduced_unread)
204 		parentIter = NULL;
205 	else if (node->parent != feedlist_get_root ())
206 		parentIter = feed_list_node_to_iter (node->parent->id);
207 
208 	position = g_slist_index (node->parent->children, node);
209 
210 	if (feedlist_reduced_unread || position < 0)
211 		gtk_tree_store_append (feedstore, iter, parentIter);
212 	else
213 		gtk_tree_store_insert (feedstore, iter, parentIter, position);
214 
215 	gtk_tree_store_set (feedstore, iter, FS_PTR, node, -1);
216 	feed_list_node_add_iter (node->id, iter);
217 	feed_list_node_update (node->id);
218 
219 	if (node->parent != feedlist_get_root ())
220 		feed_list_node_check_if_folder_is_empty (node->parent->id);
221 
222 	if (IS_FOLDER (node))
223 		feed_list_node_check_if_folder_is_empty (node->id);
224 }
225 
226 static void
feed_list_node_load_feedlist(nodePtr node)227 feed_list_node_load_feedlist (nodePtr node)
228 {
229 	GSList		*iter;
230 
231 	iter = node->children;
232 	while (iter) {
233 		node = (nodePtr)iter->data;
234 		feed_list_node_add (node);
235 
236 		if (IS_FOLDER (node) || IS_NODE_SOURCE (node))
237 			feed_list_node_load_feedlist (node);
238 
239 		iter = g_slist_next(iter);
240 	}
241 }
242 
243 static void
feed_list_node_clear_feedlist()244 feed_list_node_clear_feedlist ()
245 {
246 	gtk_tree_store_clear (feedstore);
247 	g_hash_table_remove_all (flIterHash);
248 }
249 
250 void
feed_list_node_reload_feedlist()251 feed_list_node_reload_feedlist ()
252 {
253 	feed_list_node_clear_feedlist ();
254 	feed_list_node_load_feedlist (feedlist_get_root ());
255 }
256 
257 void
feed_list_node_remove_node(nodePtr node)258 feed_list_node_remove_node (nodePtr node)
259 {
260 	GtkTreeIter	*iter;
261 	gboolean 	parentExpanded = FALSE;
262 
263 	iter = feed_list_node_to_iter (node->id);
264 	if (!iter)
265 		return;	/* must be tolerant because of DnD handling */
266 
267 	if (node->parent)
268 		parentExpanded = feed_list_node_is_expanded (node->parent->id); /* If the folder becomes empty, the folder would collapse */
269 
270 	gtk_tree_store_remove (feedstore, iter);
271 	g_hash_table_remove (flIterHash, node->id);
272 
273 	if (node->parent) {
274 		feed_list_node_check_if_folder_is_empty (node->parent->id);
275 		if (parentExpanded)
276 			feed_list_node_set_expansion (node->parent, TRUE);
277 
278 		feed_list_node_update (node->parent->id);
279 	}
280 }
281 
282 void
feed_list_node_update(const gchar * nodeId)283 feed_list_node_update (const gchar *nodeId)
284 {
285 	GtkTreeIter	*iter;
286 	gchar		*label, *count = NULL;
287 	guint		labeltype;
288 	nodePtr		node;
289 
290 	static gchar	*countColor = NULL;
291 
292 	node = node_from_id (nodeId);
293 	iter = feed_list_node_to_iter (nodeId);
294 	if (!iter)
295 		return;
296 
297 	/* Initialize unread item color Pango CSS */
298 	if (!countColor) {
299 		const gchar *bg = NULL, *fg = NULL;
300 
301 		bg = render_get_theme_color ("FEEDLIST_UNREAD_BG");
302 		fg = render_get_theme_color ("FEEDLIST_UNREAD_FG");
303 		if (fg && bg) {
304 			countColor = g_strdup_printf ("foreground='#%s' background='#%s'", fg, bg);
305 			debug1 (DEBUG_HTML, "Feed list unread CSS: %s\n", countColor);
306 		}
307 	}
308 
309 	labeltype = NODE_TYPE (node)->capabilities;
310 	labeltype &= (NODE_CAPABILITY_SHOW_UNREAD_COUNT |
311         	      NODE_CAPABILITY_SHOW_ITEM_COUNT);
312 
313 	if (node->unreadCount == 0 && (labeltype & NODE_CAPABILITY_SHOW_UNREAD_COUNT))
314 		labeltype &= ~NODE_CAPABILITY_SHOW_UNREAD_COUNT;
315 
316 	label = g_markup_escape_text (node_get_title (node), -1);
317 	switch (labeltype) {
318 		case NODE_CAPABILITY_SHOW_UNREAD_COUNT |
319 		     NODE_CAPABILITY_SHOW_ITEM_COUNT:
320 	     		/* treat like show unread count */
321 		case NODE_CAPABILITY_SHOW_UNREAD_COUNT:
322 			count = g_strdup_printf ("<span weight='bold' %s> %u </span>", countColor?countColor:"", node->unreadCount);
323 			break;
324 		case NODE_CAPABILITY_SHOW_ITEM_COUNT:
325 			count = g_strdup_printf ("<span weight='bold' %s> %u </span>", countColor?countColor:"", node->itemCount);
326 		     	break;
327 		default:
328 			break;
329 	}
330 
331 	/* Extra message for search folder rebuilds */
332 	if (IS_VFOLDER (node) && node->data) {
333 		if (((vfolderPtr)node->data)->reloading) {
334 			gchar *tmp = label;
335 			label = g_strdup_printf (_("%s\n<i>Rebuilding</i>"), label);
336 			g_free (tmp);
337 		}
338 	}
339 
340 	gtk_tree_store_set (feedstore, iter,
341 	                    FS_LABEL, label,
342 	                    FS_UNREAD, node->unreadCount,
343 	                    FS_ICON, node->available?node_get_icon (node):icon_get (ICON_UNAVAILABLE),
344 	                    FS_COUNT, count,
345 	                    -1);
346 	g_free (label);
347 	g_free (count);
348 
349 	if (node->parent)
350 		feed_list_node_update (node->parent->id);
351 }
352 
353 /* node renaming dialog */
354 
355 static void
on_nodenamedialog_response(GtkDialog * dialog,gint response_id,gpointer user_data)356 on_nodenamedialog_response (GtkDialog *dialog, gint response_id, gpointer user_data)
357 {
358 	nodePtr	node = (nodePtr)user_data;
359 
360 	if (response_id == GTK_RESPONSE_OK) {
361 		node_set_title (node, (gchar *) gtk_entry_get_text (GTK_ENTRY (liferea_dialog_lookup (GTK_WIDGET (dialog), "nameentry"))));
362 
363 		feed_list_node_update (node->id);
364 		feedlist_schedule_save ();
365 	}
366 
367 	gtk_widget_destroy (GTK_WIDGET (dialog));
368 	nodenamedialog = NULL;
369 }
370 
371 void
feed_list_node_rename(nodePtr node)372 feed_list_node_rename (nodePtr node)
373 {
374 	GtkWidget	*nameentry;
375 
376 	if (!nodenamedialog || !G_IS_OBJECT (nodenamedialog))
377 		nodenamedialog = liferea_dialog_new ("rename_node");
378 
379 	nameentry = liferea_dialog_lookup (nodenamedialog, "nameentry");
380 	gtk_entry_set_text (GTK_ENTRY (nameentry), node_get_title (node));
381 	g_signal_connect (G_OBJECT (nodenamedialog), "response",
382 	                  G_CALLBACK (on_nodenamedialog_response), node);
383 	gtk_widget_show (nodenamedialog);
384 }
385 
386 /* node deletion dialog */
387 
388 static void
feed_list_node_remove_cb(GtkDialog * dialog,gint response_id,gpointer user_data)389 feed_list_node_remove_cb (GtkDialog *dialog, gint response_id, gpointer user_data)
390 {
391 	if (GTK_RESPONSE_ACCEPT == response_id)
392 		feedlist_remove_node ((nodePtr)user_data);
393 
394 	gtk_widget_destroy (GTK_WIDGET (dialog));
395 }
396 
397 void
feed_list_node_remove(nodePtr node)398 feed_list_node_remove (nodePtr node)
399 {
400 	GtkWidget	*dialog;
401 	GtkWindow	*mainwindow;
402 	gchar		*text;
403 
404 	g_assert (node == feedlist_get_selected ());
405 
406 	liferea_shell_set_status_bar ("%s \"%s\"", _("Deleting entry"), node_get_title (node));
407 	text = g_strdup_printf (IS_FOLDER (node)?_("Are you sure that you want to delete \"%s\" and its contents?"):_("Are you sure that you want to delete \"%s\"?"), node_get_title (node));
408 
409 	mainwindow = GTK_WINDOW (liferea_shell_get_window ());
410 	dialog = gtk_message_dialog_new (mainwindow,
411 	                                 GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
412 	                                 GTK_MESSAGE_QUESTION,
413 	                                 GTK_BUTTONS_NONE,
414 	                                 "%s", text);
415 	gtk_dialog_add_buttons (GTK_DIALOG (dialog),
416 	                        _("_Cancel"), GTK_RESPONSE_CANCEL,
417 	                        _("_Delete"), GTK_RESPONSE_ACCEPT,
418 	                        NULL);
419 	gtk_window_set_title (GTK_WINDOW (dialog), _("Deletion Confirmation"));
420 	gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
421 	gtk_window_set_transient_for (GTK_WINDOW (dialog), mainwindow);
422 
423 	g_free (text);
424 
425 	gtk_widget_show_all (dialog);
426 
427 	g_signal_connect (G_OBJECT (dialog), "response",
428 	                  G_CALLBACK (feed_list_node_remove_cb), node);
429 }
430 
431 static void
feed_list_node_add_duplicate_url_cb(GtkDialog * dialog,gint response_id,gpointer user_data)432 feed_list_node_add_duplicate_url_cb (GtkDialog *dialog, gint response_id, gpointer user_data)
433 {
434 	subscriptionPtr tempSubscription = (subscriptionPtr) user_data;
435 
436 	if (GTK_RESPONSE_ACCEPT == response_id) {
437 		feedlist_add_subscription (
438 				subscription_get_source (tempSubscription),
439 				subscription_get_filter (tempSubscription),
440 				update_options_copy (tempSubscription->updateOptions),
441 				FEED_REQ_PRIORITY_HIGH
442 		);
443 	}
444 
445 	subscription_free (tempSubscription);
446 
447 	gtk_widget_destroy (GTK_WIDGET (dialog));
448 }
449 
450 void
feed_list_node_add_duplicate_url_subscription(subscriptionPtr tempSubscription,nodePtr exNode)451 feed_list_node_add_duplicate_url_subscription (subscriptionPtr tempSubscription, nodePtr exNode)
452 {
453 	GtkWidget	*dialog;
454 	GtkWindow	*mainwindow;
455 	gchar		*text;
456 
457 	text = g_strdup_printf (
458 			_("Are you sure that you want to add a new subscription with URL \"%s\"? Another subscription with the same URL already exists (\"%s\")."),
459 			tempSubscription->source,
460 			node_get_title (exNode)
461 	);
462 
463 	mainwindow = GTK_WINDOW (liferea_shell_get_window ());
464 	dialog = gtk_message_dialog_new (mainwindow,
465 									 GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
466 									 GTK_MESSAGE_QUESTION,
467 									 GTK_BUTTONS_NONE,
468 									 "%s", text);
469 	gtk_dialog_add_buttons (GTK_DIALOG (dialog),
470 							_("_Cancel"), GTK_RESPONSE_CANCEL,
471 							_("_Add"), GTK_RESPONSE_ACCEPT,
472 							NULL);
473 	gtk_window_set_title (GTK_WINDOW (dialog), _("Adding Duplicate Subscription Confirmation"));
474 	gtk_window_set_transient_for (GTK_WINDOW (dialog), mainwindow);
475 
476 	g_free (text);
477 
478 	gtk_widget_show_all (dialog);
479 
480 	g_signal_connect (G_OBJECT (dialog), "response",
481 					  G_CALLBACK (feed_list_node_add_duplicate_url_cb), tempSubscription);
482 }
483