1 /*
2  * pluma-status-combo-box.c
3  * This file is part of pluma
4  *
5  * Copyright (C) 2008 - Jesse van den Kieboom
6  * Copyright (C) 2012-2021 MATE Developers
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; if not, write to the Free Software
20  * Foundation, Inc., 51 Franklin St, Fifth Floor,
21  * Boston, MA 02110-1301, USA.
22  */
23 
24 #include "pluma-status-combo-box.h"
25 
26 #define COMBO_BOX_TEXT_DATA "PlumaStatusComboBoxTextData"
27 
28 struct _PlumaStatusComboBoxPrivate
29 {
30 	GtkWidget *frame;
31 	GtkWidget *button;
32 	GtkWidget *hbox;
33 	GtkWidget *label;
34 	GtkWidget *item;
35 	GtkWidget *arrow;
36 
37 	GtkWidget *menu;
38 	GtkWidget *current_item;
39 };
40 
41 /* Signals */
42 enum
43 {
44 	CHANGED,
45 	NUM_SIGNALS
46 };
47 
48 /* Properties */
49 enum
50 {
51 	PROP_0,
52 
53 	PROP_LABEL
54 };
55 
56 static guint signals[NUM_SIGNALS] = { 0 };
57 
G_DEFINE_TYPE_WITH_PRIVATE(PlumaStatusComboBox,pluma_status_combo_box,GTK_TYPE_EVENT_BOX)58 G_DEFINE_TYPE_WITH_PRIVATE (PlumaStatusComboBox, pluma_status_combo_box, GTK_TYPE_EVENT_BOX)
59 
60 static void
61 pluma_status_combo_box_finalize (GObject *object)
62 {
63 	G_OBJECT_CLASS (pluma_status_combo_box_parent_class)->finalize (object);
64 }
65 
66 static void
pluma_status_combo_box_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)67 pluma_status_combo_box_get_property (GObject    *object,
68 			             guint       prop_id,
69 			             GValue     *value,
70 			             GParamSpec *pspec)
71 {
72 	PlumaStatusComboBox *obj = PLUMA_STATUS_COMBO_BOX (object);
73 
74 	switch (prop_id)
75 	{
76 		case PROP_LABEL:
77 			g_value_set_string (value, pluma_status_combo_box_get_label (obj));
78 			break;
79 		default:
80 			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
81 			break;
82 	}
83 }
84 
85 static void
pluma_status_combo_box_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)86 pluma_status_combo_box_set_property (GObject      *object,
87 			             guint         prop_id,
88 			             const GValue *value,
89 			             GParamSpec   *pspec)
90 {
91 	PlumaStatusComboBox *obj = PLUMA_STATUS_COMBO_BOX (object);
92 
93 	switch (prop_id)
94 	{
95 		case PROP_LABEL:
96 			pluma_status_combo_box_set_label (obj, g_value_get_string (value));
97 			break;
98 		default:
99 			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
100 			break;
101 	}
102 }
103 
104 static void
pluma_status_combo_box_constructed(GObject * object)105 pluma_status_combo_box_constructed (GObject *object)
106 {
107 	PlumaStatusComboBox *combo = PLUMA_STATUS_COMBO_BOX (object);
108 	GtkStyleContext *context;
109 	GtkCssProvider *css;
110 	GError *error = NULL;
111 	const gchar style[] =
112 		"* {\n"
113 		"	padding: 0;\n"
114 		"}";
115 
116 	/* make it as small as possible */
117 	css = gtk_css_provider_new ();
118 	if (!gtk_css_provider_load_from_data (css, style, -1, &error))
119 	{
120 		g_warning ("%s", error->message);
121 		g_error_free (error);
122 		return;
123 	}
124 
125 	context = gtk_widget_get_style_context (GTK_WIDGET (combo));
126 	gtk_style_context_add_provider (context, GTK_STYLE_PROVIDER (css),
127 	                                GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
128 	g_object_unref (css);
129 }
130 
131 static void
pluma_status_combo_box_changed(PlumaStatusComboBox * combo,GtkMenuItem * item)132 pluma_status_combo_box_changed (PlumaStatusComboBox *combo,
133 				GtkMenuItem         *item)
134 {
135 	const gchar *text;
136 
137 	text = g_object_get_data (G_OBJECT (item), COMBO_BOX_TEXT_DATA);
138 
139 	if (text != NULL)
140 	{
141 		gtk_label_set_markup (GTK_LABEL (combo->priv->item), text);
142 		combo->priv->current_item = GTK_WIDGET (item);
143 	}
144 }
145 
146 static void
pluma_status_combo_box_class_init(PlumaStatusComboBoxClass * klass)147 pluma_status_combo_box_class_init (PlumaStatusComboBoxClass *klass)
148 {
149 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
150 
151 	object_class->finalize = pluma_status_combo_box_finalize;
152 	object_class->get_property = pluma_status_combo_box_get_property;
153 	object_class->set_property = pluma_status_combo_box_set_property;
154 	object_class->constructed = pluma_status_combo_box_constructed;
155 
156 	klass->changed = pluma_status_combo_box_changed;
157 
158 	signals[CHANGED] =
159 	    g_signal_new ("changed",
160 			  G_OBJECT_CLASS_TYPE (object_class),
161 			  G_SIGNAL_RUN_LAST,
162 			  G_STRUCT_OFFSET (PlumaStatusComboBoxClass,
163 					   changed), NULL, NULL,
164 			  g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1,
165 			  GTK_TYPE_MENU_ITEM);
166 
167 	g_object_class_install_property (object_class, PROP_LABEL,
168 					 g_param_spec_string ("label",
169 					 		      "LABEL",
170 					 		      "The label",
171 					 		      NULL,
172 					 		      G_PARAM_READWRITE));
173 }
174 
175 static void
menu_deactivate(GtkMenu * menu,PlumaStatusComboBox * combo)176 menu_deactivate (GtkMenu             *menu,
177 		 PlumaStatusComboBox *combo)
178 {
179 	gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (combo->priv->button), FALSE);
180 }
181 
182 static void
button_press_event(GtkWidget * widget,GdkEventButton * event,PlumaStatusComboBox * combo)183 button_press_event (GtkWidget           *widget,
184 		    GdkEventButton      *event,
185 		    PlumaStatusComboBox *combo)
186 {
187 	GtkRequisition request;
188 	GtkAllocation allocation;
189 	gint max_height;
190 
191 	gtk_widget_get_preferred_size (combo->priv->menu, NULL, &request);
192 	gtk_widget_get_allocation (GTK_WIDGET (combo), &allocation);
193 
194 	/* do something relative to our own height here, maybe we can do better */
195 	max_height = allocation.height * 20;
196 
197 	if (request.height > max_height)
198 	{
199 		gtk_widget_set_size_request (combo->priv->menu, -1, max_height);
200 		gtk_widget_set_size_request (gtk_widget_get_toplevel (combo->priv->menu), -1, max_height);
201 	}
202 
203 	gtk_menu_popup_at_widget (GTK_MENU (combo->priv->menu),
204 				  gtk_widget_get_parent (widget),
205 				  GDK_GRAVITY_NORTH_WEST,
206 				  GDK_GRAVITY_SOUTH_WEST,
207 				  (const GdkEvent*) event);
208 
209 	gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (combo->priv->button), TRUE);
210 
211 	if (combo->priv->current_item)
212 	{
213 		gtk_menu_shell_select_item (GTK_MENU_SHELL (combo->priv->menu),
214 					    combo->priv->current_item);
215 	}
216 }
217 
218 static void
set_shadow_type(PlumaStatusComboBox * combo)219 set_shadow_type (PlumaStatusComboBox *combo)
220 {
221 	GtkStyleContext *context;
222 	GtkShadowType shadow_type;
223 	GtkWidget *statusbar;
224 
225 	/* This is a hack needed to use the shadow type of a statusbar */
226 	statusbar = gtk_statusbar_new ();
227 	context = gtk_widget_get_style_context (statusbar);
228 
229 	gtk_style_context_get_style (context, "shadow-type", &shadow_type, NULL);
230 	gtk_frame_set_shadow_type (GTK_FRAME (combo->priv->frame), shadow_type);
231 
232 	gtk_widget_destroy (statusbar);
233 }
234 
235 static void
pluma_status_combo_box_init(PlumaStatusComboBox * self)236 pluma_status_combo_box_init (PlumaStatusComboBox *self)
237 {
238 	self->priv = pluma_status_combo_box_get_instance_private (self);
239 
240 	gtk_event_box_set_visible_window (GTK_EVENT_BOX (self), TRUE);
241 
242 	self->priv->frame = gtk_frame_new (NULL);
243 	gtk_widget_show (self->priv->frame);
244 
245 	self->priv->button = gtk_toggle_button_new ();
246 	gtk_widget_set_name (self->priv->button, "pluma-status-combo-button");
247 	gtk_button_set_relief (GTK_BUTTON (self->priv->button), GTK_RELIEF_NONE);
248 	gtk_widget_show (self->priv->button);
249 
250 	set_shadow_type (self);
251 
252 	self->priv->hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 3);
253 	gtk_widget_show (self->priv->hbox);
254 
255 	gtk_container_add (GTK_CONTAINER (self), self->priv->frame);
256 	gtk_container_add (GTK_CONTAINER (self->priv->frame), self->priv->button);
257 	gtk_container_add (GTK_CONTAINER (self->priv->button), self->priv->hbox);
258 
259 	self->priv->label = gtk_label_new ("");
260 	gtk_widget_show (self->priv->label);
261 
262 	gtk_label_set_single_line_mode (GTK_LABEL (self->priv->label), TRUE);
263 	gtk_label_set_xalign (GTK_LABEL (self->priv->label), 0.0);
264 
265 	gtk_box_pack_start (GTK_BOX (self->priv->hbox), self->priv->label, FALSE, TRUE, 0);
266 
267 	self->priv->item = gtk_label_new ("");
268 	gtk_widget_show (self->priv->item);
269 
270 	gtk_label_set_single_line_mode (GTK_LABEL (self->priv->item), TRUE);
271 	gtk_widget_set_halign (self->priv->item, GTK_ALIGN_START);
272 
273 	gtk_box_pack_start (GTK_BOX (self->priv->hbox), self->priv->item, TRUE, TRUE, 0);
274 
275 	self->priv->arrow = gtk_image_new_from_icon_name ("pan-down-symbolic", GTK_ICON_SIZE_BUTTON);
276 	gtk_widget_show (self->priv->arrow);
277 	gtk_widget_set_halign (self->priv->arrow, GTK_ALIGN_CENTER);
278 	gtk_widget_set_valign (self->priv->arrow, GTK_ALIGN_CENTER);
279 
280 	gtk_box_pack_start (GTK_BOX (self->priv->hbox), self->priv->arrow, FALSE, TRUE, 0);
281 
282 	self->priv->menu = gtk_menu_new ();
283 	g_object_ref_sink (self->priv->menu);
284 
285 	g_signal_connect (self->priv->button,
286 			  "button-press-event",
287 			  G_CALLBACK (button_press_event),
288 			  self);
289 	g_signal_connect (self->priv->menu,
290 			  "deactivate",
291 			  G_CALLBACK (menu_deactivate),
292 			  self);
293 }
294 
295 /* public functions */
296 
297 /**
298  * pluma_status_combo_box_new:
299  * @label: (allow-none):
300  */
301 GtkWidget *
pluma_status_combo_box_new(const gchar * label)302 pluma_status_combo_box_new (const gchar *label)
303 {
304 	return g_object_new (PLUMA_TYPE_STATUS_COMBO_BOX, "label", label, NULL);
305 }
306 
307 /**
308  * pluma_status_combo_box_set_label:
309  * @combo:
310  * @label: (allow-none):
311  */
312 void
pluma_status_combo_box_set_label(PlumaStatusComboBox * combo,const gchar * label)313 pluma_status_combo_box_set_label (PlumaStatusComboBox *combo,
314 				  const gchar         *label)
315 {
316 	gchar *text;
317 
318 	g_return_if_fail (PLUMA_IS_STATUS_COMBO_BOX (combo));
319 
320 	text = g_strconcat ("  ", label, ": ", NULL);
321 	gtk_label_set_markup (GTK_LABEL (combo->priv->label), text);
322 	g_free (text);
323 }
324 
325 const gchar *
pluma_status_combo_box_get_label(PlumaStatusComboBox * combo)326 pluma_status_combo_box_get_label (PlumaStatusComboBox *combo)
327 {
328 	g_return_val_if_fail (PLUMA_IS_STATUS_COMBO_BOX (combo), NULL);
329 
330 	return gtk_label_get_label (GTK_LABEL (combo->priv->label));
331 }
332 
333 static void
item_activated(GtkMenuItem * item,PlumaStatusComboBox * combo)334 item_activated (GtkMenuItem         *item,
335 		PlumaStatusComboBox *combo)
336 {
337 	pluma_status_combo_box_set_item (combo, item);
338 }
339 
340 /**
341  * pluma_status_combo_box_add_item:
342  * @combo:
343  * @item:
344  * @text: (allow-none):
345  */
346 void
pluma_status_combo_box_add_item(PlumaStatusComboBox * combo,GtkMenuItem * item,const gchar * text)347 pluma_status_combo_box_add_item (PlumaStatusComboBox *combo,
348 				 GtkMenuItem         *item,
349 				 const gchar         *text)
350 {
351 	g_return_if_fail (PLUMA_IS_STATUS_COMBO_BOX (combo));
352 	g_return_if_fail (GTK_IS_MENU_ITEM (item));
353 
354 	gtk_menu_shell_append (GTK_MENU_SHELL (combo->priv->menu), GTK_WIDGET (item));
355 
356 	pluma_status_combo_box_set_item_text (combo, item, text);
357 	g_signal_connect (item, "activate", G_CALLBACK (item_activated), combo);
358 }
359 
360 void
pluma_status_combo_box_remove_item(PlumaStatusComboBox * combo,GtkMenuItem * item)361 pluma_status_combo_box_remove_item (PlumaStatusComboBox *combo,
362 				    GtkMenuItem         *item)
363 {
364 	g_return_if_fail (PLUMA_IS_STATUS_COMBO_BOX (combo));
365 	g_return_if_fail (GTK_IS_MENU_ITEM (item));
366 
367 	gtk_container_remove (GTK_CONTAINER (combo->priv->menu),
368 			      GTK_WIDGET (item));
369 }
370 
371 /**
372  * pluma_status_combo_box_get_items:
373  * @combo:
374  *
375  * Returns: (element-type Gtk.Widget) (transfer container):
376  */
377 GList *
pluma_status_combo_box_get_items(PlumaStatusComboBox * combo)378 pluma_status_combo_box_get_items (PlumaStatusComboBox *combo)
379 {
380 	g_return_val_if_fail (PLUMA_IS_STATUS_COMBO_BOX (combo), NULL);
381 
382 	return gtk_container_get_children (GTK_CONTAINER (combo->priv->menu));
383 }
384 
385 const gchar *
pluma_status_combo_box_get_item_text(PlumaStatusComboBox * combo,GtkMenuItem * item)386 pluma_status_combo_box_get_item_text (PlumaStatusComboBox *combo,
387 				      GtkMenuItem	  *item)
388 {
389 	const gchar *ret = NULL;
390 
391 	g_return_val_if_fail (PLUMA_IS_STATUS_COMBO_BOX (combo), NULL);
392 	g_return_val_if_fail (GTK_IS_MENU_ITEM (item), NULL);
393 
394 	ret = g_object_get_data (G_OBJECT (item), COMBO_BOX_TEXT_DATA);
395 
396 	return ret;
397 }
398 
399 /**
400  * pluma_status_combo_box_set_item_text:
401  * @combo:
402  * @item:
403  * @text: (allow-none):
404  */
405 void
pluma_status_combo_box_set_item_text(PlumaStatusComboBox * combo,GtkMenuItem * item,const gchar * text)406 pluma_status_combo_box_set_item_text (PlumaStatusComboBox *combo,
407 				      GtkMenuItem	  *item,
408 				      const gchar         *text)
409 {
410 	g_return_if_fail (PLUMA_IS_STATUS_COMBO_BOX (combo));
411 	g_return_if_fail (GTK_IS_MENU_ITEM (item));
412 
413 	g_object_set_data_full (G_OBJECT (item),
414 				COMBO_BOX_TEXT_DATA,
415 				g_strdup (text),
416 				(GDestroyNotify)g_free);
417 }
418 
419 void
pluma_status_combo_box_set_item(PlumaStatusComboBox * combo,GtkMenuItem * item)420 pluma_status_combo_box_set_item (PlumaStatusComboBox *combo,
421 				 GtkMenuItem         *item)
422 {
423 	g_return_if_fail (PLUMA_IS_STATUS_COMBO_BOX (combo));
424 	g_return_if_fail (GTK_IS_MENU_ITEM (item));
425 
426 	g_signal_emit (combo, signals[CHANGED], 0, item, NULL);
427 }
428 
429 GtkLabel *
pluma_status_combo_box_get_item_label(PlumaStatusComboBox * combo)430 pluma_status_combo_box_get_item_label (PlumaStatusComboBox *combo)
431 {
432 	g_return_val_if_fail (PLUMA_IS_STATUS_COMBO_BOX (combo), NULL);
433 
434 	return GTK_LABEL (combo->priv->item);
435 }
436 
437