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 | |
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 |
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, link_: &action->link); |
151 | action_free (action); |
152 | } |
153 | } |
154 | |
155 | static Action * |
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 |
168 | action_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 | |
183 | static gboolean |
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 (action: child)) |
198 | continue; |
199 | |
200 | return FALSE; |
201 | } |
202 | |
203 | return TRUE; |
204 | } |
205 | |
206 | static gboolean |
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 (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 | |
359 | static void |
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 |
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 (a: &begin, b: &end); |
381 | |
382 | self->funcs.insert (self->funcs_data, begin, end, text, len); |
383 | } |
384 | |
385 | static void |
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 (a: &begin, b: &end); |
395 | |
396 | self->funcs.delete (self->funcs_data, begin, end, expected_text, len); |
397 | } |
398 | |
399 | static void |
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 |
410 | gtk_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 | |
430 | static void |
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 |
443 | gtk_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 | |
453 | static void |
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 |
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 |
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 (queue: &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 |
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 (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 | |
514 | static void |
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 (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 | |
542 | GtkTextHistory * |
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 |
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 |
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 |
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 | 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 | |
639 | static void |
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 | 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 | |
699 | static void |
700 | move_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 | |
713 | void |
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 (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 | |
745 | void |
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 (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 | |
781 | void |
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 (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 | |
807 | void |
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 (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 | |
870 | update_state: |
871 | gtk_text_history_update_state (self); |
872 | } |
873 | |
874 | void |
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 (queue: &self->undo_queue); |
891 | clear_action_queue (queue: &self->redo_queue); |
892 | |
893 | gtk_text_history_update_state (self); |
894 | } |
895 | |
896 | void |
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 (queue: &self->undo_queue); |
913 | clear_action_queue (queue: &self->redo_queue); |
914 | |
915 | gtk_text_history_update_state (self); |
916 | } |
917 | |
918 | static void |
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 |
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 (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 | |
973 | void |
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 | self->selection.insert = CLAMP (selection_insert, -1, G_MAXINT); |
985 | self->selection.bound = CLAMP (selection_bound, -1, G_MAXINT); |
986 | } |
987 | |
988 | void |
989 | gtk_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 | |
1015 | void |
1016 | gtk_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 | |
1053 | gboolean |
1054 | gtk_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 | |
1061 | void |
1062 | gtk_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 | |
1085 | guint |
1086 | gtk_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 | |
1093 | void |
1094 | gtk_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 | |