chiark / gitweb /
Make queue rearrangement debug output more readable.
[disorder] / disobedience / multidrag.c
1 /*
2  * Copyright (C) 2009 Richard Kettlewell
3  *
4  * Note that this license ONLY applies to multidrag.c and multidrag.h, not to
5  * the rest of DisOrder.
6  *
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.
25  */
26 /** @file disobedience/multidrag.c
27  * @brief Drag multiple rows of a GtkTreeView
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  *
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.
40  *
41  * The location of the initial click is stored in object data called @c
42  * multidrag-where.
43  *
44  * Secondly it intercepts drag-begin and constructs an icon from the rows to be
45  * dragged.
46  *
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).
50  */
51 #include <config.h>
52
53 #include <string.h>
54 #include <glib.h>
55 #include <gtk/gtk.h>
56
57 #include "multidrag.h"
58
59 static 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) {
64   return *(const gboolean *)data;
65 }
66
67 static void block_selection(GtkWidget *w, gboolean block,
68                             int x, int y) {
69   static const gboolean which[] = { FALSE, TRUE };
70   GtkTreeSelection *s = gtk_tree_view_get_selection(GTK_TREE_VIEW(w));
71   gtk_tree_selection_set_select_function(s,
72                                          multidrag_selection_block,
73                                          (gboolean *)&which[!!block],
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));
79     g_object_set_data_full(G_OBJECT(w), "multidrag-where", where,
80                            g_free);
81   }
82   where[0] = x;
83   where[1] = y;
84 }
85
86 static 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;
95   /* We are only interested in unmodified clicks (not SHIFT etc) */
96   if(event->state & GDK_MODIFIER_MASK)
97     return FALSE;
98   /* We are only interested if a well-defined path is clicked */
99   GtkTreePath *path = NULL;
100   if(!gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(w),
101                                     event->x, event->y,
102                                     &path,
103                                     NULL,
104                                     NULL, NULL))
105     return FALSE;
106   /* We are only interested if a selected row is clicked */
107   GtkTreeSelection *s = gtk_tree_view_get_selection(GTK_TREE_VIEW(w));
108   if(gtk_tree_selection_path_is_selected(s, path)) {
109     /* We block subsequent selection changes and remember where the
110      * click was */
111     block_selection(w, FALSE, event->x, event->y);
112   }
113   if(path)
114     gtk_tree_path_free(path);
115   return FALSE;                 /* propagate */
116 }
117
118 static gboolean multidrag_button_release_event(GtkWidget *w,
119                                                GdkEventButton *event,
120                                                gpointer attribute((unused)) user_data) {
121   int *where = g_object_get_data(G_OBJECT(w), "multidrag-where");
122
123   /* Did button-press-event do anything?  We just check the outcome rather than
124    * going through all the conditions it tests. */
125   if(where && where[0] != -1) {
126     // Remember where the down-click was
127     const int x = where[0], y = where[1];
128     // Re-allow selections
129     block_selection(w, TRUE, -1, -1);
130     if(x == event->x && y == event->y) {
131       // If the up-click is at the same location as the down-click,
132       // it's not a drag.
133       GtkTreePath *path = NULL;
134       GtkTreeViewColumn *col;
135       if(gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(w),
136                                        event->x, event->y,
137                                        &path,
138                                        &col,
139                                        NULL, NULL)) {
140         gtk_tree_view_set_cursor(GTK_TREE_VIEW(w), path, col, FALSE);
141       }
142       if(path)
143         gtk_tree_path_free(path);
144     }
145   }
146   return FALSE;                 /* propagate */
147 }
148
149 /** @brief State for multidrag_begin() and its callbacks */
150 struct multidrag_begin_state {
151   GtkTreeView *view;
152   multidrag_row_predicate *predicate;
153   int rows;
154   int index;
155   GdkPixmap **pixmaps;
156 };
157
158 /** @brief Callback to construct a row pixmap */
159 static void multidrag_make_row_pixmaps(GtkTreeModel attribute((unused)) *model,
160                                        GtkTreePath *path,
161                                        GtkTreeIter *iter,
162                                        gpointer data) {
163   struct multidrag_begin_state *qdbs = data;
164
165   if(qdbs->predicate(path, iter)) {
166     qdbs->pixmaps[qdbs->index++]
167       = gtk_tree_view_create_row_drag_icon(qdbs->view, path);
168   }
169 }
170
171 /** @brief Called when a drag operation starts
172  * @param w Source widget (the tree view)
173  * @param dc Drag context
174  * @param user_data Row predicate
175  */
176 static void multidrag_drag_begin(GtkWidget *w,
177                                  GdkDragContext attribute((unused)) *dc,
178                                  gpointer user_data) {
179   struct multidrag_begin_state qdbs[1];
180   GdkPixmap *icon;
181   GtkTreeSelection *sel;
182
183   //fprintf(stderr, "drag-begin\n");
184   memset(qdbs, 0, sizeof *qdbs);
185   qdbs->view = GTK_TREE_VIEW(w);
186   qdbs->predicate = (multidrag_row_predicate *)user_data;
187   sel = gtk_tree_view_get_selection(qdbs->view);
188   /* Find out how many rows there are */
189   if(!(qdbs->rows = gtk_tree_selection_count_selected_rows(sel)))
190     return;                             /* doesn't make sense */
191   /* Generate a pixmap for each row */
192   qdbs->pixmaps = g_new(GdkPixmap *, qdbs->rows);
193   gtk_tree_selection_selected_foreach(sel,
194                                       multidrag_make_row_pixmaps,
195                                       qdbs);
196   /* Might not have used all rows */
197   qdbs->rows = qdbs->index;
198   /* Determine the size of the final icon */
199   int height = 0, width = 0;
200   for(int n = 0; n < qdbs->rows; ++n) {
201     int pxw, pxh;
202     gdk_drawable_get_size(qdbs->pixmaps[n], &pxw, &pxh);
203     if(pxw > width)
204       width = pxw;
205     height += pxh;
206   }
207   if(!width || !height)
208     return;                             /* doesn't make sense */
209   /* Construct the icon */
210   icon = gdk_pixmap_new(qdbs->pixmaps[0], width, height, -1);
211   GdkGC *gc = gdk_gc_new(icon);
212   gdk_gc_set_colormap(gc, gtk_widget_get_colormap(w));
213   int y = 0;
214   for(int n = 0; n < qdbs->rows; ++n) {
215     int pxw, pxh;
216     gdk_drawable_get_size(qdbs->pixmaps[n], &pxw, &pxh);
217     gdk_draw_drawable(icon,
218                       gc,
219                       qdbs->pixmaps[n],
220                       0, 0,             /* source coords */
221                       0, y,             /* dest coords */
222                       pxw, pxh);        /* size */
223     y += pxh;
224     gdk_drawable_unref(qdbs->pixmaps[n]);
225     qdbs->pixmaps[n] = NULL;
226   }
227   g_free(qdbs->pixmaps);
228   qdbs->pixmaps = NULL;
229   // TODO scale down a bit, the resulting icons are currently a bit on the
230   // large side.
231   gtk_drag_source_set_icon(w,
232                            gtk_widget_get_colormap(w),
233                            icon,
234                            NULL);
235 }
236
237 static gboolean multidrag_default_predicate(GtkTreePath attribute((unused)) *path,
238                                             GtkTreeIter attribute((unused)) *iter) {
239   return TRUE;
240 }
241
242 /** @brief Allow multi-row drag for @p w
243  * @param w A GtkTreeView widget
244  * @param predicate Function called to test rows for draggability, or NULL
245  *
246  * Suppresses the restriction of selections when a drag is started, and
247  * intercepts drag-begin to construct an icon.
248  *
249  * @p predicate should return TRUE for draggable rows and FALSE otherwise, to
250  * control what goes in the icon.  If NULL, equivalent to a function that
251  * always returns TRUE.
252  */
253 void make_treeview_multidrag(GtkWidget *w,
254                              multidrag_row_predicate *predicate) {
255   if(!predicate)
256     predicate = multidrag_default_predicate;
257   g_signal_connect(w, "button-press-event",
258                    G_CALLBACK(multidrag_button_press_event), NULL);
259   g_signal_connect(w, "button-release-event",
260                    G_CALLBACK(multidrag_button_release_event), NULL);
261   g_signal_connect(w, "drag-begin",
262                    G_CALLBACK(multidrag_drag_begin), predicate);
263 }
264
265 /*
266 Local Variables:
267 c-basic-offset:2
268 comment-column:40
269 fill-column:79
270 indent-tabs-mode:nil
271 End:
272 */