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