chiark / gitweb /
Move drag-begin handling to multidrag.c
[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   int rows;
142   int index;
143   GdkPixmap **pixmaps;
144 };
145
146 /** @brief Callback to construct a row pixmap */
147 static void multidrag_make_row_pixmaps(GtkTreeModel attribute((unused)) *model,
148                                        GtkTreePath *path,
149                                        GtkTreeIter attribute((unused)) *iter,
150                                        gpointer data) {
151   struct multidrag_begin_state *qdbs = data;
152
153   qdbs->pixmaps[qdbs->index++]
154     = gtk_tree_view_create_row_drag_icon(qdbs->view, path);
155 }
156
157 /** @brief Called when a drag operation starts
158  * @param w Source widget (the tree view)
159  * @param dc Drag context
160  * @param user_data Not used
161  */
162 static void multidrag_drag_begin(GtkWidget *w,
163                                  GdkDragContext attribute((unused)) *dc,
164                                  gpointer attribute((unused)) user_data) {
165   struct multidrag_begin_state qdbs[1];
166   GdkPixmap *icon;
167   GtkTreeSelection *sel;
168
169   //fprintf(stderr, "drag-begin\n");
170   memset(qdbs, 0, sizeof *qdbs);
171   qdbs->view = GTK_TREE_VIEW(w);
172   sel = gtk_tree_view_get_selection(qdbs->view);
173   /* Find out how many rows there are */
174   if(!(qdbs->rows = gtk_tree_selection_count_selected_rows(sel)))
175     return;                             /* doesn't make sense */
176   /* Generate a pixmap for each row */
177   qdbs->pixmaps = g_new(GdkPixmap *, qdbs->rows);
178   gtk_tree_selection_selected_foreach(sel,
179                                       multidrag_make_row_pixmaps,
180                                       qdbs);
181   /* Determine the size of the final icon */
182   int height = 0, width = 0;
183   for(int n = 0; n < qdbs->rows; ++n) {
184     int pxw, pxh;
185     gdk_drawable_get_size(qdbs->pixmaps[n], &pxw, &pxh);
186     if(pxw > width)
187       width = pxw;
188     height += pxh;
189   }
190   if(!width || !height)
191     return;                             /* doesn't make sense */
192   /* Construct the icon */
193   icon = gdk_pixmap_new(qdbs->pixmaps[0], width, height, -1);
194   GdkGC *gc = gdk_gc_new(icon);
195   gdk_gc_set_colormap(gc, gtk_widget_get_colormap(w));
196   int y = 0;
197   for(int n = 0; n < qdbs->rows; ++n) {
198     int pxw, pxh;
199     gdk_drawable_get_size(qdbs->pixmaps[n], &pxw, &pxh);
200     gdk_draw_drawable(icon,
201                       gc,
202                       qdbs->pixmaps[n],
203                       0, 0,             /* source coords */
204                       0, y,             /* dest coords */
205                       pxw, pxh);        /* size */
206     y += pxh;
207     gdk_drawable_unref(qdbs->pixmaps[n]);
208     qdbs->pixmaps[n] = NULL;
209   }
210   g_free(qdbs->pixmaps);
211   qdbs->pixmaps = NULL;
212   // TODO scale down a bit, the resulting icons are currently a bit on the
213   // large side.
214   gtk_drag_source_set_icon(w,
215                            gtk_widget_get_colormap(w),
216                            icon,
217                            NULL);
218 }
219
220 /** @brief Allow multi-row drag for @p w
221  * @param w A GtkTreeView widget
222  *
223  * Suppresses the restriction of selections when a drag is started.
224  */
225 void make_treeview_multidrag(GtkWidget *w) {
226   g_signal_connect(w, "button-press-event",
227                    G_CALLBACK(multidrag_button_press_event), NULL);
228   g_signal_connect(w, "button-release-event",
229                    G_CALLBACK(multidrag_button_release_event), NULL);
230   g_signal_connect(w, "drag-begin",
231                    G_CALLBACK(multidrag_drag_begin), NULL);
232 }
233
234 /*
235 Local Variables:
236 c-basic-offset:2
237 comment-column:40
238 fill-column:79
239 indent-tabs-mode:nil
240 End:
241 */