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