chiark / gitweb /
DisOrder 5.0
[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 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))
98     return FALSE;
99   /* We are only interested if a well-defined path is clicked */
100   GtkTreePath *path = NULL;
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;
107   /* We are only interested if a selected row is clicked */
108   GtkTreeSelection *s = gtk_tree_view_get_selection(GTK_TREE_VIEW(w));
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);
116   return FALSE;                 /* propagate */
117 }
118
119 static 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
124   /* Did button-press-event do anything?  We just check the outcome rather than
125    * going through all the conditions it tests. */
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.
134       GtkTreePath *path = NULL;
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       }
143       if(path)
144         gtk_tree_path_free(path);
145     }
146   }
147   return FALSE;                 /* propagate */
148 }
149
150 /** @brief State for multidrag_begin() and its callbacks */
151 struct multidrag_begin_state {
152   GtkTreeView *view;
153   multidrag_row_predicate *predicate;
154   int rows;
155   int index;
156   GdkPixmap **pixmaps;
157 };
158
159 /** @brief Callback to construct a row pixmap */
160 static void multidrag_make_row_pixmaps(GtkTreeModel attribute((unused)) *model,
161                                        GtkTreePath *path,
162                                        GtkTreeIter *iter,
163                                        gpointer data) {
164   struct multidrag_begin_state *qdbs = data;
165
166   if(qdbs->predicate(path, iter)) {
167     qdbs->pixmaps[qdbs->index++]
168       = gtk_tree_view_create_row_drag_icon(qdbs->view, path);
169   }
170 }
171
172 /** @brief Called when a drag operation starts
173  * @param w Source widget (the tree view)
174  * @param dc Drag context
175  * @param user_data Row predicate
176  */
177 static void multidrag_drag_begin(GtkWidget *w,
178                                  GdkDragContext attribute((unused)) *dc,
179                                  gpointer user_data) {
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);
187   qdbs->predicate = (multidrag_row_predicate *)user_data;
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);
197   /* Might not have used all rows */
198   qdbs->rows = qdbs->index;
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
238 static gboolean multidrag_default_predicate(GtkTreePath attribute((unused)) *path,
239                                             GtkTreeIter attribute((unused)) *iter) {
240   return TRUE;
241 }
242
243 /** @brief Allow multi-row drag for @p w
244  * @param w A GtkTreeView widget
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.
249  *
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.
253  */
254 void make_treeview_multidrag(GtkWidget *w,
255                              multidrag_row_predicate *predicate) {
256   if(!predicate)
257     predicate = multidrag_default_predicate;
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);
262   g_signal_connect(w, "drag-begin",
263                    G_CALLBACK(multidrag_drag_begin), predicate);
264 }
265
266 /*
267 Local Variables:
268 c-basic-offset:2
269 comment-column:40
270 fill-column:79
271 indent-tabs-mode:nil
272 End:
273 */