1 /*
2 * talkatu
3 * Copyright (C) 2017-2018 Gary Kramlich <grim@reaperworld.com>
4 *
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Lesser General Public
7 * License as published by the Free Software Foundation; either
8 * version 2 of the License, or (at your option) any later version.
9 *
10 * This library 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 GNU
13 * Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General Public
16 * License along with this library; if not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include <stdio.h>
20
21 #include "talkatu/talkatumenutoolbutton.h"
22
23 /**
24 * SECTION:talkatumenutoolbutton
25 * @Title: Menu Tool Button
26 * @Short_description: A simple menu tool button.
27 *
28 * The normal #GtkMenuToolButton forces you to have an action on the always
29 * visible button. This #GtkToolItem instead just uses a label with no action.
30 */
31
32 struct _TalkatuMenuToolButtonPrivate {
33 GtkWidget *menu;
34 };
35
36 G_DEFINE_TYPE_WITH_PRIVATE(TalkatuMenuToolButton, talkatu_menu_tool_button, GTK_TYPE_TOOL_BUTTON)
37
38 enum {
39 PROP_0 = 0,
40 PROP_MENU,
41 N_PROPERTIES,
42 };
43
44 static GParamSpec *properties[N_PROPERTIES] = {NULL,};
45
46 /******************************************************************************
47 * GObject Stuff
48 *****************************************************************************/
49
50 #if !GTK_CHECK_VERSION(3, 22, 0)
51 static void
talkatu_menu_position_func_helper(GtkMenu * menu,gint * x,gint * y,gboolean * push_in,gpointer data)52 talkatu_menu_position_func_helper(GtkMenu *menu, gint *x, gint *y, gboolean *push_in, gpointer data)
53 {
54 GtkWidget *widget;
55 GtkRequisition requisition;
56 GdkScreen *screen;
57 GdkRectangle monitor;
58 gint monitor_num;
59 gint space_left, space_right, space_above, space_below;
60 gboolean rtl;
61
62 g_return_if_fail(GTK_IS_MENU(menu));
63
64 widget = GTK_WIDGET(menu);
65 screen = gtk_widget_get_screen(widget);
66 rtl = (gtk_widget_get_direction(widget) == GTK_TEXT_DIR_RTL);
67
68 /*
69 * We need the requisition to figure out the right place to
70 * popup the menu. In fact, we always need to ask here, since
71 * if a size_request was queued while we weren't popped up,
72 * the requisition won't have been recomputed yet.
73 */
74 gtk_widget_get_preferred_size(widget, NULL, &requisition);
75
76 monitor_num = gdk_screen_get_monitor_at_point (screen, *x, *y);
77
78 *push_in = FALSE;
79
80 /*
81 * The placement of popup menus horizontally works like this (with
82 * RTL in parentheses)
83 *
84 * - If there is enough room to the right (left) of the mouse cursor,
85 * position the menu there.
86 *
87 * - Otherwise, if if there is enough room to the left (right) of the
88 * mouse cursor, position the menu there.
89 *
90 * - Otherwise if the menu is smaller than the monitor, position it
91 * on the side of the mouse cursor that has the most space available
92 *
93 * - Otherwise (if there is simply not enough room for the menu on the
94 * monitor), position it as far left (right) as possible.
95 *
96 * Positioning in the vertical direction is similar: first try below
97 * mouse cursor, then above.
98 */
99 gdk_screen_get_monitor_geometry (screen, monitor_num, &monitor);
100
101 space_left = *x - monitor.x;
102 space_right = monitor.x + monitor.width - *x - 1;
103 space_above = *y - monitor.y;
104 space_below = monitor.y + monitor.height - *y - 1;
105
106 /* position horizontally */
107
108 if (requisition.width <= space_left || requisition.width <= space_right) {
109 if ((rtl && requisition.width <= space_left) ||
110 (!rtl && requisition.width > space_right)) {
111 /* position left */
112 *x = *x - requisition.width + 1;
113 }
114
115 /* x is clamped on-screen further down */
116
117 } else if (requisition.width <= monitor.width) {
118 /* the menu is too big to fit on either side of the mouse
119 * cursor, but smaller than the monitor. Position it on
120 * the side that has the most space
121 */
122 if (space_left > space_right) {
123 /* left justify */
124 *x = monitor.x;
125 } else {
126 /* right justify */
127 *x = monitor.x + monitor.width - requisition.width;
128 }
129
130 } else {
131 /* menu is simply too big for the monitor */
132 if (rtl) {
133 /* right justify */
134 *x = monitor.x + monitor.width - requisition.width;
135 } else {
136 /* left justify */
137 *x = monitor.x;
138 }
139 }
140
141 /* Position vertically. The algorithm is the same as above, but
142 * simpler because we don't have to take RTL into account.
143 */
144
145 if (requisition.height <= space_above || requisition.height <= space_below) {
146 if (requisition.height > space_below) {
147 *y = *y - requisition.height + 1;
148 }
149
150 *y = CLAMP(*y, monitor.y,
151 monitor.y + monitor.height - requisition.height);
152 } else if (requisition.height > space_below && requisition.height > space_above) {
153 if (space_below >= space_above) {
154 *y = monitor.y + monitor.height - requisition.height;
155 } else {
156 *y = monitor.y;
157 }
158 } else {
159 *y = monitor.y;
160 }
161 }
162
163 /* This comes from gtkmenutoolbutton.c from gtk+
164 * Copyright (C) 2003 Ricardo Fernandez Pascual
165 * Copyright (C) 2004 Paolo Borelli
166 */
167 static void
talkatu_menu_position_func(GtkMenu * menu,gint * x,gint * y,gboolean * push_in,gpointer data)168 talkatu_menu_position_func(GtkMenu *menu,
169 gint *x,
170 gint *y,
171 gboolean *push_in,
172 gpointer data)
173 {
174 GtkWidget *widget = GTK_WIDGET(data);
175 GtkAllocation allocation;
176 gint savy;
177
178 gtk_widget_get_allocation(widget, &allocation);
179 gdk_window_get_origin(gtk_widget_get_window(widget), x, y);
180
181 *x += allocation.x;
182 *y += allocation.y + allocation.height;
183 savy = *y;
184
185 talkatu_menu_position_func_helper(menu, x, y, push_in, data);
186
187 if (savy > *y + 1)
188 *y -= allocation.height;
189 }
190 #endif
191
192 static void
talkatu_menu_tool_button_clicked(GtkToolButton * button)193 talkatu_menu_tool_button_clicked(GtkToolButton *button) {
194 TalkatuMenuToolButtonPrivate *priv = NULL;
195
196 priv = talkatu_menu_tool_button_get_instance_private(TALKATU_MENU_TOOL_BUTTON(button));
197
198 #if GTK_CHECK_VERSION(3, 22, 0)
199 gtk_menu_popup_at_widget(
200 GTK_MENU(priv->menu),
201 GTK_WIDGET(button),
202 GDK_GRAVITY_SOUTH_WEST,
203 GDK_GRAVITY_NORTH_WEST,
204 NULL
205 );
206 #else
207 gtk_menu_popup(
208 GTK_MENU(priv->menu),
209 NULL,
210 NULL,
211 talkatu_menu_position_func,
212 button,
213 GDK_LEFTBUTTON,
214 gtk_get_current_event_time()
215 );
216 #endif
217 }
218
219 static void
talkatu_menu_tool_button_get_property(GObject * obj,guint prop_id,GValue * value,GParamSpec * pspec)220 talkatu_menu_tool_button_get_property(GObject *obj, guint prop_id, GValue *value, GParamSpec *pspec) {
221 TalkatuMenuToolButton *menu_button = TALKATU_MENU_TOOL_BUTTON(obj);
222
223 switch(prop_id) {
224 case PROP_MENU:
225 g_value_set_object(value, talkatu_menu_tool_button_get_menu(menu_button));
226 break;
227 default:
228 G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
229 break;
230 }
231 }
232
233 static void
talkatu_menu_tool_button_set_property(GObject * obj,guint prop_id,const GValue * value,GParamSpec * pspec)234 talkatu_menu_tool_button_set_property(GObject *obj, guint prop_id, const GValue *value, GParamSpec *pspec) {
235 TalkatuMenuToolButton *menu_button = TALKATU_MENU_TOOL_BUTTON(obj);
236
237 switch(prop_id) {
238 case PROP_MENU:
239 talkatu_menu_tool_button_set_menu(menu_button, g_value_get_object(value));
240 break;
241 default:
242 G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop_id, pspec);
243 break;
244 }
245 }
246
247 static void
talkatu_menu_tool_button_init(TalkatuMenuToolButton * menu_button)248 talkatu_menu_tool_button_init(TalkatuMenuToolButton *menu_button) {
249 }
250
251 static void
talkatu_menu_tool_button_class_init(TalkatuMenuToolButtonClass * klass)252 talkatu_menu_tool_button_class_init(TalkatuMenuToolButtonClass *klass) {
253 GObjectClass *obj_class = G_OBJECT_CLASS(klass);
254 GtkToolButtonClass *button_class = GTK_TOOL_BUTTON_CLASS(klass);
255
256 obj_class->get_property = talkatu_menu_tool_button_get_property;
257 obj_class->set_property = talkatu_menu_tool_button_set_property;
258
259 button_class->clicked = talkatu_menu_tool_button_clicked;
260
261 properties[PROP_MENU] = g_param_spec_object(
262 "menu", "menu", "The menu to show",
263 GTK_TYPE_MENU,
264 G_PARAM_READWRITE | G_PARAM_CONSTRUCT
265 );
266
267 g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
268 }
269
270 /******************************************************************************
271 * Public API
272 *****************************************************************************/
273
274 /**
275 * talkatu_menu_tool_button_new:
276 * @label: The label to display.
277 * @icon_name: The optional name of the icon to display.
278 * @menu: The menu to display.
279 *
280 * Creates a new #TalkatuMenuToolButton with the given @label, @icon_name, and
281 * @menu.
282 *
283 * Returns: (transfer full): The new #TalkatuMenuToolButton instance.
284 */
285 GtkToolItem *
talkatu_menu_tool_button_new(const gchar * label,const gchar * icon_name,GtkWidget * menu)286 talkatu_menu_tool_button_new(const gchar *label, const gchar *icon_name, GtkWidget *menu) {
287 return g_object_new(
288 TALKATU_TYPE_MENU_TOOL_BUTTON,
289 "label", label,
290 "icon-name", icon_name,
291 "menu", menu,
292 NULL
293 );
294 }
295
296 /**
297 * talkatu_menu_tool_button_get_menu:
298 * @menu_button: The #TalkatuMenuToolButton instance.
299 *
300 * Get's the menu that this tool button will display on click or #NULL if no
301 * menu is set.
302 *
303 * Returns: (transfer full): The menu.
304 */
305 GtkWidget *
talkatu_menu_tool_button_get_menu(TalkatuMenuToolButton * menu_button)306 talkatu_menu_tool_button_get_menu(TalkatuMenuToolButton *menu_button) {
307 TalkatuMenuToolButtonPrivate *priv = NULL;
308
309 g_return_val_if_fail(TALKATU_IS_MENU_TOOL_BUTTON(menu_button), FALSE);
310
311 priv = talkatu_menu_tool_button_get_instance_private(menu_button);
312
313 if(priv->menu) {
314 return g_object_ref(priv->menu);
315 }
316
317 return NULL;
318 }
319
320 /**
321 * talkatu_menu_tool_button_set_menu:
322 * @menu_button: The #TalkatuMenuToolButton instance.
323 * @menu: The menu to set.
324 *
325 * Sets the menu to be displayed when the user clicks the button.
326 */
327 void
talkatu_menu_tool_button_set_menu(TalkatuMenuToolButton * menu_button,GtkWidget * menu)328 talkatu_menu_tool_button_set_menu(TalkatuMenuToolButton *menu_button, GtkWidget *menu) {
329 TalkatuMenuToolButtonPrivate *priv = NULL;
330
331 g_return_if_fail(TALKATU_IS_MENU_TOOL_BUTTON(menu_button));
332
333 priv = talkatu_menu_tool_button_get_instance_private(menu_button);
334
335 if(priv->menu) {
336 gtk_menu_detach(GTK_MENU(priv->menu));
337 g_object_unref(G_OBJECT(priv->menu));
338 }
339
340 if(menu) {
341 priv->menu = GTK_WIDGET(g_object_ref(G_OBJECT(menu)));
342 gtk_menu_attach_to_widget(GTK_MENU(priv->menu), GTK_WIDGET(menu_button), NULL);
343 } else {
344 priv->menu = NULL;
345 }
346 }
347