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