chiark / gitweb /
Merge branch 'master' of git.distorted.org.uk:~mdw/publish/public-git/disorder
[disorder] / disobedience / multidrag.c
CommitLineData
6a7eb118 1/*
6a7eb118
RK
2 * Copyright (C) 2009 Richard Kettlewell
3 *
75fd36e9
RK
4 * Note that this license ONLY applies to multidrag.c and multidrag.h, not to
5 * the rest of DisOrder.
6a7eb118 6 *
75fd36e9
RK
7 * Permission is hereby granted, free of charge, to any person
8 * obtaining a copy of this software and associated documentation files
9 * (the "Software"), to deal in the Software without restriction,
10 * including without limitation the rights to use, copy, modify, merge,
11 * publish, distribute, sublicense, and/or sell copies of the Software,
12 * and to permit persons to whom the Software is furnished to do so,
13 * subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be
16 * included in all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21 * NONINFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE
22 * FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
23 * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
6a7eb118
RK
25 */
26/** @file disobedience/multidrag.c
27 * @brief Drag multiple rows of a GtkTreeView
30b358a3
RK
28 *
29 * Normally when you start a drag, GtkTreeView sets the selection to just row
30 * you dragged from (because it can't cope with dragging more than one row at a
31 * time).
32 *
846b01c2
RK
33 * Disobedience needs more.
34 *
35 * Firstly it intercepts button-press-event and button-release event and for
36 * clicks that might be the start of drags, suppresses changes to the
37 * selection. A consequence of this is that it needs to intercept
38 * button-release-event too, to restore the effect of the click, if it turns
39 * out not to be drag after all.
30b358a3
RK
40 *
41 * The location of the initial click is stored in object data called @c
42 * multidrag-where.
43 *
846b01c2
RK
44 * Secondly it intercepts drag-begin and constructs an icon from the rows to be
45 * dragged.
46 *
30b358a3
RK
47 * Inspired by similar code in <a
48 * href="http://code.google.com/p/quodlibet/">Quodlibet</a> (another software
49 * jukebox, albeit as far as I can see a single-user one).
6a7eb118 50 */
3ae36d41 51#include <config.h>
846b01c2
RK
52
53#include <string.h>
3ae36d41
RK
54#include <glib.h>
55#include <gtk/gtk.h>
56
57#include "multidrag.h"
6a7eb118
RK
58
59static gboolean multidrag_selection_block(GtkTreeSelection attribute((unused)) *selection,
60 GtkTreeModel attribute((unused)) *model,
61 GtkTreePath attribute((unused)) *path,
62 gboolean attribute((unused)) path_currently_selected,
63 gpointer data) {
30b358a3 64 return *(const gboolean *)data;
6a7eb118
RK
65}
66
67static void block_selection(GtkWidget *w, gboolean block,
68 int x, int y) {
30b358a3 69 static const gboolean which[] = { FALSE, TRUE };
6a7eb118
RK
70 GtkTreeSelection *s = gtk_tree_view_get_selection(GTK_TREE_VIEW(w));
71 gtk_tree_selection_set_select_function(s,
72 multidrag_selection_block,
30b358a3 73 (gboolean *)&which[!!block],
6a7eb118
RK
74 NULL);
75 // Remember the pointer location
76 int *where = g_object_get_data(G_OBJECT(w), "multidrag-where");
77 if(!where) {
78 where = g_malloc(2 * sizeof (int));
42eabfd7
RK
79 g_object_set_data_full(G_OBJECT(w), "multidrag-where", where,
80 g_free);
6a7eb118
RK
81 }
82 where[0] = x;
83 where[1] = y;
84}
85
86static gboolean multidrag_button_press_event(GtkWidget *w,
87 GdkEventButton *event,
88 gpointer attribute((unused)) user_data) {
89 /* By default we assume that anything this button press does should
90 * act as normal */
91 block_selection(w, TRUE, -1, -1);
92 /* We are only interested in left-button behavior */
93 if(event->button != 1)
94 return FALSE;
ef566eb8
RK
95 /* We are only uninterested in clicks without CTRL or SHIFT. GTK ignores the
96 * other possible modifiers, so we do too. */
97 if(event->state & (GDK_SHIFT_MASK|GDK_CONTROL_MASK))
6a7eb118
RK
98 return FALSE;
99 /* We are only interested if a well-defined path is clicked */
42eabfd7 100 GtkTreePath *path = NULL;
6a7eb118
RK
101 if(!gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(w),
102 event->x, event->y,
103 &path,
104 NULL,
105 NULL, NULL))
106 return FALSE;
6a7eb118
RK
107 /* We are only interested if a selected row is clicked */
108 GtkTreeSelection *s = gtk_tree_view_get_selection(GTK_TREE_VIEW(w));
42eabfd7
RK
109 if(gtk_tree_selection_path_is_selected(s, path)) {
110 /* We block subsequent selection changes and remember where the
111 * click was */
112 block_selection(w, FALSE, event->x, event->y);
113 }
114 if(path)
115 gtk_tree_path_free(path);
6a7eb118
RK
116 return FALSE; /* propagate */
117}
118
119static gboolean multidrag_button_release_event(GtkWidget *w,
120 GdkEventButton *event,
121 gpointer attribute((unused)) user_data) {
122 int *where = g_object_get_data(G_OBJECT(w), "multidrag-where");
123
30b358a3
RK
124 /* Did button-press-event do anything? We just check the outcome rather than
125 * going through all the conditions it tests. */
6a7eb118
RK
126 if(where && where[0] != -1) {
127 // Remember where the down-click was
128 const int x = where[0], y = where[1];
129 // Re-allow selections
130 block_selection(w, TRUE, -1, -1);
131 if(x == event->x && y == event->y) {
132 // If the up-click is at the same location as the down-click,
133 // it's not a drag.
42eabfd7 134 GtkTreePath *path = NULL;
6a7eb118
RK
135 GtkTreeViewColumn *col;
136 if(gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(w),
137 event->x, event->y,
138 &path,
139 &col,
140 NULL, NULL)) {
141 gtk_tree_view_set_cursor(GTK_TREE_VIEW(w), path, col, FALSE);
142 }
42eabfd7
RK
143 if(path)
144 gtk_tree_path_free(path);
6a7eb118
RK
145 }
146 }
147 return FALSE; /* propagate */
148}
149
846b01c2
RK
150/** @brief State for multidrag_begin() and its callbacks */
151struct multidrag_begin_state {
152 GtkTreeView *view;
ff18efce 153 multidrag_row_predicate *predicate;
846b01c2
RK
154 int rows;
155 int index;
156 GdkPixmap **pixmaps;
157};
158
159/** @brief Callback to construct a row pixmap */
160static void multidrag_make_row_pixmaps(GtkTreeModel attribute((unused)) *model,
161 GtkTreePath *path,
ff18efce 162 GtkTreeIter *iter,
846b01c2
RK
163 gpointer data) {
164 struct multidrag_begin_state *qdbs = data;
165
ff18efce
RK
166 if(qdbs->predicate(path, iter)) {
167 qdbs->pixmaps[qdbs->index++]
168 = gtk_tree_view_create_row_drag_icon(qdbs->view, path);
169 }
846b01c2
RK
170}
171
172/** @brief Called when a drag operation starts
173 * @param w Source widget (the tree view)
174 * @param dc Drag context
ff18efce 175 * @param user_data Row predicate
846b01c2
RK
176 */
177static void multidrag_drag_begin(GtkWidget *w,
178 GdkDragContext attribute((unused)) *dc,
ff18efce 179 gpointer user_data) {
846b01c2
RK
180 struct multidrag_begin_state qdbs[1];
181 GdkPixmap *icon;
182 GtkTreeSelection *sel;
183
184 //fprintf(stderr, "drag-begin\n");
185 memset(qdbs, 0, sizeof *qdbs);
186 qdbs->view = GTK_TREE_VIEW(w);
ff18efce 187 qdbs->predicate = (multidrag_row_predicate *)user_data;
846b01c2
RK
188 sel = gtk_tree_view_get_selection(qdbs->view);
189 /* Find out how many rows there are */
190 if(!(qdbs->rows = gtk_tree_selection_count_selected_rows(sel)))
191 return; /* doesn't make sense */
192 /* Generate a pixmap for each row */
193 qdbs->pixmaps = g_new(GdkPixmap *, qdbs->rows);
194 gtk_tree_selection_selected_foreach(sel,
195 multidrag_make_row_pixmaps,
196 qdbs);
ff18efce
RK
197 /* Might not have used all rows */
198 qdbs->rows = qdbs->index;
846b01c2
RK
199 /* Determine the size of the final icon */
200 int height = 0, width = 0;
201 for(int n = 0; n < qdbs->rows; ++n) {
202 int pxw, pxh;
203 gdk_drawable_get_size(qdbs->pixmaps[n], &pxw, &pxh);
204 if(pxw > width)
205 width = pxw;
206 height += pxh;
207 }
208 if(!width || !height)
209 return; /* doesn't make sense */
210 /* Construct the icon */
211 icon = gdk_pixmap_new(qdbs->pixmaps[0], width, height, -1);
212 GdkGC *gc = gdk_gc_new(icon);
213 gdk_gc_set_colormap(gc, gtk_widget_get_colormap(w));
214 int y = 0;
215 for(int n = 0; n < qdbs->rows; ++n) {
216 int pxw, pxh;
217 gdk_drawable_get_size(qdbs->pixmaps[n], &pxw, &pxh);
218 gdk_draw_drawable(icon,
219 gc,
220 qdbs->pixmaps[n],
221 0, 0, /* source coords */
222 0, y, /* dest coords */
223 pxw, pxh); /* size */
224 y += pxh;
225 gdk_drawable_unref(qdbs->pixmaps[n]);
226 qdbs->pixmaps[n] = NULL;
227 }
228 g_free(qdbs->pixmaps);
229 qdbs->pixmaps = NULL;
230 // TODO scale down a bit, the resulting icons are currently a bit on the
231 // large side.
232 gtk_drag_source_set_icon(w,
233 gtk_widget_get_colormap(w),
234 icon,
235 NULL);
236}
237
ff18efce
RK
238static gboolean multidrag_default_predicate(GtkTreePath attribute((unused)) *path,
239 GtkTreeIter attribute((unused)) *iter) {
240 return TRUE;
241}
242
30b358a3
RK
243/** @brief Allow multi-row drag for @p w
244 * @param w A GtkTreeView widget
ff18efce
RK
245 * @param predicate Function called to test rows for draggability, or NULL
246 *
247 * Suppresses the restriction of selections when a drag is started, and
248 * intercepts drag-begin to construct an icon.
30b358a3 249 *
ff18efce
RK
250 * @p predicate should return TRUE for draggable rows and FALSE otherwise, to
251 * control what goes in the icon. If NULL, equivalent to a function that
252 * always returns TRUE.
30b358a3 253 */
ff18efce
RK
254void make_treeview_multidrag(GtkWidget *w,
255 multidrag_row_predicate *predicate) {
256 if(!predicate)
257 predicate = multidrag_default_predicate;
6a7eb118
RK
258 g_signal_connect(w, "button-press-event",
259 G_CALLBACK(multidrag_button_press_event), NULL);
260 g_signal_connect(w, "button-release-event",
261 G_CALLBACK(multidrag_button_release_event), NULL);
846b01c2 262 g_signal_connect(w, "drag-begin",
ff18efce 263 G_CALLBACK(multidrag_drag_begin), predicate);
6a7eb118
RK
264}
265
266/*
267Local Variables:
268c-basic-offset:2
269comment-column:40
270fill-column:79
271indent-tabs-mode:nil
272End:
273*/