1 /**
2  * @file ui_dnd.c everything concerning Drag&Drop
3  *
4  * Copyright (C) 2003-2012 Lars Windolf <lars.windolf@gmx.de>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  */
20 
21 #include <string.h>		/* For strcmp */
22 #include "common.h"
23 #include "db.h"
24 #include "feed.h"
25 #include "feedlist.h"
26 #include "folder.h"
27 #include "debug.h"
28 #include "ui/item_list_view.h"
29 #include "ui/feed_list_view.h"
30 #include "ui/feed_list_node.h"
31 #include "ui/liferea_shell.h"
32 #include "ui/ui_dnd.h"
33 #include "fl_sources/node_source.h"
34 
35 /*
36     Why does Liferea need such a complex DnD handling (for the feed list)?
37 
38      -> Because parts of the feed list might be un-draggable.
39      -> Because drag source and target might be different node sources
40 	with even incompatible subscription types.
41      -> Because removal at drag source and insertion at drop target
42         must be atomic to avoid subscription losses.
43 
44     For simplicity the DnD code reuses the UI node removal and insertion
45     methods that asynchronously apply the actions at the node source.
46 
47     (FIXME: implement the last part)
48  */
49 
50 static gboolean (*old_feed_drop_possible)(GtkTreeDragDest   *drag_dest,
51                                           GtkTreePath       *dest_path,
52                                           GtkSelectionData  *selection_data);
53 
54 static gboolean (*old_feed_drag_data_received)(GtkTreeDragDest *drag_dest,
55                                                GtkTreePath *dest,
56                                                GtkSelectionData *selection_data);
57 
58 /* GtkTreeDragSource/GtkTreeDragDest implementation				*/
59 
60 /** decides whether a feed cannot be dragged or not */
61 static gboolean
ui_dnd_feed_draggable(GtkTreeDragSource * drag_source,GtkTreePath * path)62 ui_dnd_feed_draggable (GtkTreeDragSource *drag_source, GtkTreePath *path)
63 {
64 	GtkTreeIter	iter;
65 	nodePtr		node;
66 
67 	debug1 (DEBUG_GUI, "DnD check if feed dragging is possible (%d)", path);
68 
69 	if (gtk_tree_model_get_iter (GTK_TREE_MODEL (drag_source), &iter, path)) {
70 		gtk_tree_model_get (GTK_TREE_MODEL (drag_source), &iter, FS_PTR, &node, -1);
71 
72 		/* never drag "empty" entries or nodes of read-only subscription lists*/
73 		if (!node || !(NODE_SOURCE_TYPE (node->parent)->capabilities & NODE_SOURCE_CAPABILITY_WRITABLE_FEEDLIST))
74 			return FALSE;
75 
76 		return TRUE;
77 	} else {
78 		g_warning ("fatal error! could not resolve tree path!");
79 		return FALSE;
80 	}
81 }
82 
83 static gboolean
ui_dnd_feed_drop_possible(GtkTreeDragDest * drag_dest,GtkTreePath * dest_path,GtkSelectionData * selection_data)84 ui_dnd_feed_drop_possible (GtkTreeDragDest *drag_dest, GtkTreePath *dest_path, GtkSelectionData *selection_data)
85 {
86 	GtkTreeModel	*model = NULL;
87 	GtkTreePath	*src_path = NULL;
88 	GtkTreeIter	iter;
89 	nodePtr		sourceNode, targetNode;
90 
91 	debug1 (DEBUG_GUI, "DnD check if feed dropping is possible (%d)", dest_path);
92 
93 	if (!(old_feed_drop_possible) (drag_dest, dest_path, selection_data))
94 		return FALSE;
95 
96 	if (!gtk_tree_model_get_iter (GTK_TREE_MODEL (drag_dest), &iter, dest_path))
97 		return FALSE;
98 
99 	/* Try to get an iterator, if we get none it means either feed list
100 	   root or an "Empty" node. Both cases are fine */
101 	gtk_tree_model_get (GTK_TREE_MODEL (drag_dest), &iter, FS_PTR, &targetNode, -1);
102 	if (!targetNode)
103 		return TRUE;
104 
105 	/* If we got an iterator it's either a possible dropping
106 	   candidate (a folder or source node to drop into, or a
107 	   iterator to insert after). In any case we have to check
108 	   if it is a writeable node source. */
109 
110 	/* Never drop into read-only subscription node sources */
111 	if (!(NODE_SOURCE_TYPE (targetNode)->capabilities & NODE_SOURCE_CAPABILITY_WRITABLE_FEEDLIST))
112 		return FALSE;
113 
114 	/* never drag folders into non-hierarchic node sources */
115 	if (!gtk_tree_get_row_drag_data (selection_data, &model, &src_path))
116 		return TRUE;
117 
118 	if (gtk_tree_model_get_iter (GTK_TREE_MODEL (model), &iter, src_path)) {
119 		gtk_tree_model_get (GTK_TREE_MODEL (model), &iter, FS_PTR, &sourceNode, -1);
120 
121 		g_assert (sourceNode);
122 
123 		/* Never drop into another node source as this arises to many problems
124 		   (e.g. remote sync, different subscription type, e.g. SF #2855990) */
125 		if (NODE_SOURCE_TYPE (targetNode) != NODE_SOURCE_TYPE (sourceNode))
126 			return FALSE;
127 
128 		if (IS_FOLDER(sourceNode) && !(NODE_SOURCE_TYPE (targetNode)->capabilities & NODE_SOURCE_CAPABILITY_HIERARCHIC_FEEDLIST))
129 			return FALSE;
130 	}
131 
132 	gtk_tree_path_free (src_path);
133 
134 	return TRUE;
135 }
136 
137 static gboolean
ui_dnd_feed_drag_data_received(GtkTreeDragDest * drag_dest,GtkTreePath * dest,GtkSelectionData * selection_data)138 ui_dnd_feed_drag_data_received (GtkTreeDragDest *drag_dest, GtkTreePath *dest, GtkSelectionData *selection_data)
139 {
140 	GtkTreeIter	iter, iter2, parentIter;
141 	nodePtr		node, oldParent, newParent;
142 	gboolean	result, valid, added;
143 	gint		oldPos, pos;
144 
145 	result = old_feed_drag_data_received (drag_dest, dest, selection_data);
146 	if (result) {
147 		if (gtk_tree_model_get_iter (GTK_TREE_MODEL (drag_dest), &iter, dest)) {
148 			gtk_tree_model_get (GTK_TREE_MODEL (drag_dest), &iter, FS_PTR, &node, -1);
149 
150 			/* If we don't do anything, then because DnD is implemented by removal and
151 			   re-insertion, and the removed node is selected, the treeview selects
152 			   the next row after the removal, which is supremely irritating.
153 			   But setting a selection at this point is pointless, because the treeview
154 			   will reset it as soon as the DnD callback returns. Instead, we set
155 			   the cursor, which controls where treeview resets the selection later.
156 			 */
157 			gtk_tree_view_set_cursor(GTK_TREE_VIEW (liferea_shell_lookup ("feedlist")),
158 			    dest, NULL, FALSE);
159 
160 			/* remove from old parents child list */
161 			oldParent = node->parent;
162 			g_assert (oldParent);
163 			oldPos = g_slist_index (oldParent->children, node);
164 			oldParent->children = g_slist_remove (oldParent->children, node);
165 			node_update_counters (oldParent);
166 
167 			if (0 == g_slist_length (oldParent->children))
168 				feed_list_node_add_empty_node (feed_list_node_to_iter (oldParent->id));
169 
170 			/* and rebuild new parents child list */
171 			if (gtk_tree_model_iter_parent (GTK_TREE_MODEL (drag_dest), &parentIter, &iter)) {
172 				gtk_tree_model_get (GTK_TREE_MODEL (drag_dest), &parentIter, FS_PTR, &newParent, -1);
173 			} else {
174 				gtk_tree_model_get_iter_first (GTK_TREE_MODEL (drag_dest), &parentIter);
175 				newParent = feedlist_get_root ();
176 			}
177 
178 			/* drop old list... */
179 			debug3 (DEBUG_GUI, "old parent is %s (%d, position=%d)", oldParent->title, g_slist_length (oldParent->children), oldPos);
180 			debug2 (DEBUG_GUI, "new parent is %s (%d)", newParent->title, g_slist_length (newParent->children));
181 			g_slist_free (newParent->children);
182 			newParent->children = NULL;
183 			node->parent = newParent;
184 
185 			debug0 (DEBUG_GUI, "new newParent child list:");
186 
187 			/* and rebuild it from the tree model */
188 			if (feedlist_get_root() != newParent)
189 				valid = gtk_tree_model_iter_children (GTK_TREE_MODEL (drag_dest), &iter2, &parentIter);
190 			else
191 				valid = gtk_tree_model_iter_children (GTK_TREE_MODEL (drag_dest), &iter2, NULL);
192 
193 			pos = 0;
194 			added = FALSE;
195 			while (valid) {
196 				nodePtr	child;
197 				gtk_tree_model_get (GTK_TREE_MODEL (drag_dest), &iter2, FS_PTR, &child, -1);
198 				if (child) {
199 					/* Well this is a bit complicated... If we move a feed inside a folder
200 					   we need to skip the old insertion point (oldPos). This is easy if the
201 					   feed is added behind this position. If it is dropped before the flag
202 					   added is set once the new copy is encountered. The remaining copy
203 					   is skipped automatically when the flag is set.
204 					 */
205 
206 					/* check if this is a copy of the dragged node or the original itself */
207 					if ((newParent == oldParent) && !strcmp(node->id, child->id)) {
208 						if ((pos == oldPos) || added) {
209 							/* it is the original */
210 							debug2 (DEBUG_GUI, "   -> %d: skipping old insertion point %s", pos, child->title);
211 						} else {
212 							/* it is a copy inserted before the original */
213 							added = TRUE;
214 							debug2 (DEBUG_GUI, "   -> %d: new insertion point of %s", pos, child->title);
215 							newParent->children = g_slist_append (newParent->children, child);
216 						}
217 					} else {
218 						/* all other nodes */
219 						debug2 (DEBUG_GUI, "   -> %d: adding %s", pos, child->title);
220 						newParent->children = g_slist_append (newParent->children, child);
221 					}
222 				} else {
223 					debug0 (DEBUG_GUI, "   -> removing empty node");
224 					/* remove possible existing "(empty)" node from newParent */
225 					feed_list_node_remove_empty_node (&parentIter);
226 				}
227 				valid = gtk_tree_model_iter_next (GTK_TREE_MODEL (drag_dest), &iter2);
228 				pos++;
229 			}
230 
231 			db_node_update (node);
232 			node_update_counters (newParent);
233 
234 			feedlist_schedule_save ();
235 		}
236 	}
237 
238 	return result;
239 }
240 
241 void
ui_dnd_setup_feedlist(GtkTreeStore * feedstore)242 ui_dnd_setup_feedlist (GtkTreeStore *feedstore)
243 {
244 	GtkTreeDragSourceIface	*drag_source_iface;
245 	GtkTreeDragDestIface	*drag_dest_iface;
246 
247 	drag_source_iface = GTK_TREE_DRAG_SOURCE_GET_IFACE (GTK_TREE_MODEL (feedstore));
248 	drag_source_iface->row_draggable = ui_dnd_feed_draggable;
249 
250 	drag_dest_iface = GTK_TREE_DRAG_DEST_GET_IFACE (GTK_TREE_MODEL (feedstore));
251 	old_feed_drop_possible = drag_dest_iface->row_drop_possible;
252 	old_feed_drag_data_received = drag_dest_iface->drag_data_received;
253 	drag_dest_iface->row_drop_possible = ui_dnd_feed_drop_possible;
254 	drag_dest_iface->drag_data_received = ui_dnd_feed_drag_data_received;
255 }
256