chiark / gitweb /
Implement dragging from the choose tab. This adds a new parameter to
[disorder] / disobedience / multidrag.c
1 /*
2  * This file is part of DisOrder
3  * Copyright (C) 2009 Richard Kettlewell
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  * 
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 /** @file disobedience/multidrag.c
19  * @brief Drag multiple rows of a GtkTreeView
20  *
21  * Normally when you start a drag, GtkTreeView sets the selection to just row
22  * you dragged from (because it can't cope with dragging more than one row at a
23  * time).
24  *
25  * Disobedience needs more.
26  *
27  * Firstly it intercepts button-press-event and button-release event and for
28  * clicks that might be the start of drags, suppresses changes to the
29  * selection.  A consequence of this is that it needs to intercept
30  * button-release-event too, to restore the effect of the click, if it turns
31  * out not to be drag after all.
32  *
33  * The location of the initial click is stored in object data called @c
34  * multidrag-where.
35  *
36  * Secondly it intercepts drag-begin and constructs an icon from the rows to be
37  * dragged.
38  *
39  * Inspired by similar code in <a
40  * href="http://code.google.com/p/quodlibet/">Quodlibet</a> (another software
41  * jukebox, albeit as far as I can see a single-user one).
42  */
43 #include <config.h>
44
45 #include <string.h>
46 #include <glib.h>
47 #include <gtk/gtk.h>
48
49 #include "multidrag.h"
50
51 static gboolean multidrag_selection_block(GtkTreeSelection attribute((unused)) *selection,
52                                           GtkTreeModel attribute((unused)) *model,
53                                           GtkTreePath attribute((unused)) *path,
54                                           gboolean attribute((unused)) path_currently_selected,
55                                           gpointer data) {
56   return *(const gboolean *)data;
57 }
58
59 static void block_selection(GtkWidget *w, gboolean block,
60                             int x, int y) {
61   static const gboolean which[] = { FALSE, TRUE };
62   GtkTreeSelection *s = gtk_tree_view_get_selection(GTK_TREE_VIEW(w));
63   gtk_tree_selection_set_select_function(s,
64                                          multidrag_selection_block,
65                                          (gboolean *)&which[!!block],
66                                          NULL);
67   // Remember the pointer location
68   int *where = g_object_get_data(G_OBJECT(w), "multidrag-where");
69   if(!where) {
70     where = g_malloc(2 * sizeof (int));
71     g_object_set_data(G_OBJECT(w), "multidrag-where", where);
72   }
73   where[0] = x;
74   where[1] = y;
75   // TODO release 'where' when object is destroyed
76 }
77
78 static gboolean multidrag_button_press_event(GtkWidget *w,
79                                              GdkEventButton *event,
80                                              gpointer attribute((unused)) user_data) {
81   /* By default we assume that anything this button press does should
82    * act as normal */
83   block_selection(w, TRUE, -1, -1);
84   /* We are only interested in left-button behavior */
85   if(event->button != 1)
86     return FALSE;
87   /* We are only interested in unmodified clicks (not SHIFT etc) */
88   if(event->state & GDK_MODIFIER_MASK)
89     return FALSE;
90   /* We are only interested if a well-defined path is clicked */
91   GtkTreePath *path;
92   if(!gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(w),
93                                     event->x, event->y,
94                                     &path,
95                                     NULL,
96                                     NULL, NULL))
97     return FALSE;
98   //gtk_widget_grab_focus(w);    // TODO why??
99   /* We are only interested if a selected row is clicked */
100   GtkTreeSelection *s = gtk_tree_view_get_selection(GTK_TREE_VIEW(w));
101   if(!gtk_tree_selection_path_is_selected(s, path))
102     return FALSE;
103   /* We block subsequent selection changes and remember where the
104    * click was */
105   block_selection(w, FALSE, event->x, event->y);
106   return FALSE;                 /* propagate */
107 }
108
109 static gboolean multidrag_button_release_event(GtkWidget *w,
110                                                GdkEventButton *event,
111                                                gpointer attribute((unused)) user_data) {
112   int *where = g_object_get_data(G_OBJECT(w), "multidrag-where");
113
114   /* Did button-press-event do anything?  We just check the outcome rather than
115    * going through all the conditions it tests. */
116   if(where && where[0] != -1) {
117     // Remember where the down-click was
118     const int x = where[0], y = where[1];
119     // Re-allow selections
120     block_selection(w, TRUE, -1, -1);
121     if(x == event->x && y == event->y) {
122       // If the up-click is at the same location as the down-click,
123       // it's not a drag.
124       GtkTreePath *path;
125       GtkTreeViewColumn *col;
126       if(gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(w),
127                                        event->x, event->y,
128                                        &path,
129                                        &col,
130                                        NULL, NULL)) {
131         gtk_tree_view_set_cursor(GTK_TREE_VIEW(w), path, col, FALSE);
132       }
133     }
134   }
135   return FALSE;                 /* propagate */
136 }
137
138 /** @brief State for multidrag_begin() and its callbacks */
139 struct multidrag_begin_state {
140   GtkTreeView *view;
141   multidrag_row_predicate *predicate;
142   int rows;
143   int index;
144   GdkPixmap **pixmaps;
145 };
146
147 /** @brief Callback to construct a row pixmap */
148 static void multidrag_make_row_pixmaps(GtkTreeModel attribute((unused)) *model,
149                                        GtkTreePath *path,
150                                        GtkTreeIter *iter,
151                                        gpointer data) {
152   struct multidrag_begin_state *qdbs = data;
153
154   if(qdbs->predicate(path, iter)) {
155     qdbs->pixmaps[qdbs->index++]
156       = gtk_tree_view_create_row_drag_icon(qdbs->view, path);
157   }
158 }
159
160 /** @brief Called when a drag operation starts
161  * @param w Source widget (the tree view)
162  * @param dc Drag context
163  * @param user_data Row predicate
164  */
165 static void multidrag_drag_begin(GtkWidget *w,
166                                  GdkDragContext attribute((unused)) *dc,
167                                  gpointer user_data) {
168   struct multidrag_begin_state qdbs[1];
169   GdkPixmap *icon;
170   GtkTreeSelection *sel;
171
172   //fprintf(stderr, "drag-begin\n");
173   memset(qdbs, 0, sizeof *qdbs);
174   qdbs->view = GTK_TREE_VIEW(w);
175   qdbs->predicate = (multidrag_row_predicate *)user_data;
176   sel = gtk_tree_view_get_selection(qdbs->view);
177   /* Find out how many rows there are */
178   if(!(qdbs->rows = gtk_tree_selection_count_selected_rows(sel)))
179     return;                             /* doesn't make sense */
180   /* Generate a pixmap for each row */
181   qdbs->pixmaps = g_new(GdkPixmap *, qdbs->rows);
182   gtk_tree_selection_selected_foreach(sel,
183                                       multidrag_make_row_pixmaps,
184                                       qdbs);
185   /* Might not have used all rows */
186   qdbs->rows = qdbs->index;
187   /* Determine the size of the final icon */
188   int height = 0, width = 0;
189   for(int n = 0; n < qdbs->rows; ++n) {
190     int pxw, pxh;
191     gdk_drawable_get_size(qdbs->pixmaps[n], &pxw, &pxh);
192     if(pxw > width)
193       width = pxw;
194     height += pxh;
195   }
196   if(!width || !height)
197     return;                             /* doesn't make sense */
198   /* Construct the icon */
199   icon = gdk_pixmap_new(qdbs->pixmaps[0], width, height, -1);
200   GdkGC *gc = gdk_gc_new(icon);
201   gdk_gc_set_colormap(gc, gtk_widget_get_colormap(w));
202   int y = 0;
203   for(int n = 0; n < qdbs->rows; ++n) {
204     int pxw, pxh;
205     gdk_drawable_get_size(qdbs->pixmaps[n], &pxw, &pxh);
206     gdk_draw_drawable(icon,
207                       gc,
208                       qdbs->pixmaps[n],
209                       0, 0,             /* source coords */
210                       0, y,             /* dest coords */
211                       pxw, pxh);        /* size */
212     y += pxh;
213     gdk_drawable_unref(qdbs->pixmaps[n]);
214     qdbs->pixmaps[n] = NULL;
215   }
216   g_free(qdbs->pixmaps);
217   qdbs->pixmaps = NULL;
218   // TODO scale down a bit, the resulting icons are currently a bit on the
219   // large side.
220   gtk_drag_source_set_icon(w,
221                            gtk_widget_get_colormap(w),
222                            icon,
223                            NULL);
224 }
225
226 static gboolean multidrag_default_predicate(GtkTreePath attribute((unused)) *path,
227                                             GtkTreeIter attribute((unused)) *iter) {
228   return TRUE;
229 }
230
231 /** @brief Allow multi-row drag for @p w
232  * @param w A GtkTreeView widget
233  * @param predicate Function called to test rows for draggability, or NULL
234  *
235  * Suppresses the restriction of selections when a drag is started, and
236  * intercepts drag-begin to construct an icon.
237  *
238  * @p predicate should return TRUE for draggable rows and FALSE otherwise, to
239  * control what goes in the icon.  If NULL, equivalent to a function that
240  * always returns TRUE.
241  */
242 void make_treeview_multidrag(GtkWidget *w,
243                              multidrag_row_predicate *predicate) {
244   if(!predicate)
245     predicate = multidrag_default_predicate;
246   g_signal_connect(w, "button-press-event",
247                    G_CALLBACK(multidrag_button_press_event), NULL);
248   g_signal_connect(w, "button-release-event",
249                    G_CALLBACK(multidrag_button_release_event), NULL);
250   g_signal_connect(w, "drag-begin",
251                    G_CALLBACK(multidrag_drag_begin), predicate);
252 }
253
254 /*
255 Local Variables:
256 c-basic-offset:2
257 comment-column:40
258 fill-column:79
259 indent-tabs-mode:nil
260 End:
261 */