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
41 typedef struct _Action Action;
42 typedef enum _ActionKind ActionKind;
43
44 enum _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
55 struct _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
83 struct _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
110 static void action_free (Action *action);
111
G_DEFINE_TYPE(GtkTextHistory,gtk_text_history,G_TYPE_OBJECT)112 G_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
130 static inline void
131 uint_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
142 static void
clear_action_queue(GQueue * queue)143 clear_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, &action->link);
151 action_free (action);
152 }
153 }
154
155 static Action *
action_new(ActionKind kind)156 action_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
167 static void
action_free(Action * action)168 action_free (Action *action)
169 {
170 if (action->kind == ACTION_KIND_INSERT)
171 istring_clear (&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 (&action->u.delete.istr);
177 else if (action->kind == ACTION_KIND_GROUP)
178 clear_action_queue (&action->u.group.actions);
179
180 g_slice_free (Action, action);
181 }
182
183 static gboolean
action_group_is_empty(const Action * action)184 action_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 (child))
198 continue;
199
200 return FALSE;
201 }
202
203 return TRUE;
204 }
205
206 static gboolean
action_chain(Action * action,Action * other,gboolean in_user_action)207 action_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 (&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 (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 (tail, other, in_user_action))
245 return TRUE;
246 }
247
248 g_queue_push_tail_link (&action->u.group.actions, &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 (&action->u.insert.istr, '\n') ||
274 istring_contains_unichar (&other->u.insert.istr, '\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 (&action->u.insert.istr) ||
282 istring_ends_with_space (&action->u.insert.istr)) &&
283 istring_only_contains_space (&other->u.insert.istr))
284 goto do_chain;
285
286 /* Starting a new word, don't chain this */
287 if (istring_starts_with_space (&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 (&other->u.insert.istr))
295 return FALSE;
296 }
297
298 do_chain:
299
300 istring_append (&action->u.insert.istr, &other->u.insert.istr);
301 action->u.insert.end += other->u.insert.end - other->u.insert.begin;
302 action_free (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 (&action->u.delete.istr,
325 &other->u.delete.istr);
326 action->u.delete.begin = other->u.delete.begin;
327 action_free (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 (&other->u.delete.istr) ||
337 istring_only_contains_space (&action->u.delete.istr))
338 {
339 istring_append (&action->u.delete.istr, &other->u.delete.istr);
340 action->u.delete.end += other->u.delete.istr.n_chars;
341 action_free (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 (other);
351 return TRUE;
352
353 case ACTION_KIND_GROUP:
354 default:
355 g_return_val_if_reached (FALSE);
356 }
357 }
358
359 static void
gtk_text_history_do_change_state(GtkTextHistory * self,gboolean is_modified,gboolean can_undo,gboolean can_redo)360 gtk_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
370 static void
gtk_text_history_do_insert(GtkTextHistory * self,guint begin,guint end,const char * text,guint len)371 gtk_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 (&begin, &end);
381
382 self->funcs.insert (self->funcs_data, begin, end, text, len);
383 }
384
385 static void
gtk_text_history_do_delete(GtkTextHistory * self,guint begin,guint end,const char * expected_text,guint len)386 gtk_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 (&begin, &end);
395
396 self->funcs.delete (self->funcs_data, begin, end, expected_text, len);
397 }
398
399 static void
gtk_text_history_do_select(GtkTextHistory * self,guint selection_insert,guint selection_bound)400 gtk_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
409 static void
gtk_text_history_truncate_one(GtkTextHistory * self)410 gtk_text_history_truncate_one (GtkTextHistory *self)
411 {
412 if (self->undo_queue.length > 0)
413 {
414 Action *action = g_queue_peek_head (&self->undo_queue);
415 g_queue_unlink (&self->undo_queue, &action->link);
416 action_free (action);
417 }
418 else if (self->redo_queue.length > 0)
419 {
420 Action *action = g_queue_peek_tail (&self->redo_queue);
421 g_queue_unlink (&self->redo_queue, &action->link);
422 action_free (action);
423 }
424 else
425 {
426 g_assert_not_reached ();
427 }
428 }
429
430 static void
gtk_text_history_truncate(GtkTextHistory * self)431 gtk_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
442 static void
gtk_text_history_finalize(GObject * object)443 gtk_text_history_finalize (GObject *object)
444 {
445 GtkTextHistory *self = (GtkTextHistory *)object;
446
447 clear_action_queue (&self->undo_queue);
448 clear_action_queue (&self->redo_queue);
449
450 G_OBJECT_CLASS (gtk_text_history_parent_class)->finalize (object);
451 }
452
453 static void
gtk_text_history_class_init(GtkTextHistoryClass * klass)454 gtk_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
461 static void
gtk_text_history_init(GtkTextHistory * self)462 gtk_text_history_init (GtkTextHistory *self)
463 {
464 self->enabled = TRUE;
465 self->selection.insert = -1;
466 self->selection.bound = -1;
467 }
468
469 static gboolean
has_actionable(const GQueue * queue)470 has_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 (&action->u.group.actions))
484 return TRUE;
485 else
486 continue;
487 }
488
489 return TRUE;
490 }
491
492 return FALSE;
493 }
494
495 static void
gtk_text_history_update_state(GtkTextHistory * self)496 gtk_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 (&self->undo_queue);
508 self->can_redo = has_actionable (&self->redo_queue);
509 }
510
511 gtk_text_history_do_change_state (self, self->is_modified, self->can_undo, self->can_redo);
512 }
513
514 static void
gtk_text_history_push(GtkTextHistory * self,Action * action)515 gtk_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 (&self->redo_queue);
528 g_queue_unlink (&self->redo_queue, &peek->link);
529 action_free (peek);
530 }
531
532 peek = g_queue_peek_tail (&self->undo_queue);
533 in_user_action = self->in_user > 0;
534
535 if (peek == NULL || !action_chain (peek, action, in_user_action))
536 g_queue_push_tail_link (&self->undo_queue, &action->link);
537
538 gtk_text_history_truncate (self);
539 gtk_text_history_update_state (self);
540 }
541
542 GtkTextHistory *
gtk_text_history_new(const GtkTextHistoryFuncs * funcs,gpointer funcs_data)543 gtk_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
557 gboolean
gtk_text_history_get_can_undo(GtkTextHistory * self)558 gtk_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
565 gboolean
gtk_text_history_get_can_redo(GtkTextHistory * self)566 gtk_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
573 static void
gtk_text_history_apply(GtkTextHistory * self,Action * action,Action * peek)574 gtk_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 action->u.insert.begin,
586 action->u.insert.end,
587 istring_str (&action->u.insert.istr),
588 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 peek->u.delete.begin,
597 peek->u.delete.end);
598 else
599 gtk_text_history_do_select (self,
600 action->u.insert.end,
601 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 action->u.delete.begin,
611 action->u.delete.end,
612 istring_str (&action->u.delete.istr),
613 action->u.delete.istr.n_bytes);
614 gtk_text_history_do_select (self,
615 action->u.delete.begin,
616 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, 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
639 static void
gtk_text_history_reverse(GtkTextHistory * self,Action * action)640 gtk_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 action->u.insert.begin,
651 action->u.insert.end,
652 istring_str (&action->u.insert.istr),
653 action->u.insert.istr.n_bytes);
654 gtk_text_history_do_select (self,
655 action->u.insert.begin,
656 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 action->u.delete.begin,
665 action->u.delete.end,
666 istring_str (&action->u.delete.istr),
667 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 action->u.delete.selection.insert,
672 action->u.delete.selection.bound);
673 else if (action->u.delete.selection.insert != -1)
674 gtk_text_history_do_select (self,
675 action->u.delete.selection.insert,
676 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, 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
699 static void
move_barrier(GQueue * from_queue,Action * action,GQueue * to_queue,gboolean head)700 move_barrier (GQueue *from_queue,
701 Action *action,
702 GQueue *to_queue,
703 gboolean head)
704 {
705 g_queue_unlink (from_queue, &action->link);
706
707 if (head)
708 g_queue_push_head_link (to_queue, &action->link);
709 else
710 g_queue_push_tail_link (to_queue, &action->link);
711 }
712
713 void
gtk_text_history_undo(GtkTextHistory * self)714 gtk_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 (&self->undo_queue);
729
730 if (action->kind == ACTION_KIND_BARRIER)
731 {
732 move_barrier (&self->undo_queue, action, &self->redo_queue, TRUE);
733 action = g_queue_peek_tail (&self->undo_queue);
734 }
735
736 g_queue_unlink (&self->undo_queue, &action->link);
737 g_queue_push_head_link (&self->redo_queue, &action->link);
738 gtk_text_history_reverse (self, action);
739 gtk_text_history_update_state (self);
740
741 self->applying = FALSE;
742 }
743 }
744
745 void
gtk_text_history_redo(GtkTextHistory * self)746 gtk_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 (&self->redo_queue);
762
763 if (action->kind == ACTION_KIND_BARRIER)
764 {
765 move_barrier (&self->redo_queue, action, &self->undo_queue, FALSE);
766 action = g_queue_peek_head (&self->redo_queue);
767 }
768
769 g_queue_unlink (&self->redo_queue, &action->link);
770 g_queue_push_tail_link (&self->undo_queue, &action->link);
771
772 peek = g_queue_peek_head (&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
781 void
gtk_text_history_begin_user_action(GtkTextHistory * self)782 gtk_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 (&self->undo_queue);
795
796 if (group == NULL || group->kind != ACTION_KIND_GROUP)
797 {
798 group = action_new (ACTION_KIND_GROUP);
799 gtk_text_history_push (self, group);
800 }
801
802 group->u.group.depth++;
803
804 gtk_text_history_update_state (self);
805 }
806
807 void
gtk_text_history_end_user_action(GtkTextHistory * self)808 gtk_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 (&self->redo_queue);
819
820 peek = g_queue_peek_tail (&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 (peek))
839 {
840 g_queue_unlink (&self->undo_queue, &peek->link);
841 action_free (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 (&peek->u.group.actions, link_);
857 g_queue_unlink (&self->undo_queue, &peek->link);
858 action_free (peek);
859
860 gtk_text_history_push (self, 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_new (ACTION_KIND_BARRIER));
869
870 update_state:
871 gtk_text_history_update_state (self);
872 }
873
874 void
gtk_text_history_begin_irreversible_action(GtkTextHistory * self)875 gtk_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 (&self->undo_queue);
891 clear_action_queue (&self->redo_queue);
892
893 gtk_text_history_update_state (self);
894 }
895
896 void
gtk_text_history_end_irreversible_action(GtkTextHistory * self)897 gtk_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 (&self->undo_queue);
913 clear_action_queue (&self->redo_queue);
914
915 gtk_text_history_update_state (self);
916 }
917
918 static void
gtk_text_history_clear_modified(GtkTextHistory * self)919 gtk_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
940 void
gtk_text_history_modified_changed(GtkTextHistory * self,gboolean modified)941 gtk_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 (&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
973 void
gtk_text_history_selection_changed(GtkTextHistory * self,int selection_insert,int selection_bound)974 gtk_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 if (self->in_user == 0 && self->irreversible == 0)
985 {
986 self->selection.insert = CLAMP (selection_insert, -1, G_MAXINT);
987 self->selection.bound = CLAMP (selection_bound, -1, G_MAXINT);
988 }
989 }
990
991 void
gtk_text_history_text_inserted(GtkTextHistory * self,guint position,const char * text,int len)992 gtk_text_history_text_inserted (GtkTextHistory *self,
993 guint position,
994 const char *text,
995 int len)
996 {
997 Action *action;
998 guint n_chars;
999
1000 g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
1001
1002 return_if_not_enabled (self);
1003 return_if_applying (self);
1004 return_if_irreversible (self);
1005
1006 if (len < 0)
1007 len = strlen (text);
1008 n_chars = g_utf8_strlen (text, len);
1009
1010 action = action_new (ACTION_KIND_INSERT);
1011 action->u.insert.begin = position;
1012 action->u.insert.end = position + n_chars;
1013 istring_set (&action->u.insert.istr, text, len, n_chars);
1014
1015 gtk_text_history_push (self, action);
1016 }
1017
1018 void
gtk_text_history_text_deleted(GtkTextHistory * self,guint begin,guint end,const char * text,int len)1019 gtk_text_history_text_deleted (GtkTextHistory *self,
1020 guint begin,
1021 guint end,
1022 const char *text,
1023 int len)
1024 {
1025 Action *action;
1026 ActionKind kind;
1027
1028 g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
1029
1030 return_if_not_enabled (self);
1031 return_if_applying (self);
1032 return_if_irreversible (self);
1033
1034 if (len < 0)
1035 len = strlen (text);
1036
1037 if (self->selection.insert == -1 && self->selection.bound == -1)
1038 kind = ACTION_KIND_DELETE_PROGRAMMATIC;
1039 else if (self->selection.insert == end && self->selection.bound == -1)
1040 kind = ACTION_KIND_DELETE_BACKSPACE;
1041 else if (self->selection.insert == begin && self->selection.bound == -1)
1042 kind = ACTION_KIND_DELETE_KEY;
1043 else
1044 kind = ACTION_KIND_DELETE_SELECTION;
1045
1046 action = action_new (kind);
1047 action->u.delete.begin = begin;
1048 action->u.delete.end = end;
1049 action->u.delete.selection.insert = self->selection.insert;
1050 action->u.delete.selection.bound = self->selection.bound;
1051 istring_set (&action->u.delete.istr, text, len, ABS (end - begin));
1052
1053 gtk_text_history_push (self, action);
1054 }
1055
1056 gboolean
gtk_text_history_get_enabled(GtkTextHistory * self)1057 gtk_text_history_get_enabled (GtkTextHistory *self)
1058 {
1059 g_return_val_if_fail (GTK_IS_TEXT_HISTORY (self), FALSE);
1060
1061 return self->enabled;
1062 }
1063
1064 void
gtk_text_history_set_enabled(GtkTextHistory * self,gboolean enabled)1065 gtk_text_history_set_enabled (GtkTextHistory *self,
1066 gboolean enabled)
1067 {
1068 g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
1069
1070 enabled = !!enabled;
1071
1072 if (self->enabled != enabled)
1073 {
1074 self->enabled = enabled;
1075
1076 if (!self->enabled)
1077 {
1078 self->irreversible = 0;
1079 self->in_user = 0;
1080 clear_action_queue (&self->undo_queue);
1081 clear_action_queue (&self->redo_queue);
1082 }
1083
1084 gtk_text_history_update_state (self);
1085 }
1086 }
1087
1088 guint
gtk_text_history_get_max_undo_levels(GtkTextHistory * self)1089 gtk_text_history_get_max_undo_levels (GtkTextHistory *self)
1090 {
1091 g_return_val_if_fail (GTK_IS_TEXT_HISTORY (self), 0);
1092
1093 return self->max_undo_levels;
1094 }
1095
1096 void
gtk_text_history_set_max_undo_levels(GtkTextHistory * self,guint max_undo_levels)1097 gtk_text_history_set_max_undo_levels (GtkTextHistory *self,
1098 guint max_undo_levels)
1099 {
1100 g_return_if_fail (GTK_IS_TEXT_HISTORY (self));
1101
1102 if (self->max_undo_levels != max_undo_levels)
1103 {
1104 self->max_undo_levels = max_undo_levels;
1105 gtk_text_history_truncate (self);
1106 }
1107 }
1108