1
2 /*
3 * The Real SoundTracker - User activity history
4 *
5 * Copyright (C) 2019-2021 Yury Aliaev
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
20 */
21
22 #include <stdarg.h>
23
24 #include <glib/gi18n.h>
25
26 #include "gui-settings.h"
27 #include "gui.h"
28 #include "history.h"
29
30 typedef struct {
31 HistoryActionType type;
32 const gchar* title;
33 gint page, ins, smp, pos, pat;
34 gint extra_flags;
35 union {
36 gpointer arg_pointer;
37 gint iarg;
38 } arg;
39 gsize arg_size;
40 void (*undo_func)(const gint ins, const gint smp, const gboolean redo,
41 gpointer arg, gpointer data);
42 void (*cleanup_func)(gpointer arg);
43 gpointer data;
44 } Action;
45
46 static GQueue historique = G_QUEUE_INIT;
47 static GList *current = NULL, *saved = NULL;
48
49 static GtkWidget *undo_menu, *redo_menu;
50
51 static gsize free_size = -1;
52 static gboolean force_modified = FALSE, in_history = FALSE;
53 gboolean history_skip = FALSE;
54
55 static void
update_menus(void)56 update_menus(void)
57 {
58 Action* element;
59 GList* prev;
60 gchar* label;
61
62 if (current) {
63 element = current->data;
64 label = g_strdup_printf("%s: %s", _("_Undo"), element->title);
65 gtk_menu_item_set_label(GTK_MENU_ITEM(undo_menu), label);
66 g_free(label);
67 } else
68 gtk_menu_item_set_label(GTK_MENU_ITEM(undo_menu), _("_Undo"));
69 gtk_widget_set_sensitive(undo_menu, current != NULL);
70
71 prev = current ? current->prev : historique.tail;
72 if (prev) {
73 element = prev->data;
74 label = g_strdup_printf("%s: %s", _("_Redo"), element->title);
75 gtk_menu_item_set_label(GTK_MENU_ITEM(redo_menu), label);
76 g_free(label);
77 } else
78 gtk_menu_item_set_label(GTK_MENU_ITEM(redo_menu), _("_Redo"));
79 gtk_widget_set_sensitive(redo_menu, prev != NULL);
80 }
81
82 void
history_init(GtkBuilder * bd)83 history_init(GtkBuilder* bd)
84 {
85 free_size = gui_settings.undo_size << 20;
86
87 undo_menu = gui_get_widget(bd, "edit_undo", XML_FILE);
88 gtk_widget_set_sensitive(undo_menu, FALSE);
89 redo_menu = gui_get_widget(bd, "edit_redo", XML_FILE);
90 gtk_widget_set_sensitive(redo_menu, FALSE);
91 }
92
93 void
history_clear(const gboolean set_modified)94 history_clear(const gboolean set_modified)
95 {
96 Action* element;
97
98 for (element = g_queue_pop_head(&historique);
99 element;
100 element = g_queue_pop_head(&historique)) {
101 if (element->type == HISTORY_ACTION_POINTER) {
102 if (element->cleanup_func)
103 element->cleanup_func(element->arg.arg_pointer);
104 g_free(element->arg.arg_pointer);
105 }
106 g_free(element);
107 }
108 current = NULL;
109 saved = NULL;
110 force_modified = set_modified;
111 free_size = gui_settings.undo_size << 20;
112
113 update_menus();
114 gui_update_title(NULL);
115 }
116
117 void
history_save(void)118 history_save(void)
119 {
120 saved = current;
121 force_modified = FALSE;
122 gui_update_title(NULL);
123 }
124
125 gboolean
history_get_modified(void)126 history_get_modified(void)
127 {
128 return force_modified || saved != current;
129 }
130
131 static void
undo_redo_common(Action * element,const gboolean redo)132 undo_redo_common(Action* element, const gboolean redo)
133 {
134 in_history = TRUE;
135
136 if (element->page != -1)
137 gui_go_to_page(element->page);
138 if (element->ins != -1)
139 gui_set_current_instrument(element->ins);
140 if (element->smp != -1)
141 gui_set_current_sample(element->smp);
142 if (element->pos != -1)
143 gui_set_current_position(element->pos);
144 if (element->pat != -1)
145 gui_set_current_pattern(element->pat, TRUE);
146
147 /* We need all idle functions to do their work before
148 changing some values */
149 while (gtk_events_pending())
150 gtk_main_iteration();
151
152 switch(element->type) {
153 case HISTORY_ACTION_POINTER:
154 case HISTORY_ACTION_POINTER_NOFREE:
155 element->undo_func(element->ins, element->smp, redo, element->arg.arg_pointer, element->data);
156 break;
157 case HISTORY_ACTION_INT:
158 element->undo_func(element->ins, element->smp, redo, &(element->arg.iarg), element->data);
159 break;
160 default:
161 g_assert_not_reached();
162 }
163 update_menus();
164 gui_update_title(NULL);
165
166 in_history = FALSE;
167 }
168
169 void
history_undo(void)170 history_undo(void)
171 {
172 Action* element;
173
174 if (!current)
175 return;
176
177 element = current->data;
178 current = current->next;
179 undo_redo_common(element, FALSE);
180 }
181
182 void
history_redo(void)183 history_redo(void)
184 {
185 if (current) {
186 if (!current->prev)
187 return;
188 current = current->prev;
189 } else {
190 if (!historique.tail)
191 return;
192 current = historique.tail;
193 }
194
195 undo_redo_common(current->data, TRUE);
196 }
197
198 static void
selection_changed(GtkTreeSelection * sel,gint * current_row)199 selection_changed(GtkTreeSelection* sel, gint* current_row)
200 {
201 gint row = gui_list_get_selection_index(sel);
202
203 if (row < *current_row) {
204 for(; row < *current_row; (*current_row)--)
205 history_undo();
206 } else if (row > *current_row) {
207 for(; row > *current_row; (*current_row)++)
208 history_redo();
209 }
210 }
211
212 void
history_show_dialog(void)213 history_show_dialog(void)
214 {
215 static GtkWidget *dialog = NULL, *history_list;
216 static GtkListStore* ls;
217 static GtkTreeSelection* sel;
218 static gint tag, current_row;
219 GtkTreeIter iter, sel_iter;
220 GtkTreeModel* tm;
221 GList* l;
222 gint i;
223 const gchar* titles[] = {N_("Operation"), N_("Saved")};
224 GType types[] = {G_TYPE_STRING, G_TYPE_STRING};
225 const gfloat alignments[] = {0.0, 0.5};
226 const gboolean expands[] = {FALSE, FALSE};
227
228 if (!dialog) {
229 dialog = gtk_dialog_new_with_buttons(_("Undo History"), GTK_WINDOW(mainwindow),
230 GTK_DIALOG_DESTROY_WITH_PARENT, GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, NULL);
231 gui_dialog_connect(dialog, NULL);
232 gui_dialog_adjust(dialog, GTK_RESPONSE_CLOSE);
233
234 history_list = gui_list_in_scrolled_window_full(2, titles,
235 gtk_dialog_get_content_area(GTK_DIALOG(dialog)),
236 types, alignments, expands, GTK_SELECTION_BROWSE, TRUE, TRUE,
237 NULL, GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);
238 sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(history_list));
239 ls = GUI_GET_LIST_STORE(history_list);
240 tag = g_signal_connect_after(sel, "changed",
241 G_CALLBACK(selection_changed), ¤t_row);
242
243 gtk_widget_set_size_request(history_list, -1, 100);
244 gtk_widget_show_all(dialog);
245 }
246
247 g_signal_handler_block(G_OBJECT(sel), tag);
248 gui_list_clear(history_list);
249 tm = gui_list_freeze(history_list);
250 gtk_list_store_append(ls, &iter);
251 gtk_list_store_set(ls, &iter, 0, "[Initial State]",
252 1, (saved == NULL && !force_modified) ? "*" : "", -1);
253 if (!current) {
254 sel_iter = iter;
255 current_row = 0;
256 }
257 for (i = 1, l = historique.tail; l; l = l->prev, i++) {
258 gtk_list_store_append(ls, &iter);
259 gtk_list_store_set(ls, &iter, 0, ((Action *)l->data)->title,
260 1, (saved == l && !force_modified) ? "*" : "", -1);
261 if (l == current) {
262 sel_iter = iter;
263 current_row = i;
264 }
265 }
266 gui_list_thaw(history_list, tm);
267
268 gtk_tree_selection_select_iter(sel, &sel_iter);
269 gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(history_list),
270 gtk_tree_model_get_path(tm, &sel_iter), NULL, TRUE, 0.5, 0.5);
271 g_signal_handler_unblock(G_OBJECT(sel), tag);
272
273 gtk_window_present(GTK_WINDOW(dialog));
274 }
275
276 gboolean
history_test_collate(HistoryActionType type,const gint flags,gpointer data)277 history_test_collate(HistoryActionType type,
278 const gint flags,
279 gpointer data)
280 {
281 gint ins, smp, pos, pat;
282 Action* element;
283
284 if (historique.head != current)
285 return FALSE;
286 element = g_queue_peek_head(&historique);
287 if (!element)
288 return FALSE;
289
290 /* After the saved state we have to begin a new operation */
291 if (current == saved)
292 return FALSE;
293
294 ins = (flags & HISTORY_FLAG_LOG_INS) ? gui_get_current_instrument() : -1;
295 smp = (flags & HISTORY_FLAG_LOG_SMP) ? gui_get_current_sample() : -1;
296 pos = (flags & HISTORY_FLAG_LOG_POS) ? gui_get_current_position() : -1;
297 pat = (flags & HISTORY_FLAG_LOG_PAT) ? gui_get_current_pattern() : -1;
298 if (element->type == type && element->data == data &&
299 element->ins == ins && element->smp == smp &&
300 element->pos == pos && element->pat == pat &&
301 element->extra_flags == (flags & HISTORY_EXTRA_FLAGS_MASK))
302 return TRUE;
303
304 return FALSE;
305 }
306
307 HistoryStatus
history_log_action_full(HistoryActionType type,const gchar * title,const gint flags,void (* undo_func)(const gint ins,const gint smp,const gboolean redo,gpointer arg,gpointer data),void (* cleanup_func)(gpointer arg),gpointer data,gsize arg_size,...)308 history_log_action_full(HistoryActionType type,
309 const gchar* title,
310 const gint flags,
311 void (*undo_func)(const gint ins, const gint smp, const gboolean redo,
312 gpointer arg, gpointer data),
313 void (*cleanup_func)(gpointer arg),
314 gpointer data,
315 gsize arg_size, ...) /* The last argument can have various type */
316 {
317 Action* element;
318 va_list ap;
319 gboolean collatable = flags & HISTORY_FLAG_COLLATABLE;
320 gint ins, smp, pos, pat;
321
322 /* Sanity check to make debugging easier */
323 g_assert(history_check_size(arg_size));
324
325 if (history_skip || in_history)
326 return HISTORY_STATUS_OK;
327
328 /* Current state is not the history head.
329 Elements newer than current state should be removed */
330 while (historique.head != current) {
331 /* Only the newest action in the list can be collated with the current one */
332 collatable = FALSE;
333 if (historique.head == saved) {
334 /* Saved state is lost */
335 saved = NULL;
336 force_modified = TRUE;
337 }
338 element = g_queue_pop_head(&historique);
339 free_size += element->arg_size;
340 if (element->type == HISTORY_ACTION_POINTER) {
341 if (element->cleanup_func)
342 element->cleanup_func(element->arg.arg_pointer);
343 g_free(element->arg.arg_pointer);
344 }
345 g_free(element);
346 }
347
348 ins = (flags & HISTORY_FLAG_LOG_INS) ? gui_get_current_instrument() : -1;
349 smp = (flags & HISTORY_FLAG_LOG_SMP) ? gui_get_current_sample() : -1;
350 pos = (flags & HISTORY_FLAG_LOG_POS) ? gui_get_current_position() : -1;
351 if (flags & HISTORY_FLAG_FORCE_PAT)
352 pat = flags & HISTORY_FLAG_PARAMETER_MASK;
353 else
354 pat = (flags & HISTORY_FLAG_LOG_PAT) ? gui_get_current_pattern() : -1;
355 element = g_queue_peek_head(&historique);
356 if (collatable && element && current != saved)
357 if (element->type == type && element->data == data &&
358 element->ins == ins && element->smp == smp &&
359 element->pos == pos && element->pat == pat &&
360 element->extra_flags == (flags & HISTORY_EXTRA_FLAGS_MASK))
361 /* Collation, nothing to do */
362 return HISTORY_STATUS_COLLATED;
363
364 if (arg_size > gui_settings.undo_size << 20) {
365 /* Argument is too big, undo is not possible.
366 Caller must check size before logging to avoid this case
367 otherwise argument has to be freed */
368 force_modified = TRUE;
369 return HISTORY_STATUS_NOMEM;
370 }
371 /* Freeing oldest elements if necessary */
372 while (free_size < arg_size) {
373 if (historique.tail == saved)
374 /* Saved state is lost */
375 saved = NULL;
376 /* If saved state has already been NULL (initial state), it
377 anyway will be lost if any tail element will be deleted */
378 if (!saved)
379 force_modified = TRUE;
380
381 element = g_queue_pop_tail(&historique);
382 free_size += element->arg_size;
383 if (element->type == HISTORY_ACTION_POINTER) {
384 if (element->cleanup_func)
385 element->cleanup_func(element->arg.arg_pointer);
386 g_free(element->arg.arg_pointer);
387 }
388 g_free(element);
389 }
390
391 element = g_new(Action, 1);
392 element->type = type;
393 element->title = title;
394 element->extra_flags = flags & HISTORY_EXTRA_FLAGS_MASK;
395 if (flags & HISTORY_FLAG_FORCE_PAGE)
396 element->page = flags & HISTORY_FLAG_PARAMETER_MASK;
397 else
398 element->page = (flags & HISTORY_FLAG_LOG_PAGE) ? notebook_current_page : -1;
399 element->ins = ins;
400 element->smp = smp;
401 element->pos = pos;
402 element->pat = pat;
403
404 va_start(ap, arg_size);
405 switch (type) {
406 case HISTORY_ACTION_POINTER:
407 case HISTORY_ACTION_POINTER_NOFREE:
408 element->arg.arg_pointer = va_arg(ap, gpointer);
409 break;
410 case HISTORY_ACTION_INT:
411 element->arg.iarg = va_arg(ap, gint);
412 break;
413 default:
414 g_assert_not_reached();
415 }
416 va_end(ap);
417
418 element->arg_size = arg_size;
419 element->undo_func = undo_func;
420 element->cleanup_func = cleanup_func;
421 element->data = data;
422 g_queue_push_head(&historique, element);
423 free_size -= element->arg_size;
424
425 current = historique.head;
426 update_menus();
427 gui_update_title(NULL);
428 return HISTORY_STATUS_OK;
429 }
430
431 gboolean
history_check_size(gsize size)432 history_check_size(gsize size)
433 {
434 return size <= gui_settings.undo_size << 20;
435 }
436
437 gboolean
_history_query_irreversible(GtkWidget * parent,const gchar * message)438 _history_query_irreversible(GtkWidget *parent, const gchar* message)
439 {
440 gboolean res = gui_ok_cancel_modal(parent, _(message));
441
442 if (res) {
443 /* Provided that some irreversible action will be done.
444 Since this point all previous changes cannot be reverted. */
445 history_clear(TRUE);
446 }
447
448 return res;
449 }
450
451 /* Most often used logging and undo/redo functions */
452
453 static void
spin_button_undo(const gint ins,const gint smp,const gboolean redo,gpointer arg,gpointer data)454 spin_button_undo(const gint ins, const gint smp, const gboolean redo,
455 gpointer arg, gpointer data)
456 {
457 gint tmp_value;
458 gint* value = arg;
459
460 g_assert(GTK_IS_SPIN_BUTTON(data));
461
462 tmp_value = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(data));
463 gtk_spin_button_set_value(GTK_SPIN_BUTTON(data), *value);
464 *value = tmp_value;
465 }
466
467 void
history_log_spin_button(GtkSpinButton * sb,const gchar * title,const gint flags,const gint prev_value)468 history_log_spin_button(GtkSpinButton* sb,
469 const gchar* title,
470 const gint flags,
471 const gint prev_value)
472 {
473 history_log_action(HISTORY_ACTION_INT, title, flags | HISTORY_FLAG_COLLATABLE, spin_button_undo,
474 sb, 0, prev_value);
475 }
476
477 static void
toggle_button_undo(const gint ins,const gint smp,const gboolean redo,gpointer arg,gpointer data)478 toggle_button_undo(const gint ins, const gint smp, const gboolean redo,
479 gpointer arg, gpointer data)
480 {
481 gboolean tmp_value;
482 gboolean* value = arg;
483
484 g_assert(GTK_IS_TOGGLE_BUTTON(data));
485
486 tmp_value = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(data));
487 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(data), *value);
488 *value = tmp_value;
489 }
490
491 void
history_log_toggle_button(GtkToggleButton * tb,const gchar * title,const gint flags,const gboolean prev_value)492 history_log_toggle_button(GtkToggleButton* tb,
493 const gchar* title,
494 const gint flags,
495 const gboolean prev_value)
496 {
497 history_log_action(HISTORY_ACTION_INT, title, flags, toggle_button_undo,
498 tb, 0, prev_value);
499 }
500
501 struct EntryArg {
502 gint maxlen;
503 gchar data[1];
504 };
505
506 static void
entry_undo(const gint ins,const gint smp,const gboolean redo,gpointer arg,gpointer data)507 entry_undo(const gint ins, const gint smp, const gboolean redo,
508 gpointer arg, gpointer data)
509 {
510 gchar* tmp_value;
511 struct EntryArg* ea = arg;
512
513 g_assert(GTK_IS_ENTRY(data));
514
515 tmp_value = alloca(ea->maxlen);
516 strncpy(tmp_value, gtk_entry_get_text(GTK_ENTRY(data)), ea->maxlen);
517 gtk_entry_set_text(GTK_ENTRY(data), ea->data);
518 strncpy(ea->data, tmp_value, ea->maxlen);
519 }
520
521 void
history_log_entry(GtkEntry * en,const gchar * title,const gint maxlen,const gint flags,const gchar * prev_value)522 history_log_entry(GtkEntry* en,
523 const gchar* title,
524 const gint maxlen,
525 const gint flags,
526 const gchar* prev_value)
527 {
528 const gsize arg_size = sizeof(struct EntryArg) + sizeof(gchar) * maxlen - 1;
529 struct EntryArg* arg;
530
531 if (history_test_collate(HISTORY_ACTION_POINTER, flags, en))
532 return;
533
534 arg = g_malloc(arg_size);
535 arg->maxlen = maxlen;
536 strncpy(arg->data, prev_value, maxlen);
537 history_log_action(HISTORY_ACTION_POINTER, title, flags, entry_undo,
538 en, arg_size, arg);
539 }
540
541 static void
radio_group_undo(const gint ins,const gint smp,const gboolean redo,gpointer arg,gpointer data)542 radio_group_undo(const gint ins, const gint smp, const gboolean redo,
543 gpointer arg, gpointer data)
544 {
545 gint *index = arg;
546 GtkWidget** buttons = data;
547 gint tmp_value = find_current_toggle(buttons, *index >> 16);
548
549 g_assert(GTK_IS_TOGGLE_BUTTON(buttons[0]));
550
551 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(buttons[*index & 0xffff]), TRUE);
552 *index = (*index & 0xffff0000) | tmp_value;
553 }
554
555 void
history_log_radio_group(GtkWidget ** group,const gchar * title,const gint flags,const gint prev_value,const gint number)556 history_log_radio_group(GtkWidget** group,
557 const gchar* title,
558 const gint flags,
559 const gint prev_value,
560 const gint number)
561 {
562 history_log_action(HISTORY_ACTION_INT, title, flags,
563 /* I (yaliaev) don't want to allocate devoted argument in heap, so
564 I store 2 integers just inside the history queue provided that
565 we have less than 2^16 radio buttons in a group :-) */
566 radio_group_undo, group, 0, prev_value | number << 16);
567 }
568