1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
#![doc(html_favicon_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo-icon.png")]
#![doc(html_logo_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo.png")]
//!
//! Undo properties.
//!
//! # Crate
//!
#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
#![warn(unused_extern_crates)]
#![warn(missing_docs)]

use std::time::Duration;

use zng_ext_undo::*;
use zng_wgt::prelude::*;

/// Sets if the widget is an undo scope.
///
/// If `true` the widget will handle [`UNDO_CMD`] and [`REDO_CMD`] for all undo actions
/// that happen inside it.
///
/// [`UNDO_CMD`]: static@zng_ext_undo::UNDO_CMD
/// [`REDO_CMD`]: static@zng_ext_undo::REDO_CMD
#[property(CONTEXT - 10, default(false))]
pub fn undo_scope(child: impl UiNode, is_scope: impl IntoVar<bool>) -> impl UiNode {
    let mut scope = WidgetUndoScope::new();
    let mut undo_cmd = CommandHandle::dummy();
    let mut redo_cmd = CommandHandle::dummy();
    let mut clear_cmd = CommandHandle::dummy();
    let is_scope = is_scope.into_var();
    match_node(child, move |c, mut op| {
        match &mut op {
            UiNodeOp::Init => {
                WIDGET.sub_var(&is_scope);

                if !is_scope.get() {
                    return; // default handling without scope context.
                }

                scope.init();

                let id = WIDGET.id();
                undo_cmd = UNDO_CMD.scoped(id).subscribe(false);
                redo_cmd = REDO_CMD.scoped(id).subscribe(false);
                clear_cmd = CLEAR_HISTORY_CMD.scoped(id).subscribe(false);
            }
            UiNodeOp::Deinit => {
                if !is_scope.get() {
                    return;
                }

                UNDO.with_scope(&mut scope, || c.deinit());
                scope.deinit();
                undo_cmd = CommandHandle::dummy();
                redo_cmd = CommandHandle::dummy();
                return;
            }
            UiNodeOp::Info { info } => {
                if !is_scope.get() {
                    return;
                }
                scope.info(info);
            }
            UiNodeOp::Event { update } => {
                if !is_scope.get() {
                    return;
                }

                let id = WIDGET.id();
                if let Some(args) = UNDO_CMD.scoped(id).on_unhandled(update) {
                    args.propagation().stop();
                    UNDO.with_scope(&mut scope, || {
                        if let Some(&n) = args.param::<u32>() {
                            UNDO.undo_select(n);
                        } else if let Some(&i) = args.param::<Duration>() {
                            UNDO.undo_select(i);
                        } else if let Some(&t) = args.param::<DInstant>() {
                            UNDO.undo_select(t);
                        } else {
                            UNDO.undo();
                        }
                    });
                } else if let Some(args) = REDO_CMD.scoped(id).on_unhandled(update) {
                    args.propagation().stop();
                    UNDO.with_scope(&mut scope, || {
                        if let Some(&n) = args.param::<u32>() {
                            UNDO.redo_select(n);
                        } else if let Some(&i) = args.param::<Duration>() {
                            UNDO.redo_select(i);
                        } else if let Some(&t) = args.param::<DInstant>() {
                            UNDO.redo_select(t);
                        } else {
                            UNDO.redo();
                        }
                    });
                } else if let Some(args) = CLEAR_HISTORY_CMD.scoped(id).on_unhandled(update) {
                    args.propagation().stop();
                    UNDO.with_scope(&mut scope, || {
                        UNDO.clear();
                    });
                }
            }
            UiNodeOp::Update { .. } => {
                if let Some(is_scope) = is_scope.get_new() {
                    WIDGET.info();

                    if is_scope {
                        if !scope.is_inited() {
                            scope.init();

                            let id = WIDGET.id();
                            undo_cmd = UNDO_CMD.scoped(id).subscribe(false);
                            redo_cmd = REDO_CMD.scoped(id).subscribe(false);
                        }
                    } else if scope.is_inited() {
                        scope.deinit();
                        undo_cmd = CommandHandle::dummy();
                        redo_cmd = CommandHandle::dummy();
                    }
                }
                if !is_scope.get() {
                    return;
                }
            }
            _ => {
                if !is_scope.get() {
                    return;
                }
            }
        }

        UNDO.with_scope(&mut scope, || c.op(op));

        let can_undo = scope.can_undo();
        let can_redo = scope.can_redo();
        undo_cmd.set_enabled(can_undo);
        redo_cmd.set_enabled(can_redo);
        clear_cmd.set_enabled(can_undo || can_redo);
    })
}

/// Enable or disable undo inside the widget.
#[property(CONTEXT, default(true))]
pub fn undo_enabled(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
    let enabled = enabled.into_var();
    match_node(child, move |c, op| {
        if !enabled.get() {
            UNDO.with_disabled(|| c.op(op))
        }
    })
}

/// Sets the maximum length for undo/redo stacks in the widget and descendants.
///
/// This property sets the [`UNDO_LIMIT_VAR`].
///
/// [`UNDO_LIMIT_VAR`]: zng_ext_undo::UNDO_LIMIT_VAR
#[property(CONTEXT - 11, default(UNDO_LIMIT_VAR))]
pub fn undo_limit(child: impl UiNode, max: impl IntoVar<u32>) -> impl UiNode {
    with_context_var(child, UNDO_LIMIT_VAR, max)
}

/// Sets the time interval that undo and redo cover each call for undo handlers in the widget and descendants.
///
/// When undo is requested inside the context all actions after the latest that are within `interval` of the
/// previous are undone.
///
/// This property sets the [`UNDO_INTERVAL_VAR`].
///
/// [`UNDO_INTERVAL_VAR`]: zng_ext_undo::UNDO_INTERVAL_VAR
#[property(CONTEXT - 11, default(UNDO_INTERVAL_VAR))]
pub fn undo_interval(child: impl UiNode, interval: impl IntoVar<Duration>) -> impl UiNode {
    with_context_var(child, UNDO_INTERVAL_VAR, interval)
}

/// Undo scope widget mixin.
///
/// Widget is an undo/redo scope, it tracks changes and handles undo/redo commands.
///
/// You can force the widget to use a parent undo scope by setting [`undo_scope`] to `false`, this will cause the widget
/// to start registering undo/redo actions in the parent, note that the widget will continue behaving as if it
/// owns the scope, so it may clear it.
///
/// [`undo_scope`]: fn@undo_scope
#[widget_mixin]
pub struct UndoMix<P>(P);

impl<P: WidgetImpl> UndoMix<P> {
    fn widget_intrinsic(&mut self) {
        widget_set! {
            self;
            crate::undo_scope = true;
        }
    }

    widget_impl! {
        /// If the widget can register undo actions.
        ///
        /// Is `true` by default in this widget, if set to `false` disables undo in the widget.
        pub undo_enabled(enabled: impl IntoVar<bool>);

        /// Sets the maximum number of undo/redo actions that are retained in the widget.
        pub undo_limit(limit: impl IntoVar<u32>);

        /// Sets the time interval that undo and redo cover each call for undo handlers in the widget and descendants.
        ///
        /// When undo is requested inside the context all actions after the latest that are within `interval` of the
        /// previous are undone.
        pub undo_interval(interval: impl IntoVar<Duration>);
    }
}