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), &current_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