1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 
3 /*
4  *  GThumb
5  *
6  *  Copyright (C) 2019 The Free Software Foundation, Inc.
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, see <http://www.gnu.org/licenses/>.
20  */
21 
22 #include <config.h>
23 #include <gtk/gtk.h>
24 #include "dlg-preferences-shortcuts.h"
25 #include "glib-utils.h"
26 #include "gth-accel-dialog.h"
27 #include "gth-browser.h"
28 #include "gth-main.h"
29 #include "gth-preferences.h"
30 #include "gtk-utils.h"
31 #include "main.h"
32 
33 
34 #define GET_WIDGET(name) _gtk_builder_get_widget (data->builder, (name))
35 #define BROWSER_DATA_KEY "shortcuts-preference-data"
36 #define GTH_SHORTCUT_CATEGORY_ALL "all"
37 #define GTH_SHORTCUT_CATEGORY_MODIFIED "modified"
38 
39 
40 enum {
41 	ACTION_NAME_COLUMN,
42 	DESCRIPTION_COLUMN,
43 	ACCELERATOR_LABEL_COLUMN
44 };
45 
46 
47 typedef struct {
48 	GthBrowser *browser;
49 	GtkBuilder *builder;
50 	GtkWidget  *preferences_dialog;
51 	GPtrArray  *rows;
52 	char       *show_category;
53 } BrowserData;
54 
55 
56 static void
browser_data_free(BrowserData * data)57 browser_data_free (BrowserData *data)
58 {
59 	g_free (data->show_category);
60 	g_ptr_array_unref (data->rows);
61 	g_object_unref (data->builder);
62 	g_free (data);
63 }
64 
65 
66 typedef struct {
67 	BrowserData *browser_data;
68 	GthShortcut *shortcut;
69 	GtkWidget   *accel_label;
70 	GtkWidget   *revert_button;
71 	gboolean     modified;
72 } RowData;
73 
74 
75 static RowData *
row_data_new(BrowserData * data,GthShortcut * shortcut)76 row_data_new (BrowserData *data,
77 	      GthShortcut *shortcut)
78 {
79 	RowData *row_data;
80 
81 	row_data = g_new0 (RowData, 1);
82 	row_data->browser_data = data;
83 	row_data->shortcut = shortcut;
84 	row_data->accel_label = NULL;
85 
86 	g_ptr_array_add (row_data->browser_data->rows, row_data);
87 
88 	return row_data;
89 }
90 
91 
92 static void
row_data_free(RowData * row_data)93 row_data_free (RowData *row_data)
94 {
95 	if (row_data == NULL)
96 		return;
97 	g_free (row_data);
98 }
99 
100 
101 static void
row_data_update_accel_label(RowData * row_data)102 row_data_update_accel_label (RowData *row_data)
103 {
104 	if (row_data == NULL)
105 		return;
106 
107 	row_data->modified = g_strcmp0 (row_data->shortcut->default_accelerator, row_data->shortcut->accelerator) != 0;
108 	if (row_data->modified) {
109 		char *esc_text;
110 		char *markup_text;
111 
112 		esc_text = g_markup_escape_text (row_data->shortcut->label, -1);
113 		markup_text = g_strdup_printf ("<b>%s</b>", esc_text);
114 		gtk_label_set_markup (GTK_LABEL (row_data->accel_label), markup_text);
115 
116 		g_free (markup_text);
117 		g_free (esc_text);
118 	}
119 	else
120 		gtk_label_set_text (GTK_LABEL (row_data->accel_label), row_data->shortcut->label);
121 
122 	gtk_widget_set_sensitive (row_data->revert_button, row_data->modified);
123 	gtk_widget_set_child_visible (row_data->revert_button, row_data->modified);
124 }
125 
126 
127 static RowData *
find_row_by_shortcut(BrowserData * browser_data,GthShortcut * shortcut)128 find_row_by_shortcut (BrowserData *browser_data,
129 		      GthShortcut *shortcut)
130 {
131 	int i;
132 
133 	for (i = 0; i < browser_data->rows->len; i++) {
134 		RowData *row_data = g_ptr_array_index (browser_data->rows, i);
135 		if (g_strcmp0 (row_data->shortcut->detailed_action, shortcut->detailed_action) == 0)
136 			return row_data;
137 	}
138 
139 	return NULL;
140 }
141 
142 
143 static void
update_filter_if_required(BrowserData * data)144 update_filter_if_required (BrowserData *data)
145 {
146 	if (g_strcmp0 (data->show_category, GTH_SHORTCUT_CATEGORY_MODIFIED) == 0)
147 		gtk_list_box_invalidate_filter (GTK_LIST_BOX (_gtk_builder_get_widget (data->builder, "shortcuts_list")));
148 }
149 
150 
151 static gboolean
row_data_update_shortcut(RowData * row_data,guint keycode,GdkModifierType modifiers,GtkWindow * parent)152 row_data_update_shortcut (RowData         *row_data,
153 			  guint            keycode,
154 			  GdkModifierType  modifiers,
155 			  GtkWindow       *parent)
156 {
157 	gboolean change;
158 
159 	change = gth_window_can_change_shortcut (GTH_WINDOW (row_data->browser_data->browser),
160 						 row_data->shortcut->detailed_action,
161 						 row_data->shortcut->context,
162 						 keycode,
163 						 modifiers,
164 						 parent);
165 
166 	if (change) {
167 		GPtrArray   *shortcuts_v;
168 		GthShortcut *shortcut;
169 
170 		shortcuts_v = gth_window_get_shortcuts (GTH_WINDOW (row_data->browser_data->browser));
171 		shortcut = gth_shortcut_array_find (shortcuts_v,
172 						    row_data->shortcut->context,
173 						    keycode,
174 						    modifiers);
175 		if (shortcut != NULL) {
176 			gth_shortcut_set_key (shortcut, 0, 0);
177 			row_data_update_accel_label (find_row_by_shortcut (row_data->browser_data, shortcut));
178 		}
179 
180 		gth_shortcut_set_key (row_data->shortcut, keycode, modifiers);
181 		row_data_update_accel_label (row_data);
182 		update_filter_if_required (row_data->browser_data);
183 
184 		gth_main_shortcuts_changed (gth_window_get_shortcuts (GTH_WINDOW (row_data->browser_data->browser)));
185 	}
186 
187 	return change;
188 }
189 
190 
191 static void
accel_dialog_response_cb(GtkDialog * dialog,gint response_id,gpointer user_data)192 accel_dialog_response_cb (GtkDialog *dialog,
193 			  gint       response_id,
194 			  gpointer   user_data)
195 {
196 	RowData         *row_data = user_data;
197 	guint            keycode;
198 	GdkModifierType  modifiers;
199 
200 	switch (response_id) {
201 	case GTK_RESPONSE_OK:
202 		if (gth_accel_dialog_get_accel (GTH_ACCEL_DIALOG (dialog), &keycode, &modifiers))
203 			if (row_data_update_shortcut (row_data, keycode, modifiers, GTK_WINDOW (dialog)))
204 				gtk_widget_destroy (GTK_WIDGET (dialog));
205 		break;
206 
207 	case GTK_RESPONSE_CANCEL:
208 		gtk_widget_destroy (GTK_WIDGET (dialog));
209 		break;
210 
211 	case GTH_ACCEL_BUTTON_RESPONSE_DELETE:
212 		row_data_update_shortcut (row_data, 0, 0, GTK_WINDOW (dialog));
213 		gtk_widget_destroy (GTK_WIDGET (dialog));
214 		break;
215 	}
216 }
217 
218 
219 static void
shortcuts_list_row_activated_cb(GtkListBox * box,GtkListBoxRow * row,gpointer user_data)220 shortcuts_list_row_activated_cb (GtkListBox    *box,
221 				 GtkListBoxRow *row,
222 				 gpointer       user_data)
223 {
224 	RowData   *row_data;
225 	GtkWidget *dialog;
226 
227 	row_data = g_object_get_data (G_OBJECT (row), "shortcut-row-data");
228 
229 	dialog = gth_accel_dialog_new (_("Shortcut"),
230 				       _gtk_widget_get_toplevel_if_window (GTK_WIDGET (box)),
231 				       row_data->shortcut->keyval,
232 				       row_data->shortcut->modifiers);
233 	g_signal_connect (dialog,
234 			  "response",
235 			  G_CALLBACK (accel_dialog_response_cb),
236 			  row_data);
237 	gtk_widget_show (dialog);
238 }
239 
240 
241 static void
revert_button_clicked_cb(GtkButton * button,gpointer user_data)242 revert_button_clicked_cb (GtkButton *button,
243 			  gpointer   user_data)
244 {
245 	RowData         *row_data = user_data;
246 	guint            keycode;
247 	GdkModifierType  modifiers;
248 
249 	gtk_accelerator_parse (row_data->shortcut->default_accelerator, &keycode, &modifiers);
250 	row_data_update_shortcut (row_data, keycode, modifiers, GTK_WINDOW (row_data->browser_data->preferences_dialog));
251 }
252 
253 
254 static GtkWidget *
_new_shortcut_row(GthShortcut * shortcut,BrowserData * data)255 _new_shortcut_row (GthShortcut *shortcut,
256 		   BrowserData *data)
257 {
258 	GtkWidget *row;
259 	RowData   *row_data;
260 	GtkWidget *box;
261 	GtkWidget *label;
262 	GtkWidget *button_box;
263 	GtkWidget *button;
264 
265 	row = gtk_list_box_row_new ();
266 	row_data = row_data_new (data, shortcut);
267 	g_object_set_data_full (G_OBJECT (row), "shortcut-row-data", row_data, (GDestroyNotify) row_data_free);
268 
269 	box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
270 	gtk_container_set_border_width (GTK_CONTAINER (box), 5);
271 	gtk_container_add (GTK_CONTAINER (row), box);
272 
273 	label = gtk_label_new (_(shortcut->description));
274 	gtk_label_set_xalign (GTK_LABEL (label), 0.0);
275 	gtk_widget_set_margin_end (label, 12);
276 	gtk_size_group_add_widget (GTK_SIZE_GROUP (gtk_builder_get_object (data->builder, "column1_size_group")), label);
277 	gtk_box_pack_start (GTK_BOX (box), label, TRUE, TRUE, 0);
278 
279 	row_data->accel_label = label = gtk_label_new ("");
280 	gtk_label_set_xalign (GTK_LABEL (label), 0.0);
281 	gtk_widget_set_margin_end (label, 12);
282 	gtk_style_context_add_class (gtk_widget_get_style_context (label), GTK_STYLE_CLASS_DIM_LABEL);
283 	gtk_size_group_add_widget (GTK_SIZE_GROUP (gtk_builder_get_object (data->builder, "column2_size_group")), label);
284 	gtk_box_pack_start (GTK_BOX (box), label, FALSE, FALSE, 0);
285 
286 	button_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
287 	gtk_widget_set_margin_start (button_box, 12);
288 	gtk_widget_set_margin_end (button_box, 12);
289 	gtk_size_group_add_widget (GTK_SIZE_GROUP (gtk_builder_get_object (data->builder, "column3_size_group")), button_box);
290 	gtk_box_pack_start (GTK_BOX (box), button_box, FALSE, FALSE, 0);
291 
292 	row_data->revert_button = button = gtk_button_new_from_icon_name ("edit-clear-symbolic", GTK_ICON_SIZE_MENU);
293 	gtk_button_set_relief (GTK_BUTTON (button), GTK_RELIEF_NONE);
294 	gtk_widget_set_tooltip_text (button, _("Revert"));
295 	gtk_style_context_add_class (gtk_widget_get_style_context (button), "circular");
296 	gtk_style_context_add_class (gtk_widget_get_style_context (button), "revert-shortcut-button");
297 	gtk_box_pack_start (GTK_BOX (button_box), button, FALSE, FALSE, 0);
298 
299 	gtk_widget_show_all (row);
300 	row_data_update_accel_label (row_data);
301 
302 	g_signal_connect (row_data->revert_button,
303 			  "clicked",
304 			  G_CALLBACK (revert_button_clicked_cb),
305 			  row_data);
306 
307 	return row;
308 }
309 
310 
311 static GtkWidget *
_new_shortcut_category_row(const char * category_id,int n_category)312 _new_shortcut_category_row (const char *category_id,
313 			    int         n_category)
314 {
315 	GtkWidget           *row;
316 	GtkWidget           *box;
317 	GthShortcutCategory *category;
318 	const char          *text;
319 	char                *esc_text;
320 	char                *markup_text;
321 	GtkWidget           *label;
322 
323 	row = gtk_list_box_row_new ();
324 	gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (row), FALSE);
325 	gtk_list_box_row_set_selectable (GTK_LIST_BOX_ROW (row), FALSE);
326 
327 	box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
328 	if (n_category > 1)
329 		gtk_widget_set_margin_top (box, 15);
330 	gtk_container_add (GTK_CONTAINER (row), box);
331 
332 	category = gth_main_get_shortcut_category (category_id);
333 	text = (category != NULL) ? _(category->display_name) : _("Other");
334 	esc_text = g_markup_escape_text (text, -1);
335 	markup_text = g_strdup_printf ("<b>%s</b>", esc_text);
336 
337 	label =  gtk_label_new (NULL);
338 	gtk_label_set_markup (GTK_LABEL (label), markup_text);
339 	gtk_label_set_xalign (GTK_LABEL (label), 0.0);
340 	gtk_widget_set_margin_start (label, 5);
341 	gtk_widget_set_margin_end (label, 5);
342 	gtk_widget_set_margin_top (label, 5);
343 	gtk_widget_set_margin_bottom (label, 5);
344 	gtk_box_pack_start (GTK_BOX (box), label, FALSE, FALSE, 0);
345 	gtk_style_context_add_class (gtk_widget_get_style_context (label), GTK_STYLE_CLASS_DIM_LABEL);
346 	gtk_box_pack_start (GTK_BOX (box), gtk_separator_new (GTK_ORIENTATION_HORIZONTAL), FALSE, FALSE, 0);
347 
348 	gtk_widget_show_all (row);
349 
350 	g_free (markup_text);
351 	g_free (esc_text);
352 
353 	return row;
354 }
355 
356 
357 static void
restore_all_button_clicked_cb(GtkButton * button,gpointer user_data)358 restore_all_button_clicked_cb (GtkButton *button,
359 			       gpointer   user_data)
360 {
361 	BrowserData *data = user_data;
362 	GtkWidget   *dialog;
363 	gboolean     reassign;
364 
365 	dialog = _gtk_yesno_dialog_new (GTK_WINDOW (data->preferences_dialog),
366 					GTK_DIALOG_MODAL,
367 					_("Do you want to revert all the changes and use the default shortcuts?"),
368 					_GTK_LABEL_CANCEL,
369 					_("Revert"));
370 	_gtk_dialog_add_class_to_response (GTK_DIALOG (dialog), GTK_RESPONSE_YES, GTK_STYLE_CLASS_DESTRUCTIVE_ACTION);
371 
372 	reassign = gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_YES;
373 	gtk_widget_destroy (GTK_WIDGET (dialog));
374 
375 	if (reassign) {
376 		int i;
377 
378 		for (i = 0; i < data->rows->len; i++) {
379 			RowData *row_data = g_ptr_array_index (data->rows, i);
380 
381 			gth_shortcut_set_accelerator (row_data->shortcut, row_data->shortcut->default_accelerator);
382 			row_data_update_accel_label (row_data);
383 		}
384 
385 		gth_main_shortcuts_changed (gth_window_get_shortcuts (GTH_WINDOW (data->browser)));
386 	}
387 }
388 
389 
390 static int
cmp_category(gconstpointer a,gconstpointer b)391 cmp_category (gconstpointer a,
392 	      gconstpointer b)
393 {
394 	GthShortcutCategory *cat_a = * (GthShortcutCategory **) a;
395 	GthShortcutCategory *cat_b = * (GthShortcutCategory **) b;
396 
397 	if (cat_a->sort_order == cat_b->sort_order)
398 		return 0;
399 	else if (cat_a->sort_order > cat_b->sort_order)
400 		return 1;
401 	else
402 		return -1;
403 }
404 
405 
406 static void
category_combo_box_changed_cb(GtkComboBox * combo_box,gpointer user_data)407 category_combo_box_changed_cb (GtkComboBox *combo_box,
408 			       gpointer     user_data)
409 {
410 	BrowserData *data = user_data;
411 	GtkTreeIter  iter;
412 
413 	if (gtk_combo_box_get_active_iter (combo_box, &iter)) {
414 		char *category_id;
415 
416 		gtk_tree_model_get (GTK_TREE_MODEL (gtk_builder_get_object (data->builder, "category_liststore")),
417 				    &iter,
418 				    0, &category_id,
419 				    -1);
420 
421 		g_free (data->show_category);
422 		data->show_category = g_strdup (category_id);
423 
424 		gtk_list_box_invalidate_filter (GTK_LIST_BOX (_gtk_builder_get_widget (data->builder, "shortcuts_list")));
425 
426 		g_free (category_id);
427 	}
428 }
429 
430 
431 static gboolean
shortcut_filter_func(GtkListBoxRow * row,gpointer user_data)432 shortcut_filter_func (GtkListBoxRow *row,
433 		      gpointer       user_data)
434 {
435 	BrowserData *data = user_data;
436 	RowData     *row_data;
437 
438 	if (g_strcmp0 (data->show_category, GTH_SHORTCUT_CATEGORY_ALL) == 0)
439 		return TRUE;
440 
441 	row_data = g_object_get_data (G_OBJECT (row), "shortcut-row-data");
442 	if ((row_data == NULL) || (row_data->shortcut == NULL))
443 		return FALSE;
444 
445 	if (g_strcmp0 (data->show_category, GTH_SHORTCUT_CATEGORY_MODIFIED) == 0)
446 		return row_data->modified;
447 
448 	return g_strcmp0 (data->show_category, row_data->shortcut->category) == 0;
449 }
450 
451 
452 void
shortcuts__dlg_preferences_construct_cb(GtkWidget * dialog,GthBrowser * browser,GtkBuilder * dialog_builder)453 shortcuts__dlg_preferences_construct_cb (GtkWidget  *dialog,
454 					 GthBrowser *browser,
455 					 GtkBuilder *dialog_builder)
456 {
457 	BrowserData *data;
458 	GHashTable  *visible_categories;
459 	GtkWidget   *shortcuts_list;
460 	GPtrArray   *shortcuts_v;
461 	const char  *last_category;
462 	int          n_category;
463 	int          i;
464 	GtkWidget   *label;
465 	GtkWidget   *page;
466 
467 	data = g_new0 (BrowserData, 1);
468 	data->browser = browser;
469 	data->builder = _gtk_builder_new_from_file ("shortcuts-preferences.ui", NULL);
470 	data->preferences_dialog = dialog;
471 	data->rows = g_ptr_array_new ();
472 	data->show_category = g_strdup (GTH_SHORTCUT_CATEGORY_ALL);
473 
474 	g_object_set_data_full (G_OBJECT (dialog), BROWSER_DATA_KEY, data, (GDestroyNotify) browser_data_free);
475 
476 	/* shortcut list */
477 
478 	visible_categories = g_hash_table_new (g_str_hash, g_str_equal);
479 	shortcuts_list = _gtk_builder_get_widget (data->builder, "shortcuts_list");
480 	shortcuts_v = gth_window_get_shortcuts_by_category (GTH_WINDOW (browser));
481 	last_category = NULL;
482 	n_category = 0;
483 	for (i = 0; i < shortcuts_v->len; i++) {
484 		GthShortcut *shortcut = g_ptr_array_index (shortcuts_v, i);
485 
486 		if (g_strcmp0 (shortcut->category, GTH_SHORTCUT_CATEGORY_HIDDEN) == 0)
487 			continue;
488 
489 		if ((shortcut->context & GTH_SHORTCUT_CONTEXT_INTERNAL) != 0)
490 			continue;
491 
492 		if ((shortcut->context & GTH_SHORTCUT_CONTEXT_DOC) != 0)
493 			continue;
494 
495 		if ((shortcut->context & GTH_SHORTCUT_CONTEXT_FIXED) != 0)
496 			continue;
497 
498 		if (g_strcmp0 (shortcut->category,last_category) != 0) {
499 			last_category = shortcut->category;
500 			n_category++;
501 			g_hash_table_add (visible_categories, shortcut->category);
502 			gtk_list_box_insert (GTK_LIST_BOX (shortcuts_list),
503 					     _new_shortcut_category_row (shortcut->category, n_category),
504 					     -1);
505 		}
506 		gtk_list_box_insert (GTK_LIST_BOX (shortcuts_list),
507 				     _new_shortcut_row (shortcut, data),
508 				     -1);
509 	}
510 	gtk_list_box_set_filter_func (GTK_LIST_BOX (shortcuts_list),
511 				      shortcut_filter_func,
512 				      data,
513 				      NULL);
514 
515 	g_signal_connect (shortcuts_list,
516 			  "row-activated",
517 			  G_CALLBACK (shortcuts_list_row_activated_cb),
518 			  data);
519 	g_signal_connect (_gtk_builder_get_widget (data->builder, "restore_all_button"),
520 			  "clicked",
521 			  G_CALLBACK (restore_all_button_clicked_cb),
522 			  data);
523 
524 	/* shortcut categories */
525 
526 	{
527 		GPtrArray    *category_v;
528 		GtkListStore *list_store;
529 		GtkTreeIter   iter;
530 		int           i;
531 
532 		category_v = _g_ptr_array_dup (gth_main_get_shortcut_categories (), NULL, NULL);
533 		g_ptr_array_sort (category_v, cmp_category);
534 
535 		list_store = (GtkListStore *) gtk_builder_get_object (data->builder, "category_liststore");
536 
537 		gtk_list_store_append (list_store, &iter);
538 		gtk_list_store_set (list_store, &iter,
539 				    0, GTH_SHORTCUT_CATEGORY_ALL,
540 				    1, C_("Shortcuts", "All"),
541 				    -1);
542 
543 		gtk_list_store_append (list_store, &iter);
544 		gtk_list_store_set (list_store, &iter,
545 				    0, GTH_SHORTCUT_CATEGORY_MODIFIED,
546 				    1, C_("Shortcuts", "Modified"),
547 				    -1);
548 
549 		for (i = 0; i < category_v->len; i++) {
550 			GthShortcutCategory *category = g_ptr_array_index (category_v, i);
551 
552 			if (! g_hash_table_contains (visible_categories, category->id))
553 				continue;
554 
555 			gtk_list_store_append (list_store, &iter);
556 			gtk_list_store_set (list_store, &iter,
557 					    0, category->id,
558 					    1, _(category->display_name),
559 					    -1);
560 		}
561 
562 		gtk_combo_box_set_active (GTK_COMBO_BOX (_gtk_builder_get_widget (data->builder, "category_combobox")), 0);
563 		g_signal_connect (_gtk_builder_get_widget (data->builder, "category_combobox"),
564 				  "changed",
565 				  G_CALLBACK (category_combo_box_changed_cb),
566 				  data);
567 
568 		g_ptr_array_unref (category_v);
569 	}
570 
571 	g_hash_table_unref (visible_categories);
572 
573 	/* add the page to the preferences dialog */
574 
575 	label = gtk_label_new (_("Shortcuts"));
576 	gtk_widget_show (label);
577 
578 	page = _gtk_builder_get_widget (data->builder, "preferences_page");
579 	gtk_widget_show (page);
580 	gtk_notebook_append_page (GTK_NOTEBOOK (_gtk_builder_get_widget (dialog_builder, "notebook")), page, label);
581 }
582