1 /*
2  * eog-zoom-entry.c
3  * This file is part of eog
4  *
5  * Author: Felix Riemann <friemann@gnome.org>
6  *
7  * Copyright (C) 2017 GNOME Foundation
8  *
9  * Based on code (ev-zoom-action.c) by:
10  *      - Carlos Garcia Campos <carlosgc@gnome.org>
11  *
12  * This program is free software; you can redistribute it and/or
13  * modify it under the terms of the GNU General Public License
14  * as published by the Free Software Foundation; either version 2
15  * of the License, or (at your option) any later version.
16  *
17  * This program is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20  * GNU General Public License for more details.
21  *
22  * You should have received a copy of the GNU General Public License along
23  * with this program; if not, write to the Free Software Foundation, Inc.,
24  * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
25  */
26 #ifdef HAVE_CONFIG_H
27 #include "config.h"
28 #endif
29 
30 #include "eog-zoom-entry.h"
31 #include <glib/gi18n.h>
32 #include <gtk/gtk.h>
33 #include <math.h>
34 
35 enum {
36 	PROP_0,
37 	PROP_SCROLL_VIEW,
38 	PROP_MENU
39 };
40 
41 typedef struct _EogZoomEntryPrivate {
42 	GtkWidget *btn_zoom_in;
43 	GtkWidget *btn_zoom_out;
44 	GtkWidget *value_entry;
45 
46 	EogScrollView *view;
47 
48 	GMenu *menu;
49 	GMenuModel *zoom_free_section;
50 	GtkWidget       *popup;
51 	gboolean         popup_shown;
52 } EogZoomEntryPrivate;
53 
54 struct _EogZoomEntry {
55 	GtkBox box;
56 
57 	EogZoomEntryPrivate *priv;
58 };
59 
60 static const gdouble zoom_levels[] = {
61 	(1.0/3.0), (1.0/2.0),
62 	1.0, /* 100% */
63 	(1.0/0.75), 2.0, 5.0, 10.0, 15.0, 20.0
64 };
65 
66 G_DEFINE_TYPE_WITH_PRIVATE (EogZoomEntry, eog_zoom_entry, GTK_TYPE_BOX);
67 
68 static void eog_zoom_entry_reset_zoom_level (EogZoomEntry *entry);
69 static void eog_zoom_entry_set_zoom_level (EogZoomEntry *entry, gdouble zoom);
70 static void eog_zoom_entry_update_sensitivity (EogZoomEntry *entry);
71 
72 static gchar*
eog_zoom_entry_format_zoom_value(gdouble value)73 eog_zoom_entry_format_zoom_value (gdouble value)
74 {
75 	gchar *name;
76 	/* Mimic the zoom calculation from EogWindow to get matching displays */
77 	const gint zoom_percent = (gint) floor (value * 100. + 0.5);
78 
79 	/* L10N: This is a percentage value used for the image zoom.
80 	 * This should be translated similar to the statusbar zoom value. */
81 	name = g_strdup_printf(_("%d%%"), zoom_percent);
82 
83 	return name;
84 }
85 
86 static void
eog_zoom_entry_populate_free_zoom_section(EogZoomEntry * zoom_entry)87 eog_zoom_entry_populate_free_zoom_section (EogZoomEntry *zoom_entry)
88 {
89 	guint   i;
90 
91 	for (i = 0; i < G_N_ELEMENTS (zoom_levels); i++) {
92 		GMenuItem *item;
93 		gchar *name;
94 
95 
96 		if (zoom_levels[i] > EOG_SCROLL_VIEW_MAX_ZOOM_FACTOR)
97 			break;
98 
99 		name = eog_zoom_entry_format_zoom_value (zoom_levels[i]);
100 
101 		item = g_menu_item_new (name, NULL);
102 		g_menu_item_set_action_and_target (item, "win.zoom-set",
103 		                                   "d", zoom_levels[i]);
104 		g_menu_append_item (G_MENU (zoom_entry->priv->zoom_free_section), item);
105 		g_object_unref (item);
106 		g_free (name);
107 	}
108 }
109 
110 static void
eog_zoom_entry_activate_cb(GtkEntry * gtk_entry,EogZoomEntry * entry)111 eog_zoom_entry_activate_cb (GtkEntry *gtk_entry, EogZoomEntry *entry)
112 {
113 	const gchar *text = gtk_entry_get_text (gtk_entry);
114 	gchar *end_ptr = NULL;
115 	double zoom_perc;
116 
117 	if (!text || text[0] == '\0') {
118 		eog_zoom_entry_reset_zoom_level (entry);
119 		return;
120 	}
121 	zoom_perc = g_strtod (text, &end_ptr);
122 
123 	if (end_ptr) {
124 		/* Skip whitespace after the digits */
125 		while (end_ptr[0] != '\0' && g_ascii_isspace (end_ptr[0]))
126 			end_ptr++;
127 		if (end_ptr[0] != '\0' && end_ptr[0] != '%') {
128 			eog_zoom_entry_reset_zoom_level (entry);
129 			return;
130 		}
131 	}
132 
133 	eog_scroll_view_set_zoom (entry->priv->view, zoom_perc / 100.0);
134 }
135 
136 static gboolean
focus_out_cb(EogZoomEntry * zoom_entry)137 focus_out_cb (EogZoomEntry *zoom_entry)
138 {
139 	eog_zoom_entry_reset_zoom_level (zoom_entry);
140 
141 	return FALSE;
142 }
143 
144 static void
popup_menu_closed(GtkWidget * popup,EogZoomEntry * zoom_entry)145 popup_menu_closed (GtkWidget    *popup,
146                    EogZoomEntry *zoom_entry)
147 {
148 	if (zoom_entry->priv->popup != popup)
149 		return;
150 
151 	zoom_entry->priv->popup_shown = FALSE;
152 	zoom_entry->priv->popup = NULL;
153 }
154 
155 static GtkWidget*
get_popup(EogZoomEntry * zoom_entry)156 get_popup (EogZoomEntry *zoom_entry)
157 {
158 	GdkRectangle rect;
159 
160 	if (zoom_entry->priv->popup)
161 		return zoom_entry->priv->popup;
162 
163 	zoom_entry->priv->popup = gtk_popover_new_from_model (GTK_WIDGET (zoom_entry),
164 	                                                       G_MENU_MODEL (zoom_entry->priv->menu));
165 	g_signal_connect (zoom_entry->priv->popup, "closed",
166 	                  G_CALLBACK (popup_menu_closed),
167 	                  zoom_entry);
168 	gtk_entry_get_icon_area (GTK_ENTRY (zoom_entry->priv->value_entry),
169 	                         GTK_ENTRY_ICON_SECONDARY, &rect);
170 	gtk_popover_set_relative_to (GTK_POPOVER (zoom_entry->priv->popup),
171 	                             zoom_entry->priv->value_entry);
172 	gtk_popover_set_pointing_to (GTK_POPOVER (zoom_entry->priv->popup), &rect);
173 	gtk_popover_set_position (GTK_POPOVER (zoom_entry->priv->popup), GTK_POS_BOTTOM);
174 	gtk_widget_set_size_request (zoom_entry->priv->popup, 150, -1);
175 
176 	return zoom_entry->priv->popup;
177 }
178 
179 static void
eog_zoom_entry_icon_press_cb(GtkEntry * entry,GtkEntryIconPosition icon_pos,GdkEvent * event,gpointer data)180 eog_zoom_entry_icon_press_cb (GtkEntry *entry, GtkEntryIconPosition icon_pos,
181                               GdkEvent *event, gpointer data)
182 {
183 	EogZoomEntry *zoom_entry;
184 	guint button;
185 
186 	g_return_if_fail (EOG_IS_ZOOM_ENTRY (data));
187 	g_return_if_fail (icon_pos == GTK_ENTRY_ICON_SECONDARY);
188 
189 	if (!gdk_event_get_button (event, &button) || button != GDK_BUTTON_PRIMARY)
190 		return;
191 
192 	zoom_entry = EOG_ZOOM_ENTRY (data);
193 
194 	gtk_widget_show (get_popup (zoom_entry));
195 	zoom_entry->priv->popup_shown = TRUE;
196 }
197 
198 static void
eog_zoom_entry_view_zoom_changed_cb(EogScrollView * view,gdouble zoom,gpointer data)199 eog_zoom_entry_view_zoom_changed_cb (EogScrollView *view, gdouble zoom,
200                                      gpointer data)
201 {
202 	EogZoomEntry *zoom_entry = EOG_ZOOM_ENTRY (data);
203 
204 	eog_zoom_entry_set_zoom_level (zoom_entry, zoom);
205 }
206 
207 static void
button_sensitivity_changed_cb(GObject * gobject,GParamSpec * pspec,gpointer user_data)208 button_sensitivity_changed_cb (GObject    *gobject,
209                                GParamSpec *pspec,
210                                gpointer    user_data)
211 {
212 	g_return_if_fail (EOG_IS_ZOOM_ENTRY (user_data));
213 
214 	eog_zoom_entry_update_sensitivity (EOG_ZOOM_ENTRY (user_data));
215 }
216 
217 static void
eog_zoom_entry_constructed(GObject * object)218 eog_zoom_entry_constructed (GObject *object)
219 {
220 	EogZoomEntry *zoom_entry = EOG_ZOOM_ENTRY (object);
221 
222 	G_OBJECT_CLASS (eog_zoom_entry_parent_class)->constructed (object);
223 
224 	g_signal_connect (zoom_entry->priv->view,
225 	                  "zoom-changed",
226 	                  G_CALLBACK (eog_zoom_entry_view_zoom_changed_cb),
227 	                  zoom_entry);
228 	eog_zoom_entry_reset_zoom_level (zoom_entry);
229 
230 	zoom_entry->priv->zoom_free_section =
231 	                g_menu_model_get_item_link (G_MENU_MODEL (zoom_entry->priv->menu),
232 	                                            1, G_MENU_LINK_SECTION);
233 	eog_zoom_entry_populate_free_zoom_section (zoom_entry);
234 
235 	g_signal_connect (zoom_entry->priv->btn_zoom_in, "notify::sensitive",
236 	                  G_CALLBACK (button_sensitivity_changed_cb),
237 	                  zoom_entry);
238 	g_signal_connect (zoom_entry->priv->btn_zoom_out, "notify::sensitive",
239 	                  G_CALLBACK (button_sensitivity_changed_cb),
240 	                  zoom_entry);
241 	eog_zoom_entry_update_sensitivity (zoom_entry);
242 }
243 
244 static void
eog_zoom_entry_finalize(GObject * object)245 eog_zoom_entry_finalize (GObject *object)
246 {
247 	EogZoomEntry *zoom_entry = EOG_ZOOM_ENTRY (object);
248 
249 	g_clear_object (&zoom_entry->priv->menu);
250 	g_clear_object (&zoom_entry->priv->zoom_free_section);
251 	g_clear_object (&zoom_entry->priv->view);
252 
253 	G_OBJECT_CLASS (eog_zoom_entry_parent_class)->finalize (object);
254 }
255 
256 static void
eog_zoom_entry_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)257 eog_zoom_entry_set_property (GObject      *object,
258                              guint         prop_id,
259                              const GValue *value,
260                              GParamSpec   *pspec)
261 {
262 	EogZoomEntry *zoom_entry = EOG_ZOOM_ENTRY (object);
263 
264 	switch (prop_id) {
265 	case PROP_SCROLL_VIEW:
266 		zoom_entry->priv->view = g_value_dup_object (value);
267 		break;
268 	case PROP_MENU:
269 		zoom_entry->priv->menu = g_value_dup_object (value);
270 		break;
271 	default:
272 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
273 	}
274 }
275 
276 static void
eog_zoom_entry_set_zoom_level(EogZoomEntry * entry,gdouble zoom)277 eog_zoom_entry_set_zoom_level (EogZoomEntry *entry, gdouble zoom)
278 {
279 	gchar *zoom_str;
280 
281 	zoom = CLAMP (zoom, EOG_SCROLL_VIEW_MIN_ZOOM_FACTOR,
282 	              EOG_SCROLL_VIEW_MAX_ZOOM_FACTOR);
283 	zoom_str = eog_zoom_entry_format_zoom_value (zoom);
284 	gtk_entry_set_text (GTK_ENTRY (entry->priv->value_entry), zoom_str);
285 	g_free (zoom_str);
286 }
287 
288 static void
eog_zoom_entry_reset_zoom_level(EogZoomEntry * entry)289 eog_zoom_entry_reset_zoom_level (EogZoomEntry *entry)
290 {
291 	const gdouble zoom = eog_scroll_view_get_zoom (entry->priv->view);
292 	eog_zoom_entry_set_zoom_level (entry, zoom);
293 }
294 
295 static void
eog_zoom_entry_update_sensitivity(EogZoomEntry * entry)296 eog_zoom_entry_update_sensitivity (EogZoomEntry *entry)
297 {
298 	const gboolean current_state =
299 	                gtk_widget_is_sensitive (entry->priv->value_entry);
300 	const gboolean new_state =
301 	                gtk_widget_is_sensitive (entry->priv->btn_zoom_in)
302 	                | gtk_widget_is_sensitive (entry->priv->btn_zoom_out);
303 
304 	if (current_state != new_state) {
305 		gtk_widget_set_sensitive (entry->priv->value_entry, new_state);
306 	}
307 
308 }
309 
310 static void
eog_zoom_entry_class_init(EogZoomEntryClass * klass)311 eog_zoom_entry_class_init (EogZoomEntryClass *klass)
312 {
313 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
314 	GtkWidgetClass *wklass = GTK_WIDGET_CLASS (klass);
315 
316 	object_class->constructed = eog_zoom_entry_constructed;
317 	object_class->set_property = eog_zoom_entry_set_property;
318 	object_class->finalize = eog_zoom_entry_finalize;
319 
320 	gtk_widget_class_set_template_from_resource (wklass,
321 	                                             "/org/gnome/eog/ui/eog-zoom-entry.ui");
322 	gtk_widget_class_bind_template_child_private (wklass,
323 	                                              EogZoomEntry,
324 	                                              btn_zoom_in);
325 	gtk_widget_class_bind_template_child_private (wklass,
326 	                                              EogZoomEntry,
327 	                                              btn_zoom_out);
328 	gtk_widget_class_bind_template_child_private (wklass,
329 	                                              EogZoomEntry,
330 	                                              value_entry);
331 
332 	gtk_widget_class_bind_template_callback (wklass,
333 	                                         eog_zoom_entry_activate_cb);
334 	gtk_widget_class_bind_template_callback (wklass,
335 	                                         eog_zoom_entry_icon_press_cb);
336 
337 	g_object_class_install_property (object_class, PROP_SCROLL_VIEW,
338 	                                 g_param_spec_object ("scroll-view",
339 	                                                      "EogScrollView",
340 	                                                      "The EogScrollView to work with",
341 	                                                      EOG_TYPE_SCROLL_VIEW,
342 	                                                      G_PARAM_WRITABLE |
343 	                                                      G_PARAM_CONSTRUCT_ONLY |
344 	                                                      G_PARAM_STATIC_STRINGS));
345 
346 	g_object_class_install_property (object_class, PROP_MENU,
347 	                                 g_param_spec_object ("menu",
348 	                                                      "Menu",
349 	                                                      "The zoom popup menu",
350 	                                                      G_TYPE_MENU,
351 	                                                      G_PARAM_WRITABLE |
352 	                                                      G_PARAM_CONSTRUCT_ONLY |
353 	                                                      G_PARAM_STATIC_STRINGS));
354 }
355 
356 static void
eog_zoom_entry_init(EogZoomEntry * entry)357 eog_zoom_entry_init (EogZoomEntry *entry)
358 {
359 	entry->priv = eog_zoom_entry_get_instance_private (entry);
360 	gtk_widget_init_template (GTK_WIDGET (entry));
361 
362 	g_signal_connect_swapped (entry->priv->value_entry, "focus-out-event",
363 	                          G_CALLBACK (focus_out_cb),
364 	                          entry);
365 }
366 
367 GtkWidget*
eog_zoom_entry_new(EogScrollView * view,GMenu * menu)368 eog_zoom_entry_new(EogScrollView *view, GMenu *menu)
369 {
370 	g_return_val_if_fail (EOG_IS_SCROLL_VIEW (view), NULL);
371 	g_return_val_if_fail (G_IS_MENU (menu), NULL);
372 
373 	return g_object_new (EOG_TYPE_ZOOM_ENTRY,
374 	                     "scroll-view", view,
375 	                     "menu", menu,
376 	                     NULL);
377 }
378