1 ///////////////////////////////////////////////////////////////////////////////
2 // Name:        src/gtk/win_gtk.cpp
3 // Purpose:     native GTK+ widget for wxWindow
4 // Author:      Paul Cornett
5 // Copyright:   (c) 2007 Paul Cornett
6 // Licence:     wxWindows licence
7 ///////////////////////////////////////////////////////////////////////////////
8 
9 #include "wx/wxprec.h"
10 
11 #include "wx/defs.h"
12 
13 #include <gtk/gtk.h>
14 #include "wx/gtk/private/win_gtk.h"
15 
16 #include "wx/gtk/private.h"
17 #include "wx/gtk/private/gtk2-compat.h"
18 
19 /*
20 wxPizza is a custom GTK+ widget derived from GtkFixed.  A custom widget
21 is needed to adapt GTK+ to wxWidgets needs in 3 areas: scrolling, window
22 borders, and RTL.
23 
24 For scrolling, the "set_scroll_adjustments" signal is implemented
25 to make wxPizza appear scrollable to GTK+, allowing it to be put in a
26 GtkScrolledWindow.  Child widget positions are adjusted for the scrolling
27 position in size_allocate.
28 
29 For borders, space is reserved in realize and size_allocate.  The border is
30 drawn on wxPizza's parent GdkWindow.
31 
32 For RTL, child widget positions are mirrored in size_allocate.
33 */
34 
35 struct wxPizzaChild
36 {
37     GtkWidget* widget;
38     int x, y, width, height;
39 };
40 
41 static GtkWidgetClass* parent_class;
42 
43 #ifdef __WXGTK3__
44 enum {
45     PROP_0,
46     PROP_HADJUSTMENT,
47     PROP_VADJUSTMENT,
48     PROP_HSCROLL_POLICY,
49     PROP_VSCROLL_POLICY
50 };
51 #endif
52 
53 extern "C" {
54 
55 struct wxPizzaClass
56 {
57     GtkFixedClass parent;
58 #ifndef __WXGTK3__
59     void (*set_scroll_adjustments)(GtkWidget*, GtkAdjustment*, GtkAdjustment*);
60 #endif
61 };
62 
pizza_size_allocate(GtkWidget * widget,GtkAllocation * alloc)63 static void pizza_size_allocate(GtkWidget* widget, GtkAllocation* alloc)
64 {
65     wxPizza* pizza = WX_PIZZA(widget);
66     GtkBorder border;
67     pizza->get_border(border);
68     int w = alloc->width - border.left - border.right;
69     if (w < 0) w = 0;
70 
71     if (gtk_widget_get_realized(widget))
72     {
73         int h = alloc->height - border.top - border.bottom;
74         if (h < 0) h = 0;
75         const int x = alloc->x + border.left;
76         const int y = alloc->y + border.top;
77 
78         GdkWindow* window = gtk_widget_get_window(widget);
79         int old_x, old_y;
80         gdk_window_get_position(window, &old_x, &old_y);
81 
82         if (x != old_x || y != old_y ||
83             w != gdk_window_get_width(window) || h != gdk_window_get_height(window))
84         {
85             gdk_window_move_resize(window, x, y, w, h);
86 
87             if (border.left + border.right + border.top + border.bottom)
88             {
89                 // old and new border areas need to be invalidated,
90                 // otherwise they will not be erased/redrawn properly
91                 GtkAllocation old_alloc;
92                 gtk_widget_get_allocation(widget, &old_alloc);
93                 GdkWindow* parent = gtk_widget_get_parent_window(widget);
94                 gdk_window_invalidate_rect(parent, &old_alloc, false);
95                 gdk_window_invalidate_rect(parent, alloc, false);
96             }
97         }
98     }
99 
100     gtk_widget_set_allocation(widget, alloc);
101 
102     // adjust child positions
103     for (const GList* p = pizza->m_children; p; p = p->next)
104     {
105         const wxPizzaChild* child = static_cast<wxPizzaChild*>(p->data);
106         if (gtk_widget_get_visible(child->widget))
107         {
108             GtkAllocation child_alloc;
109             // note that child positions do not take border into
110             // account, they need to be relative to widget->window,
111             // which has already been adjusted
112             child_alloc.x = child->x - pizza->m_scroll_x;
113             child_alloc.y = child->y - pizza->m_scroll_y;
114             child_alloc.width  = child->width;
115             child_alloc.height = child->height;
116             if (gtk_widget_get_direction(widget) == GTK_TEXT_DIR_RTL)
117                 child_alloc.x = w - child_alloc.x - child_alloc.width;
118             gtk_widget_size_allocate(child->widget, &child_alloc);
119         }
120     }
121 }
122 
pizza_realize(GtkWidget * widget)123 static void pizza_realize(GtkWidget* widget)
124 {
125     parent_class->realize(widget);
126 
127     wxPizza* pizza = WX_PIZZA(widget);
128     if (pizza->m_windowStyle & wxPizza::BORDER_STYLES)
129     {
130         GtkBorder border;
131         pizza->get_border(border);
132         GtkAllocation a;
133         gtk_widget_get_allocation(widget, &a);
134         int x = a.x + border.left;
135         int y = a.y + border.top;
136         int w = a.width - border.left - border.right;
137         int h = a.height - border.top - border.bottom;
138         if (w < 0) w = 0;
139         if (h < 0) h = 0;
140         gdk_window_move_resize(gtk_widget_get_window(widget), x, y, w, h);
141     }
142 }
143 
pizza_show(GtkWidget * widget)144 static void pizza_show(GtkWidget* widget)
145 {
146     GtkWidget* parent = gtk_widget_get_parent(widget);
147     if (parent && (WX_PIZZA(widget)->m_windowStyle & wxPizza::BORDER_STYLES))
148     {
149         // invalidate whole allocation so borders will be drawn properly
150         GtkAllocation a;
151         gtk_widget_get_allocation(widget, &a);
152         gtk_widget_queue_draw_area(parent, a.x, a.y, a.width, a.height);
153     }
154 
155     parent_class->show(widget);
156 }
157 
pizza_hide(GtkWidget * widget)158 static void pizza_hide(GtkWidget* widget)
159 {
160     GtkWidget* parent = gtk_widget_get_parent(widget);
161     if (parent && (WX_PIZZA(widget)->m_windowStyle & wxPizza::BORDER_STYLES))
162     {
163         // invalidate whole allocation so borders will be erased properly
164         GtkAllocation a;
165         gtk_widget_get_allocation(widget, &a);
166         gtk_widget_queue_draw_area(parent, a.x, a.y, a.width, a.height);
167     }
168 
169     parent_class->hide(widget);
170 }
171 
pizza_add(GtkContainer * container,GtkWidget * widget)172 static void pizza_add(GtkContainer* container, GtkWidget* widget)
173 {
174     WX_PIZZA(container)->put(widget, 0, 0, 1, 1);
175 }
176 
pizza_remove(GtkContainer * container,GtkWidget * widget)177 static void pizza_remove(GtkContainer* container, GtkWidget* widget)
178 {
179     GTK_CONTAINER_CLASS(parent_class)->remove(container, widget);
180 
181     wxPizza* pizza = WX_PIZZA(container);
182     for (GList* p = pizza->m_children; p; p = p->next)
183     {
184         wxPizzaChild* child = static_cast<wxPizzaChild*>(p->data);
185         if (child->widget == widget)
186         {
187             pizza->m_children = g_list_delete_link(pizza->m_children, p);
188             delete child;
189             break;
190         }
191     }
192 }
193 
194 #ifdef __WXGTK3__
195 // Get preferred size of children, to avoid GTK+ warnings complaining
196 // that they were size-allocated without asking their preferred size
children_get_preferred_size(const GList * p)197 static void children_get_preferred_size(const GList* p)
198 {
199     for (; p; p = p->next)
200     {
201         const wxPizzaChild* child = static_cast<wxPizzaChild*>(p->data);
202         if (gtk_widget_get_visible(child->widget))
203         {
204             GtkRequisition req;
205             gtk_widget_get_preferred_size(child->widget, &req, NULL);
206         }
207     }
208 }
209 
pizza_get_preferred_width(GtkWidget * widget,int * minimum,int * natural)210 static void pizza_get_preferred_width(GtkWidget* widget, int* minimum, int* natural)
211 {
212     children_get_preferred_size(WX_PIZZA(widget)->m_children);
213     *minimum = 0;
214     gtk_widget_get_size_request(widget, natural, NULL);
215     if (*natural < 0)
216         *natural = 0;
217 }
218 
pizza_get_preferred_height(GtkWidget * widget,int * minimum,int * natural)219 static void pizza_get_preferred_height(GtkWidget* widget, int* minimum, int* natural)
220 {
221     children_get_preferred_size(WX_PIZZA(widget)->m_children);
222     *minimum = 0;
223     gtk_widget_get_size_request(widget, NULL, natural);
224     if (*natural < 0)
225         *natural = 0;
226 }
227 
pizza_adjust_size_request(GtkWidget * widget,GtkOrientation orientation,int * minimum,int * natural)228 static void pizza_adjust_size_request(GtkWidget* widget, GtkOrientation orientation, int* minimum, int* natural)
229 {
230     parent_class->adjust_size_request(widget, orientation, minimum, natural);
231     // Override adjustments to minimum size. GtkWidgetClass.adjust_size_request()
232     // will use the size request, if set, as the minimum.
233     // But don't override if in a GtkToolbar, it uses the minimum as actual size.
234     GtkWidget* parent = gtk_widget_get_parent(widget);
235     if (parent)
236         parent = gtk_widget_get_parent(parent);
237     if (!GTK_IS_TOOL_ITEM(parent))
238         *minimum = 0;
239 }
240 
241 // Needed to implement GtkScrollable interface, but we don't care about the
242 // properties. wxWindowGTK handles the adjustments and scroll policy.
pizza_get_property(GObject *,guint,GValue *,GParamSpec *)243 static void pizza_get_property(GObject*, guint, GValue*, GParamSpec*)
244 {
245 }
246 
pizza_set_property(GObject *,guint,const GValue *,GParamSpec *)247 static void pizza_set_property(GObject*, guint, const GValue*, GParamSpec*)
248 {
249 }
250 #else
251 // not used, but needs to exist so gtk_widget_set_scroll_adjustments will work
pizza_set_scroll_adjustments(GtkWidget *,GtkAdjustment *,GtkAdjustment *)252 static void pizza_set_scroll_adjustments(GtkWidget*, GtkAdjustment*, GtkAdjustment*)
253 {
254 }
255 
256 // Marshaller needed for set_scroll_adjustments signal,
257 // generated with GLib-2.4.6 glib-genmarshal
258 #define g_marshal_value_peek_object(v)   g_value_get_object (v)
259 static void
g_cclosure_user_marshal_VOID__OBJECT_OBJECT(GClosure * closure,GValue *,guint n_param_values,const GValue * param_values,gpointer,gpointer marshal_data)260 g_cclosure_user_marshal_VOID__OBJECT_OBJECT (GClosure     *closure,
261                                              GValue       * /*return_value*/,
262                                              guint         n_param_values,
263                                              const GValue *param_values,
264                                              gpointer      /*invocation_hint*/,
265                                              gpointer      marshal_data)
266 {
267   typedef void (*GMarshalFunc_VOID__OBJECT_OBJECT) (gpointer     data1,
268                                                     gpointer     arg_1,
269                                                     gpointer     arg_2,
270                                                     gpointer     data2);
271   GMarshalFunc_VOID__OBJECT_OBJECT callback;
272   GCClosure *cc = (GCClosure*) closure;
273   gpointer data1, data2;
274 
275   g_return_if_fail (n_param_values == 3);
276 
277   if (G_CCLOSURE_SWAP_DATA (closure))
278     {
279       data1 = closure->data;
280       data2 = g_value_peek_pointer (param_values + 0);
281     }
282   else
283     {
284       data1 = g_value_peek_pointer (param_values + 0);
285       data2 = closure->data;
286     }
287   callback = (GMarshalFunc_VOID__OBJECT_OBJECT) (marshal_data ? marshal_data : cc->callback);
288 
289   callback (data1,
290             g_marshal_value_peek_object (param_values + 1),
291             g_marshal_value_peek_object (param_values + 2),
292             data2);
293 }
294 #endif
295 
class_init(void * g_class,void *)296 static void class_init(void* g_class, void*)
297 {
298     GtkWidgetClass* widget_class = (GtkWidgetClass*)g_class;
299     widget_class->size_allocate = pizza_size_allocate;
300     widget_class->realize = pizza_realize;
301     widget_class->show = pizza_show;
302     widget_class->hide = pizza_hide;
303     GtkContainerClass* container_class = (GtkContainerClass*)g_class;
304     container_class->add = pizza_add;
305     container_class->remove = pizza_remove;
306 
307 #ifdef __WXGTK3__
308     widget_class->get_preferred_width = pizza_get_preferred_width;
309     widget_class->get_preferred_height = pizza_get_preferred_height;
310     widget_class->adjust_size_request = pizza_adjust_size_request;
311     GObjectClass *gobject_class = G_OBJECT_CLASS(g_class);
312     gobject_class->set_property = pizza_set_property;
313     gobject_class->get_property = pizza_get_property;
314     g_object_class_override_property(gobject_class, PROP_HADJUSTMENT, "hadjustment");
315     g_object_class_override_property(gobject_class, PROP_VADJUSTMENT, "vadjustment");
316     g_object_class_override_property(gobject_class, PROP_HSCROLL_POLICY, "hscroll-policy");
317     g_object_class_override_property(gobject_class, PROP_VSCROLL_POLICY, "vscroll-policy");
318 #else
319     wxPizzaClass* klass = static_cast<wxPizzaClass*>(g_class);
320     // needed to make widget appear scrollable to GTK+
321     klass->set_scroll_adjustments = pizza_set_scroll_adjustments;
322     widget_class->set_scroll_adjustments_signal =
323         g_signal_new(
324             "set_scroll_adjustments",
325             G_TYPE_FROM_CLASS(g_class),
326             G_SIGNAL_RUN_LAST,
327             G_STRUCT_OFFSET(wxPizzaClass, set_scroll_adjustments),
328             NULL, NULL,
329             g_cclosure_user_marshal_VOID__OBJECT_OBJECT,
330             G_TYPE_NONE, 2, GTK_TYPE_ADJUSTMENT, GTK_TYPE_ADJUSTMENT);
331 #endif
332     parent_class = GTK_WIDGET_CLASS(g_type_class_peek_parent(g_class));
333 }
334 
335 } // extern "C"
336 
type()337 GType wxPizza::type()
338 {
339     static GType type;
340     if (type == 0)
341     {
342         const char* name = "wxPizza";
343         char buf[30];
344         for (unsigned i = 0; g_type_from_name(name); i++)
345         {
346             g_snprintf(buf, sizeof(buf), "wxPizza%u", i);
347             name = buf;
348         }
349         const GTypeInfo info = {
350             sizeof(wxPizzaClass),
351             NULL, NULL,
352             class_init,
353             NULL, NULL,
354             sizeof(wxPizza), 0,
355             NULL, NULL
356         };
357         type = g_type_register_static(
358             GTK_TYPE_FIXED, name, &info, GTypeFlags(0));
359 #ifdef __WXGTK3__
360         const GInterfaceInfo interface_info = { NULL, NULL, NULL };
361         g_type_add_interface_static(type, GTK_TYPE_SCROLLABLE, &interface_info);
362 #endif
363     }
364     return type;
365 }
366 
New(long windowStyle)367 GtkWidget* wxPizza::New(long windowStyle)
368 {
369     GtkWidget* widget = GTK_WIDGET(g_object_new(type(), NULL));
370     wxPizza* pizza = WX_PIZZA(widget);
371     pizza->m_children = NULL;
372     pizza->m_scroll_x = 0;
373     pizza->m_scroll_y = 0;
374     pizza->m_windowStyle = windowStyle;
375 #ifdef __WXGTK3__
376     gtk_widget_set_has_window(widget, true);
377 #else
378     gtk_fixed_set_has_window(GTK_FIXED(widget), true);
379 #endif
380     gtk_widget_add_events(widget,
381         GDK_EXPOSURE_MASK |
382         GDK_SCROLL_MASK |
383 #if GTK_CHECK_VERSION(3,4,0)
384         GDK_SMOOTH_SCROLL_MASK |
385 #endif
386         GDK_POINTER_MOTION_MASK |
387         GDK_POINTER_MOTION_HINT_MASK |
388         GDK_BUTTON_MOTION_MASK |
389         GDK_BUTTON1_MOTION_MASK |
390         GDK_BUTTON2_MOTION_MASK |
391         GDK_BUTTON3_MOTION_MASK |
392         GDK_BUTTON_PRESS_MASK |
393         GDK_BUTTON_RELEASE_MASK |
394         GDK_KEY_PRESS_MASK |
395         GDK_KEY_RELEASE_MASK |
396         GDK_ENTER_NOTIFY_MASK |
397         GDK_LEAVE_NOTIFY_MASK |
398         GDK_FOCUS_CHANGE_MASK);
399     return widget;
400 }
401 
move(GtkWidget * widget,int x,int y,int width,int height)402 void wxPizza::move(GtkWidget* widget, int x, int y, int width, int height)
403 {
404     for (const GList* p = m_children; p; p = p->next)
405     {
406         wxPizzaChild* child = static_cast<wxPizzaChild*>(p->data);
407         if (child->widget == widget)
408         {
409             child->x = x;
410             child->y = y;
411             child->width = width;
412             child->height = height;
413             // normally a queue-resize would be needed here, but we know
414             // wxWindowGTK::DoMoveWindow() will take care of it
415             break;
416         }
417     }
418 }
419 
put(GtkWidget * widget,int x,int y,int width,int height)420 void wxPizza::put(GtkWidget* widget, int x, int y, int width, int height)
421 {
422     // Re-parenting a TLW under a child window is possible at wx level but
423     // using a TLW as child at GTK+ level results in problems, so don't do it.
424     if (!gtk_widget_is_toplevel(GTK_WIDGET(widget)))
425         gtk_fixed_put(GTK_FIXED(this), widget, 0, 0);
426 
427     wxPizzaChild* child = new wxPizzaChild;
428     child->widget = widget;
429     child->x = x;
430     child->y = y;
431     child->width = width;
432     child->height = height;
433     m_children = g_list_append(m_children, child);
434 }
435 
436 struct AdjustData {
437     GdkWindow* window;
438     int dx, dy;
439 };
440 
441 // Adjust allocations for all widgets using the GdkWindow which was just scrolled
442 extern "C" {
scroll_adjust(GtkWidget * widget,void * data)443 static void scroll_adjust(GtkWidget* widget, void* data)
444 {
445     const AdjustData* p = static_cast<AdjustData*>(data);
446     GtkAllocation a;
447     gtk_widget_get_allocation(widget, &a);
448     a.x += p->dx;
449     a.y += p->dy;
450     gtk_widget_set_allocation(widget, &a);
451 
452     if (gtk_widget_get_window(widget) == p->window)
453     {
454         // GtkFrame requires a queue_resize, otherwise parts of
455         // the frame newly exposed by the scroll are not drawn.
456         // To be safe, do it for all widgets.
457         gtk_widget_queue_resize_no_redraw(widget);
458         if (GTK_IS_CONTAINER(widget))
459             gtk_container_forall(GTK_CONTAINER(widget), scroll_adjust, data);
460     }
461 }
462 }
463 
scroll(int dx,int dy)464 void wxPizza::scroll(int dx, int dy)
465 {
466     GtkWidget* widget = GTK_WIDGET(this);
467     if (gtk_widget_get_direction(widget) == GTK_TEXT_DIR_RTL)
468         dx = -dx;
469     m_scroll_x -= dx;
470     m_scroll_y -= dy;
471     GdkWindow* window = gtk_widget_get_window(widget);
472     if (window)
473     {
474         gdk_window_scroll(window, dx, dy);
475         // Adjust child allocations. Doing a queue_resize on the children is not
476         // enough, sometimes they redraw in the wrong place during fast scrolling.
477         AdjustData data = { window, dx, dy };
478         gtk_container_forall(GTK_CONTAINER(widget), scroll_adjust, &data);
479     }
480 }
481 
get_border(GtkBorder & border)482 void wxPizza::get_border(GtkBorder& border)
483 {
484 #ifndef __WXUNIVERSAL__
485     if (m_windowStyle & wxBORDER_SIMPLE)
486         border.left = border.right = border.top = border.bottom = 1;
487     else if (m_windowStyle & (wxBORDER_RAISED | wxBORDER_SUNKEN | wxBORDER_THEME))
488     {
489 #ifdef __WXGTK3__
490         GtkStyleContext* sc;
491         if (m_windowStyle & (wxHSCROLL | wxVSCROLL))
492             sc = gtk_widget_get_style_context(wxGTKPrivate::GetTreeWidget());
493         else
494             sc = gtk_widget_get_style_context(wxGTKPrivate::GetEntryWidget());
495 
496         gtk_style_context_set_state(sc, GTK_STATE_FLAG_NORMAL);
497         gtk_style_context_get_border(sc, GTK_STATE_FLAG_NORMAL, &border);
498 #else // !__WXGTK3__
499         GtkStyle* style;
500         if (m_windowStyle & (wxHSCROLL | wxVSCROLL))
501             style = gtk_widget_get_style(wxGTKPrivate::GetTreeWidget());
502         else
503             style = gtk_widget_get_style(wxGTKPrivate::GetEntryWidget());
504 
505         border.left = border.right = style->xthickness;
506         border.top = border.bottom = style->ythickness;
507 #endif // !__WXGTK3__
508     }
509     else
510 #endif // !__WXUNIVERSAL__
511     {
512         border.left = border.right = border.top = border.bottom = 0;
513     }
514 }
515