1 /*
2  *      fm-dnd-auto-scroll.c
3  *
4  *      Copyright 2010 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
5  *
6  *      This program is free software; you can redistribute it and/or modify
7  *      it under the terms of the GNU General Public License as published by
8  *      the Free Software Foundation; either version 2 of the License, or
9  *      (at your option) any later version.
10  *
11  *      This program is distributed in the hope that it will be useful,
12  *      but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *      GNU General Public License for more details.
15  *
16  *      You should have received a copy of the GNU General Public License
17  *      along with this program; if not, write to the Free Software
18  *      Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19  *      MA 02110-1301, USA.
20  */
21 
22 /**
23  * SECTION:fm-dnd-auto-scroll
24  * @short_description: Autoscroll when dragged out of widget.
25  * @title: Drag and drop autoscrolling
26  *
27  * @include: libfm/fm-gtk.h
28  *
29  */
30 #include "fm-dnd-auto-scroll.h"
31 #include "gtk-compat.h"
32 
33 #define SCROLL_EDGE_SIZE 15
34 
35 typedef struct _FmDndAutoScroll FmDndAutoScroll;
36 struct _FmDndAutoScroll
37 {
38     GtkWidget* widget;
39     guint timeout;
40     GtkAdjustment* hadj;
41     GtkAdjustment* vadj;
42 };
43 
44 static GQuark data_id = 0;
45 
on_auto_scroll(gpointer user_data)46 static gboolean on_auto_scroll(gpointer user_data)
47 {
48     FmDndAutoScroll* as = (FmDndAutoScroll*)user_data;
49     int x, y;
50     GtkAdjustment* va = as->vadj;
51     GtkAdjustment* ha = as->hadj;
52     GtkWidget* widget = as->widget;
53     GtkAllocation allocation;
54 
55     if(g_source_is_destroyed(g_main_current_source()))
56         return FALSE;
57 
58     gdk_window_get_device_position (gtk_widget_get_window(widget),
59                                     gdk_device_manager_get_client_pointer(
60                                         gdk_display_get_device_manager(
61                                             gdk_window_get_display(
62                                                 gtk_widget_get_window(widget)))),
63                                     &x, &y, NULL);
64     gtk_widget_get_allocation(widget, &allocation);
65 
66     /*
67        HACK.
68        Sometimes we do not get drag-leave signal. (Why?)
69        This check prevents infinite scrolling.
70     */
71     if (y < 0 || y > allocation.height ||
72         x < 0 || x > allocation.width)
73     {
74         as->timeout = 0;
75         return FALSE;
76     }
77 
78     if(va)
79     {
80         if(y < SCROLL_EDGE_SIZE) /* scroll up */
81         {
82             gdouble value = gtk_adjustment_get_value(va);
83             gdouble lower = gtk_adjustment_get_lower(va);
84             if(value > lower)
85             {
86                 value -= gtk_adjustment_get_step_increment(va);
87                 if(value < lower)
88                     value = lower;
89                 gtk_adjustment_set_value(va, value);
90             }
91         }
92         else if(y > (allocation.height - SCROLL_EDGE_SIZE))
93         {
94             gdouble value = gtk_adjustment_get_value(va);
95             gdouble upper = gtk_adjustment_get_upper(va) - gtk_adjustment_get_page_size(va);
96             /* scroll down */
97             if(value < upper)
98             {
99                 value += gtk_adjustment_get_step_increment(va);
100                 if(value > upper)
101                     value = upper;
102                 gtk_adjustment_set_value(va, value);
103             }
104         }
105         gtk_adjustment_value_changed(va);
106     }
107 
108     if(ha)
109     {
110         if(x < SCROLL_EDGE_SIZE) /* scroll to left */
111         {
112             gdouble value = gtk_adjustment_get_value(ha);
113             gdouble lower = gtk_adjustment_get_lower(ha);
114             if(value > lower)
115             {
116                 value -= gtk_adjustment_get_step_increment(ha);
117                 if(value < lower)
118                     value = lower;
119                 gtk_adjustment_set_value(ha, value);
120             }
121         }
122         else if(x > (allocation.width - SCROLL_EDGE_SIZE))
123         {
124             gdouble value = gtk_adjustment_get_value(ha);
125             gdouble upper = gtk_adjustment_get_upper(ha) - gtk_adjustment_get_page_size(ha);
126             /* scroll to right */
127             if(value < upper)
128             {
129                 value += gtk_adjustment_get_step_increment(ha);
130                 if(value > upper)
131                     value = upper;
132                 gtk_adjustment_set_value(ha, value);
133             }
134         }
135         gtk_adjustment_value_changed(ha);
136     }
137     return TRUE;
138 }
139 
on_drag_motion(GtkWidget * widget,GdkDragContext * drag_context,gint x,gint y,guint time,gpointer user_data)140 static gboolean on_drag_motion(GtkWidget *widget, GdkDragContext *drag_context,
141                                gint x, gint y, guint time, gpointer user_data)
142 {
143     FmDndAutoScroll* as = (FmDndAutoScroll*)user_data;
144     /* FIXME: this is a dirty hack for GTK_TREE_MODEL_ROW. When dragging GTK_TREE_MODEL_ROW
145      * we cannot receive "drag-leave" message. So weied! Is it a gtk+ bug? */
146     GdkAtom target = gtk_drag_dest_find_target(widget, drag_context, NULL);
147     if(target == GDK_NONE)
148         return FALSE;
149     if(0 == as->timeout) /* install a scroll timeout if needed */
150     {
151         as->timeout = gdk_threads_add_timeout(150, on_auto_scroll, as);
152     }
153     return FALSE;
154 }
155 
on_drag_leave(GtkWidget * widget,GdkDragContext * drag_context,guint time,gpointer user_data)156 static void on_drag_leave(GtkWidget *widget, GdkDragContext *drag_context,
157                           guint time, gpointer user_data)
158 {
159     FmDndAutoScroll* as = (FmDndAutoScroll*)user_data;
160     if(as->timeout)
161     {
162         g_source_remove(as->timeout);
163         as->timeout = 0;
164     }
165 }
166 
fm_dnd_auto_scroll_free(gpointer user_data)167 static void fm_dnd_auto_scroll_free(gpointer user_data)
168 {
169     FmDndAutoScroll* as = (FmDndAutoScroll*)user_data;
170     if(as->timeout)
171         g_source_remove(as->timeout);
172     if(as->hadj)
173         g_object_unref(as->hadj);
174     if(as->vadj)
175         g_object_unref(as->vadj);
176 
177     g_signal_handlers_disconnect_by_func(as->widget, on_drag_motion, as);
178     g_signal_handlers_disconnect_by_func(as->widget, on_drag_leave, as);
179     g_slice_free(FmDndAutoScroll, as);
180 }
181 
182 /**
183  * fm_dnd_set_dest_auto_scroll
184  * @drag_dest_widget: a drag destination widget
185  * @hadj: horizontal GtkAdjustment
186  * @vadj: vertical GtkAdjustment
187  *
188  * This function installs a "drag-motion" handler to the dest widget
189  * to support auto-scroll when the dragged item is near the margin
190  * of the destination widget. For example, when a user drags an item
191  * over the bottom of a GtkTreeView, the desired behavior should be
192  * to scroll up the content of the tree view and to expose the items
193  * below currently visible region. So the user can drop on them.
194  */
fm_dnd_set_dest_auto_scroll(GtkWidget * drag_dest_widget,GtkAdjustment * hadj,GtkAdjustment * vadj)195 void fm_dnd_set_dest_auto_scroll(GtkWidget* drag_dest_widget,
196                                  GtkAdjustment* hadj, GtkAdjustment* vadj)
197 {
198     FmDndAutoScroll* as;
199     if(G_UNLIKELY(data_id == 0))
200         data_id = g_quark_from_static_string("FmDndAutoScroll");
201 
202     if(G_UNLIKELY(!hadj && !vadj))
203     {
204         g_object_set_qdata_full(G_OBJECT(drag_dest_widget), data_id, NULL, NULL);
205         return;
206     }
207 
208     as = g_slice_new(FmDndAutoScroll);
209     as->widget = drag_dest_widget; /* no g_object_ref is needed here */
210     as->timeout = 0;
211     as->hadj = hadj ? GTK_ADJUSTMENT(g_object_ref(hadj)) : NULL;
212     as->vadj = vadj ? GTK_ADJUSTMENT(g_object_ref(vadj)) : NULL;
213 
214     g_object_set_qdata_full(G_OBJECT(drag_dest_widget), data_id,
215                             as, fm_dnd_auto_scroll_free);
216 
217     g_signal_connect(drag_dest_widget, "drag-motion",
218                      G_CALLBACK(on_drag_motion), as);
219     g_signal_connect(drag_dest_widget, "drag-leave",
220                      G_CALLBACK(on_drag_leave), as);
221 }
222 
223 /**
224  * fm_dnd_unset_dest_auto_scroll
225  * @drag_dest_widget: drag destination widget.
226  *
227  * Unsets what has been done by fm_dnd_set_dest_auto_scroll()
228  */
fm_dnd_unset_dest_auto_scroll(GtkWidget * drag_dest_widget)229 void fm_dnd_unset_dest_auto_scroll(GtkWidget* drag_dest_widget)
230 {
231     if(G_UNLIKELY(data_id == 0))
232         data_id = g_quark_from_static_string("FmDndAutoScroll");
233     g_object_set_qdata_full(G_OBJECT(drag_dest_widget), data_id, NULL, NULL);
234 }
235