1/* Copyright (C) 2019 Red Hat, Inc.
2 *
3 * This library is free software; you can redistribute it and/or
4 * modify it under the terms of the GNU Lesser General Public
5 * License as published by the Free Software Foundation; either
6 * version 2 of the License, or (at your option) any later version.
7 *
8 * This library is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 * Lesser General Public License for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public
14 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
15 */
16
17#include "config.h"
18
19#include "gtkistringprivate.h"
20#include "gtktexthistoryprivate.h"
21
22/*
23 * The GtkTextHistory works in a way that allows text widgets to deliver
24 * information about changes to the underlying text at given offsets within
25 * their text. The GtkTextHistory object uses a series of callback functions
26 * (see GtkTextHistoryFuncs) to apply changes as undo/redo is performed.
27 *
28 * The GtkTextHistory object is careful to avoid tracking changes while
29 * applying specific undo/redo actions.
30 *
31 * Changes are tracked within a series of actions, contained in groups. The
32 * group may be coalesced when gtk_text_history_end_user_action() is
33 * called.
34 *
35 * Calling gtk_text_history_begin_irreversible_action() and
36 * gtk_text_history_end_irreversible_action() can be used to denote a
37 * section of operations that cannot be undone. This will cause all previous
38 * changes tracked by the GtkTextHistory to be discarded.
39 */
40
41typedef struct _Action Action;
42typedef enum _ActionKind ActionKind;
43
44enum _ActionKind
45{
46 ACTION_KIND_BARRIER = 1,
47 ACTION_KIND_DELETE_BACKSPACE = 2,
48 ACTION_KIND_DELETE_KEY = 3,
49 ACTION_KIND_DELETE_PROGRAMMATIC = 4,
50 ACTION_KIND_DELETE_SELECTION = 5,
51 ACTION_KIND_GROUP = 6,
52 ACTION_KIND_INSERT = 7,
53};
54
55struct _Action
56{
57 ActionKind kind;
58 GList link;
59 guint is_modified : 1;
60 guint is_modified_set : 1;
61 union {
62 struct {
63 IString istr;
64 guint begin;
65 guint end;
66 } insert;
67 struct {
68 IString istr;
69 guint begin;
70 guint end;
71 struct {
72 int insert;
73 int bound;
74 } selection;
75 } delete;
76 struct {
77 GQueue actions;
78 guint depth;
79 } group;
80 } u;
81};
82
83struct _GtkTextHistory
84{
85 GObject parent_instance;
86
87 GtkTextHistoryFuncs funcs;
88 gpointer funcs_data;
89
90 GQueue undo_queue;
91 GQueue redo_queue;
92
93 struct {
94 int insert;
95 int bound;
96 } selection;
97
98 guint irreversible;
99 guint in_user;
100 guint max_undo_levels;
101
102 guint can_undo : 1;
103 guint can_redo : 1;
104 guint is_modified : 1;
105 guint is_modified_set : 1;
106 guint applying : 1;
107 guint enabled : 1;
108};
109
110static void action_free (Action *action);
111
112G_DEFINE_TYPE (GtkTextHistory, gtk_text_history, G_TYPE_OBJECT)
113
114#define return_if_applying(instance) \
115 G_STMT_START { \
116 if ((instance)->applying) \
117 return; \
118 } G_STMT_END
119#define return_if_irreversible(instance) \
120 G_STMT_START { \
121 if ((instance)->irreversible) \
122 return; \
123 } G_STMT_END
124#define return_if_not_enabled(instance) \
125 G_STMT_START { \
126 if (!(instance)->enabled) \
127 return; \
128 } G_STMT_END
129
130static inline void
131uint_order (guint *a,
132 guint *b)
133{
134 if (*a > *b)
135 {
136 guint tmp = *a;
137 *a = *b;
138 *b = tmp;
139 }
140}
141
142static void
143clear_action_queue (GQueue *queue)
144{
145 g_assert (queue != NULL);
146
147 while (queue->length > 0)
148 {
149 Action *action = g_queue_peek_head (queue);
150 g_queue_unlink (queue, link_: &action->link);
151 action_free (action);
152 }
153}
154
155static Action *
156action_new (ActionKind kind)
157{
158 Action *action;
159
160 action = g_slice_new0 (Action);
161 action->kind = kind;
162 action->link.data = action;
163
164 return action;
165}
166
167static void
168action_free (Action *action)
169{
170 if (action->kind == ACTION_KIND_INSERT)
171 istring_clear (str: &action->u.insert.istr);
172 else if (action->kind == ACTION_KIND_DELETE_BACKSPACE ||
173 action->kind == ACTION_KIND_DELETE_KEY ||
174 action->kind == ACTION_KIND_DELETE_PROGRAMMATIC ||
175 action->kind == ACTION_KIND_DELETE_SELECTION)
176 istring_clear (str: &action->u.delete.istr);
177 else if (action->kind == ACTION_KIND_GROUP)
178 clear_action_queue (queue: &action->u.group.actions);
179
180 g_slice_free (Action, action);
181}
182
183static gboolean
184action_group_is_empty (const Action *action)
185{
186 const GList *iter;
187
188 g_assert (action->kind == ACTION_KIND_GROUP);
189
190 for (iter = action->u.group.actions.head; iter; iter = iter->next)
191 {
192 const Action *child = iter->data;
193
194 if (child->kind == ACTION_KIND_BARRIER)
195 continue;
196
197 if (child->kind == ACTION_KIND_GROUP && action_group_is_empty (action: child))
198 continue;
199
200 return FALSE;
201 }
202
203 return TRUE;
204}
205
206static gboolean
207action_chain (Action *action,
208 Action *other,
209 gboolean in_user_action)
210{
211 g_assert (action != NULL);
212 g_assert (other != NULL);
213
214 if (action->kind == ACTION_KIND_GROUP)
215 {
216 Action *tail = g_queue_peek_tail (queue: &action->u.group.actions);
217
218 /* Always push new items onto a group, so that we can coalesce
219 * items when gtk_text_history_end_user_action() is called.
220 *
221 * But we don't care if this is a barrier since we will always
222 * apply things as a group anyway.
223 */
224
225 if (other->kind == ACTION_KIND_BARRIER)
226 {
227 /* If we're not in a user action, this barrier is meant to
228 * stop items from coallescing into this group.
229 */
230 if (!in_user_action && action->u.group.depth == 0)
231 return FALSE;
232
233 action_free (action: other);
234 return TRUE;
235 }
236
237 /* Try to chain onto the tail item in the group to increase
238 * the chances we have a single action within the group. That
239 * way we are more likely to hoist out of the group when the
240 * user action is ended.
241 */
242 if (tail != NULL && tail->kind == other->kind)
243 {
244 if (action_chain (action: tail, other, in_user_action))
245 return TRUE;
246 }
247
248 g_queue_push_tail_link (queue: &action->u.group.actions, link_: &other->link);
249
250 return TRUE;
251 }
252
253 /* The rest can only be merged to themselves */
254 if (action->kind != other->kind)
255 return FALSE;
256
257 switch (action->kind)
258 {
259 case ACTION_KIND_INSERT: {
260
261 /* Make sure the new insert is at the end of the previous */
262 if (action->u.insert.end != other->u.insert.begin)
263 return FALSE;
264
265 /* If we are not within a user action, be more selective */
266 if (!in_user_action)
267 {
268 /* Avoid pathological cases */
269 if (other->u.insert.istr.n_chars > 1000)
270 return FALSE;
271
272 /* We will coalesce space, but not new lines. */
273 if (istring_contains_unichar (str: &action->u.insert.istr, ch: '\n') ||
274 istring_contains_unichar (str: &other->u.insert.istr, ch: '\n'))
275 return FALSE;
276
277 /* Chain space to items that ended in space. This is generally
278 * just at the start of a line where we could have indentation
279 * space.
280 */
281 if ((istring_empty (str: &action->u.insert.istr) ||
282 istring_ends_with_space (str: &action->u.insert.istr)) &&
283 istring_only_contains_space (str: &other->u.insert.istr))
284 goto do_chain;
285
286 /* Starting a new word, don't chain this */
287 if (istring_starts_with_space (str: &other->u.insert.istr))
288 return FALSE;
289
290 /* Check for possible paste (multi-character input) or word input that
291 * has spaces in it (and should treat as one operation).
292 */
293 if (other->u.insert.istr.n_chars > 1 &&
294 istring_contains_space (str: &other->u.insert.istr))
295 return FALSE;
296 }
297
298 do_chain:
299
300 istring_append (str: &action->u.insert.istr, other: &other->u.insert.istr);
301 action->u.insert.end += other->u.insert.end - other->u.insert.begin;
302 action_free (action: other);
303
304 return TRUE;
305 }
306
307 case ACTION_KIND_DELETE_PROGRAMMATIC:
308 /* We can't tell if this should be chained because we don't
309 * have a group to coalesce. But unless each action deletes
310 * a single character, the overhead isn't too bad as we embed
311 * the strings in the action.
312 */
313 return FALSE;
314
315 case ACTION_KIND_DELETE_SELECTION:
316 /* Don't join selection deletes as they should appear as a single
317 * operation and have selection reinstanted when performing undo.
318 */
319 return FALSE;
320
321 case ACTION_KIND_DELETE_BACKSPACE:
322 if (other->u.delete.end == action->u.delete.begin)
323 {
324 istring_prepend (str: &action->u.delete.istr,
325 other: &other->u.delete.istr);
326 action->u.delete.begin = other->u.delete.begin;
327 action_free (action: other);
328 return TRUE;
329 }
330
331 return FALSE;
332
333 case ACTION_KIND_DELETE_KEY:
334 if (action->u.delete.begin == other->u.delete.begin)
335 {
336 if (!istring_contains_space (str: &other->u.delete.istr) ||
337 istring_only_contains_space (str: &action->u.delete.istr))
338 {
339 istring_append (str: &action->u.delete.istr, other: &other->u.delete.istr);
340 action->u.delete.end += other->u.delete.istr.n_chars;
341 action_free (action: other);
342 return TRUE;
343 }
344 }
345
346 return FALSE;
347
348 case ACTION_KIND_BARRIER:
349 /* Only allow a single barrier to be added. */
350 action_free (action: other);
351 return TRUE;
352
353 case ACTION_KIND_GROUP:
354 default:
355 g_return_val_if_reached (FALSE);
356 }
357}
358
359static void
360gtk_text_history_do_change_state (GtkTextHistory *self,
361 gboolean is_modified,
362 gboolean can_undo,
363 gboolean can_redo)
364{
365 g_assert (GTK_IS_TEXT_HISTORY (self));
366
367 self->funcs.change_state (self->funcs_data, is_modified, can_undo, can_redo);
368}
369
370static void
371gtk_text_history_do_insert (GtkTextHistory *self,
372 guint begin,
373 guint end,
374 const char *text,
375 guint len)
376{
377 g_assert (GTK_IS_TEXT_HISTORY (self));
378 g_assert (text != NULL);
379
380 uint_order (a: &begin, b: &end);
381
382 self->funcs.insert (self->funcs_data, begin, end, text, len);
383}
384
385static void
386gtk_text_history_do_delete (GtkTextHistory *self,
387 guint begin,
388 guint end,
389 const char *expected_text,
390 guint len)
391{
392 g_assert (GTK_IS_TEXT_HISTORY (self));
393
394 uint_order (a: &begin, b: &end);
395
396 self->funcs.delete (self->funcs_data, begin, end, expected_text, len);
397}
398
399static void
400gtk_text_history_do_select (GtkTextHistory *self,
401 guint selection_insert,
402 guint selection_bound)
403{
404 g_assert (GTK_IS_TEXT_HISTORY (self));
405
406 self->funcs.select (self->funcs_data, selection_insert, selection_bound);
407}
408
409static void
410gtk_text_history_truncate_one (GtkTextHistory *self)
411{
412 if (self->undo_queue.length > 0)
413 {
414 Action *action = g_queue_peek_head (queue: &self->undo_queue);
415 g_queue_unlink (queue: &self->undo_queue, link_: &action->link);
416 action_free (action);
417 }
418 else if (self->redo_queue.length > 0)
419 {
420 Action *action = g_queue_peek_tail (queue: &self->redo_queue);
421 g_queue_unlink (queue: &self->redo_queue, link_: &action->link);
422 action_free (action);
423 }
424 else
425 {
426 g_assert_not_reached ();
427 }
428}
429
430static void
431gtk_text_history_truncate (GtkTextHistory *self)
432{
433 g_assert (GTK_IS_TEXT_HISTORY (self));
434
435 if (self->max_undo_levels == 0)
436 return;
437
438 while (self->undo_queue.length + self->redo_queue.length > self->max_undo_levels)
439 gtk_text_history_truncate_one (self);
440}
441
442static void
443gtk_text_history_finalize (GObject *object)
444{
445 GtkTextHistory *self = (GtkTextHistory *)object;
446
447 clear_action_queue (queue: &self->undo_queue);
448 clear_action_queue (queue: &self->redo_queue);
449
450 G_OBJECT_CLASS (gtk_text_history_parent_class)->finalize (object);
451}
452
453static void
454gtk_text_history_class_init (GtkTextHistoryClass *klass)
455{
456 GObjectClass *object_class = G_OBJECT_CLASS (klass);
457
458 object_class->finalize = gtk_text_history_finalize;
459}
460
461static void
462gtk_text_history_init (GtkTextHistory *self)
463{
464 self->enabled = TRUE;
465 self->selection.insert = -1;
466 self->selection.bound = -1;
467}
468
469static gboolean
470has_actionable (const GQueue *queue)
471{
472 const GList *iter;
473
474 for (iter = queue->head; iter; iter = iter->next)
475 {
476 const Action *action = iter->data;
477
478 if (action->kind == ACTION_KIND_BARRIER)
479 continue;
480
481 if (action->kind == ACTION_KIND_GROUP)
482 {
483 if (has_actionable (queue: &action->u.group.actions))
484 return TRUE;
485 else
486 continue;
487 }
488
489 return TRUE;
490 }
491
492 return FALSE;
493}
494
495static void
496gtk_text_history_update_state (GtkTextHistory *self)
497{
498 g_assert (GTK_IS_TEXT_HISTORY (self));
499
500 if (self->irreversible || self->in_user)
501 {
502 self->can_undo = FALSE;
503 self->can_redo = FALSE;
504 }
505 else
506 {
507 self->can_undo = has_actionable (queue: &self->undo_queue);
508 self->can_redo = has_actionable (queue: &self->redo_queue);
509 }
510
511 gtk_text_history_do_change_state (self, is_modified: self->is_modified, can_undo: self->can_undo, can_redo: self->can_redo);
512}
513
514static void
515gtk_text_history_push (GtkTextHistory *self,
516 Action *action)
517{
518 Action *peek;
519 gboolean in_user_action;
520
521 g_assert (GTK_IS_TEXT_HISTORY (self));
522 g_assert (self->enabled);
523 g_assert (action != NULL);
524
525 while (self->redo_queue.length > 0)
526 {
527 peek = g_queue_peek_head (queue: &self->redo_queue);
528 g_queue_unlink (queue: &self->redo_queue, link_: &peek->link);
529 action_free (action: peek);
530 }
531
532 peek = g_queue_peek_tail (queue: &self->undo_queue);
533 in_user_action = self->in_user > 0;
534
535 if (peek == NULL || !action_chain (action: peek, other: action, in_user_action))
536 g_queue_push_tail_link (queue: &self->undo_queue, link_: &action->link);
537
538 gtk_text_history_truncate (self);
539 gtk_text_history_update_state (self);
540}
541
542GtkTextHistory *
543gtk_text_history_new (const GtkTextHistoryFuncs *funcs,
544 gpointer funcs_data)
545{
546 GtkTextHistory *self;
547
548 g_return_val_if_fail (funcs != NULL, NULL);
549
550 self = g_object_new (GTK_TYPE_TEXT_HISTORY, NULL);
551 self->funcs = *funcs;
552 self->funcs_data = funcs_data;
553
554 return g_steal_pointer (&self);
555}
556
557gboolean
558gtk_text_history_get_can_undo (GtkTextHistory *self)
559{
560 g_return_val_if_fail (GTK_IS_TEXT_HISTORY (self), FALSE);
561
562 return self->can_undo;
563}
564
565gboolean
566gtk_text_history_get_can_redo (GtkTextHistory *self)
567{
568 g_return_val_if_fail (GTK_IS_TEXT_HISTORY (self), FALSE);
569
570 return self->can_redo;
571}
572
573static void
574gtk_text_history_apply (GtkTextHistory *self,
575 Action *action,
576 Action *peek)
577{
578 g_assert (GTK_IS_TEXT_HISTORY (self));
579 g_assert (action != NULL);
580
581 switch (action->kind)
582 {
583 case ACTION_KIND_INSERT:
584 gtk_text_history_do_insert (self,
585 begin: action->u.insert.begin,
586 end: action->u.insert.end,
587 text: istring_str (str: &action->u.insert.istr),
588 len: action->u.insert.istr.n_bytes);
589
590 /* If the next item is a DELETE_SELECTION, then we want to
591 * pre-select the text for the user. Otherwise, just place
592 * the cursor were we think it was.
593 */
594 if (peek != NULL && peek->kind == ACTION_KIND_DELETE_SELECTION)
595 gtk_text_history_do_select (self,
596 selection_insert: peek->u.delete.begin,
597 selection_bound: peek->u.delete.end);
598 else
599 gtk_text_history_do_select (self,
600 selection_insert: action->u.insert.end,
601 selection_bound: action->u.insert.end);
602
603 break;
604
605 case ACTION_KIND_DELETE_BACKSPACE:
606 case ACTION_KIND_DELETE_KEY:
607 case ACTION_KIND_DELETE_PROGRAMMATIC:
608 case ACTION_KIND_DELETE_SELECTION:
609 gtk_text_history_do_delete (self,
610 begin: action->u.delete.begin,
611 end: action->u.delete.end,
612 expected_text: istring_str (str: &action->u.delete.istr),
613 len: action->u.delete.istr.n_bytes);
614 gtk_text_history_do_select (self,
615 selection_insert: action->u.delete.begin,
616 selection_bound: action->u.delete.begin);
617 break;
618
619 case ACTION_KIND_GROUP: {
620 const GList *actions = action->u.group.actions.head;
621
622 for (const GList *iter = actions; iter; iter = iter->next)
623 gtk_text_history_apply (self, action: iter->data, NULL);
624
625 break;
626 }
627
628 case ACTION_KIND_BARRIER:
629 break;
630
631 default:
632 g_assert_not_reached ();
633 }
634
635 if (action->is_modified_set)
636 self->is_modified = action->is_modified;
637}
638
639static void
640gtk_text_history_reverse (GtkTextHistory *self,
641 Action *action)
642{
643 g_assert (GTK_IS_TEXT_HISTORY (self));
644 g_assert (action != NULL);
645
646 switch (action->kind)
647 {
648 case ACTION_KIND_INSERT:
649 gtk_text_history_do_delete (self,
650 begin: action->u.insert.begin,
651 end: action->u.insert.end,
652 expected_text: istring_str (str: &action->u.insert.istr),
653 len: action->u.insert.istr.n_bytes);
654 gtk_text_history_do_select (self,
655 selection_insert: action->u.insert.begin,
656 selection_bound: action->u.insert.begin);
657 break;
658
659 case ACTION_KIND_DELETE_BACKSPACE:
660 case ACTION_KIND_DELETE_KEY:
661 case ACTION_KIND_DELETE_PROGRAMMATIC:
662 case ACTION_KIND_DELETE_SELECTION:
663 gtk_text_history_do_insert (self,
664 begin: action->u.delete.begin,
665 end: action->u.delete.end,
666 text: istring_str (str: &action->u.delete.istr),
667 len: action->u.delete.istr.n_bytes);
668 if (action->u.delete.selection.insert != -1 &&
669 action->u.delete.selection.bound != -1)
670 gtk_text_history_do_select (self,
671 selection_insert: action->u.delete.selection.insert,
672 selection_bound: action->u.delete.selection.bound);
673 else if (action->u.delete.selection.insert != -1)
674 gtk_text_history_do_select (self,
675 selection_insert: action->u.delete.selection.insert,
676 selection_bound: action->u.delete.selection.insert);
677 break;
678
679 case ACTION_KIND_GROUP: {
680 const GList *actions = action->u.group.actions.tail;
681
682 for (const GList *iter = actions; iter; iter = iter->prev)
683 gtk_text_history_reverse (self, action: iter->data);
684
685 break;
686 }
687
688 case ACTION_KIND_BARRIER:
689 break;
690
691 default:
692 g_assert_not_reached ();
693 }
694
695 if (action->is_modified_set)
696 self->is_modified = !action->is_modified;
697}
698
699static void
700move_barrier (GQueue *from_queue,
701 Action *action,
702 GQueue *to_queue,
703 gboolean head)
704{
705 g_queue_unlink (queue: from_queue, link_: &action->link);
706
707 if (head)
708 g_queue_push_head_link (queue: to_queue, link_: &action->link);
709 else
710 g_queue_push_tail_link (queue: to_queue, link_: &action->link);
711}
712
713void
714gtk_text_history_undo (GtkTextHistory *self)
715{
716 g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
717
718 return_if_not_enabled (self);
719 return_if_applying (self);
720 return_if_irreversible (self);
721
722 if (gtk_text_history_get_can_undo (self))
723 {
724 Action *action;
725
726 self->applying = TRUE;
727
728 action = g_queue_peek_tail (queue: &self->undo_queue);
729
730 if (action->kind == ACTION_KIND_BARRIER)
731 {
732 move_barrier (from_queue: &self->undo_queue, action, to_queue: &self->redo_queue, TRUE);
733 action = g_queue_peek_tail (queue: &self->undo_queue);
734 }
735
736 g_queue_unlink (queue: &self->undo_queue, link_: &action->link);
737 g_queue_push_head_link (queue: &self->redo_queue, link_: &action->link);
738 gtk_text_history_reverse (self, action);
739 gtk_text_history_update_state (self);
740
741 self->applying = FALSE;
742 }
743}
744
745void
746gtk_text_history_redo (GtkTextHistory *self)
747{
748 g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
749
750 return_if_not_enabled (self);
751 return_if_applying (self);
752 return_if_irreversible (self);
753
754 if (gtk_text_history_get_can_redo (self))
755 {
756 Action *action;
757 Action *peek;
758
759 self->applying = TRUE;
760
761 action = g_queue_peek_head (queue: &self->redo_queue);
762
763 if (action->kind == ACTION_KIND_BARRIER)
764 {
765 move_barrier (from_queue: &self->redo_queue, action, to_queue: &self->undo_queue, FALSE);
766 action = g_queue_peek_head (queue: &self->redo_queue);
767 }
768
769 g_queue_unlink (queue: &self->redo_queue, link_: &action->link);
770 g_queue_push_tail_link (queue: &self->undo_queue, link_: &action->link);
771
772 peek = g_queue_peek_head (queue: &self->redo_queue);
773
774 gtk_text_history_apply (self, action, peek);
775 gtk_text_history_update_state (self);
776
777 self->applying = FALSE;
778 }
779}
780
781void
782gtk_text_history_begin_user_action (GtkTextHistory *self)
783{
784 Action *group;
785
786 g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
787
788 return_if_not_enabled (self);
789 return_if_applying (self);
790 return_if_irreversible (self);
791
792 self->in_user++;
793
794 group = g_queue_peek_tail (queue: &self->undo_queue);
795
796 if (group == NULL || group->kind != ACTION_KIND_GROUP)
797 {
798 group = action_new (kind: ACTION_KIND_GROUP);
799 gtk_text_history_push (self, action: group);
800 }
801
802 group->u.group.depth++;
803
804 gtk_text_history_update_state (self);
805}
806
807void
808gtk_text_history_end_user_action (GtkTextHistory *self)
809{
810 Action *peek;
811
812 g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
813
814 return_if_not_enabled (self);
815 return_if_applying (self);
816 return_if_irreversible (self);
817
818 clear_action_queue (queue: &self->redo_queue);
819
820 peek = g_queue_peek_tail (queue: &self->undo_queue);
821
822 if (peek->kind != ACTION_KIND_GROUP)
823 {
824 g_warning ("miss-matched %s end_user_action. Expected group, got %d",
825 G_OBJECT_TYPE_NAME (self),
826 peek->kind);
827 return;
828 }
829
830 self->in_user--;
831 peek->u.group.depth--;
832
833 /* Unless this is the last user action, short-circuit */
834 if (peek->u.group.depth > 0)
835 return;
836
837 /* Unlikely, but if the group is empty, just remove it */
838 if (action_group_is_empty (action: peek))
839 {
840 g_queue_unlink (queue: &self->undo_queue, link_: &peek->link);
841 action_free (action: peek);
842 goto update_state;
843 }
844
845 /* If there is a single item within the group, we can hoist
846 * it up increasing the chances that we can join actions.
847 */
848 if (peek->u.group.actions.length == 1)
849 {
850 GList *link_ = peek->u.group.actions.head;
851 Action *replaced = link_->data;
852
853 replaced->is_modified = peek->is_modified;
854 replaced->is_modified_set = peek->is_modified_set;
855
856 g_queue_unlink (queue: &peek->u.group.actions, link_);
857 g_queue_unlink (queue: &self->undo_queue, link_: &peek->link);
858 action_free (action: peek);
859
860 gtk_text_history_push (self, action: replaced);
861
862 goto update_state;
863 }
864
865 /* Now insert a barrier action so we don't allow
866 * joining items to this node in the future.
867 */
868 gtk_text_history_push (self, action: action_new (kind: ACTION_KIND_BARRIER));
869
870update_state:
871 gtk_text_history_update_state (self);
872}
873
874void
875gtk_text_history_begin_irreversible_action (GtkTextHistory *self)
876{
877 g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
878
879 return_if_not_enabled (self);
880 return_if_applying (self);
881
882 if (self->in_user)
883 {
884 g_warning ("Cannot begin irreversible action while in user action");
885 return;
886 }
887
888 self->irreversible++;
889
890 clear_action_queue (queue: &self->undo_queue);
891 clear_action_queue (queue: &self->redo_queue);
892
893 gtk_text_history_update_state (self);
894}
895
896void
897gtk_text_history_end_irreversible_action (GtkTextHistory *self)
898{
899 g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
900
901 return_if_not_enabled (self);
902 return_if_applying (self);
903
904 if (self->in_user)
905 {
906 g_warning ("Cannot end irreversible action while in user action");
907 return;
908 }
909
910 self->irreversible--;
911
912 clear_action_queue (queue: &self->undo_queue);
913 clear_action_queue (queue: &self->redo_queue);
914
915 gtk_text_history_update_state (self);
916}
917
918static void
919gtk_text_history_clear_modified (GtkTextHistory *self)
920{
921 const GList *iter;
922
923 for (iter = self->undo_queue.head; iter; iter = iter->next)
924 {
925 Action *action = iter->data;
926
927 action->is_modified = FALSE;
928 action->is_modified_set = FALSE;
929 }
930
931 for (iter = self->redo_queue.head; iter; iter = iter->next)
932 {
933 Action *action = iter->data;
934
935 action->is_modified = FALSE;
936 action->is_modified_set = FALSE;
937 }
938}
939
940void
941gtk_text_history_modified_changed (GtkTextHistory *self,
942 gboolean modified)
943{
944 Action *peek;
945
946 g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
947
948 return_if_not_enabled (self);
949 return_if_applying (self);
950 return_if_irreversible (self);
951
952 /* If we have a new save point, clear all previous modified states. */
953 gtk_text_history_clear_modified (self);
954
955 if ((peek = g_queue_peek_tail (queue: &self->undo_queue)))
956 {
957 if (peek->kind == ACTION_KIND_BARRIER)
958 {
959 if (!(peek = peek->link.prev->data))
960 return;
961 }
962
963 peek->is_modified = !!modified;
964 peek->is_modified_set = TRUE;
965 }
966
967 self->is_modified = !!modified;
968 self->is_modified_set = TRUE;
969
970 gtk_text_history_update_state (self);
971}
972
973void
974gtk_text_history_selection_changed (GtkTextHistory *self,
975 int selection_insert,
976 int selection_bound)
977{
978 g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
979
980 return_if_not_enabled (self);
981 return_if_applying (self);
982 return_if_irreversible (self);
983
984 self->selection.insert = CLAMP (selection_insert, -1, G_MAXINT);
985 self->selection.bound = CLAMP (selection_bound, -1, G_MAXINT);
986}
987
988void
989gtk_text_history_text_inserted (GtkTextHistory *self,
990 guint position,
991 const char *text,
992 int len)
993{
994 Action *action;
995 guint n_chars;
996
997 g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
998
999 return_if_not_enabled (self);
1000 return_if_applying (self);
1001 return_if_irreversible (self);
1002
1003 if (len < 0)
1004 len = strlen (s: text);
1005 n_chars = g_utf8_strlen (p: text, max: len);
1006
1007 action = action_new (kind: ACTION_KIND_INSERT);
1008 action->u.insert.begin = position;
1009 action->u.insert.end = position + n_chars;
1010 istring_set (str: &action->u.insert.istr, text, n_bytes: len, n_chars);
1011
1012 gtk_text_history_push (self, action);
1013}
1014
1015void
1016gtk_text_history_text_deleted (GtkTextHistory *self,
1017 guint begin,
1018 guint end,
1019 const char *text,
1020 int len)
1021{
1022 Action *action;
1023 ActionKind kind;
1024
1025 g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
1026
1027 return_if_not_enabled (self);
1028 return_if_applying (self);
1029 return_if_irreversible (self);
1030
1031 if (len < 0)
1032 len = strlen (s: text);
1033
1034 if (self->selection.insert == -1 && self->selection.bound == -1)
1035 kind = ACTION_KIND_DELETE_PROGRAMMATIC;
1036 else if (self->selection.insert == end && self->selection.bound == -1)
1037 kind = ACTION_KIND_DELETE_BACKSPACE;
1038 else if (self->selection.insert == begin && self->selection.bound == -1)
1039 kind = ACTION_KIND_DELETE_KEY;
1040 else
1041 kind = ACTION_KIND_DELETE_SELECTION;
1042
1043 action = action_new (kind);
1044 action->u.delete.begin = begin;
1045 action->u.delete.end = end;
1046 action->u.delete.selection.insert = self->selection.insert;
1047 action->u.delete.selection.bound = self->selection.bound;
1048 istring_set (str: &action->u.delete.istr, text, n_bytes: len, ABS (end - begin));
1049
1050 gtk_text_history_push (self, action);
1051}
1052
1053gboolean
1054gtk_text_history_get_enabled (GtkTextHistory *self)
1055{
1056 g_return_val_if_fail (GTK_IS_TEXT_HISTORY (self), FALSE);
1057
1058 return self->enabled;
1059}
1060
1061void
1062gtk_text_history_set_enabled (GtkTextHistory *self,
1063 gboolean enabled)
1064{
1065 g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
1066
1067 enabled = !!enabled;
1068
1069 if (self->enabled != enabled)
1070 {
1071 self->enabled = enabled;
1072
1073 if (!self->enabled)
1074 {
1075 self->irreversible = 0;
1076 self->in_user = 0;
1077 clear_action_queue (queue: &self->undo_queue);
1078 clear_action_queue (queue: &self->redo_queue);
1079 }
1080
1081 gtk_text_history_update_state (self);
1082 }
1083}
1084
1085guint
1086gtk_text_history_get_max_undo_levels (GtkTextHistory *self)
1087{
1088 g_return_val_if_fail (GTK_IS_TEXT_HISTORY (self), 0);
1089
1090 return self->max_undo_levels;
1091}
1092
1093void
1094gtk_text_history_set_max_undo_levels (GtkTextHistory *self,
1095 guint max_undo_levels)
1096{
1097 g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
1098
1099 if (self->max_undo_levels != max_undo_levels)
1100 {
1101 self->max_undo_levels = max_undo_levels;
1102 gtk_text_history_truncate (self);
1103 }
1104}
1105

source code of gtk/gtk/gtktexthistory.c