Phi: nicer scroll animation for METAR widget
[fg:fgdata.git] / Nasal / prop_key_handler.nas
1 # Property Key Handler
2 # ------------------------------------------------------------------
3 # This is an extension mainly targeted at developers. It implements
4 # some useful tools for dealing with internal properties if enabled
5 # (Menu->Debug->Configure Development Extensions). To use this feature,
6 # press the '/'-key, then type a property path (using the <TAB> key to
7 # complete property path elements if possible), or a search string ...
8 #
9 #
10 # Commands:
11 #
12 #   <property>=<value><CR> -> set property to value
13 #   <property><CR>         -> print property and value to screen and terminal
14 #   <property>*            -> print property and all children to terminal
15 #   <property>!            -> add property to display list  (reset list with  /!)
16 #   <property>:            -> open property browser in this property's directory
17 #   <string>?              -> print all properties whose path or value contains this string
18 #
19 #
20 # Keys:
21 #
22 #   <CR>              ... carriage return or enter, to confirm some operations
23 #   <TAB>             ... complete property path element (if possible), or
24 #                         cycle through available elements
25 #   <Shift-TAB>       ... like <TAB> but cycles backwards
26 #   <CurUp>/<CurDown> ... switch back/forth in the history
27 #   <Escape>          ... cancel the operation
28 #   <Shift-Backspace> ... remove last whole path element
29 #
30 #
31 # Colors:
32 #
33 #   white   ... syntactically correct path to not yet existing property
34 #   green   ... path to existing property
35 #   red     ... broken path syntax  (e.g. "/foo*bar" ... '*' not allowed)
36 #   yellow  ... while typing in value for a valid property path
37 #   magenta ... while typing search string (except when first character is '/')
38 #
39 #
40 # For example, to open the property browser in /position/, type '/p<TAB>:'.
41
42
43 var listener = nil;
44 var input = nil;   # what the user typed (doesn't contain unconfirmed autocompleted parts)
45 var text = nil;    # what is shown in the popup
46 var state = nil;
47
48 var completion = [];
49 var completion_pos = -1;
50 var history = [];
51 var history_pos = -1;
52
53
54 var start = func {
55         state = parse_input(text = "");
56         handle_key(`/`, 0);
57
58         listener = setlistener("/devices/status/keyboard/event", func(event) {
59                 if (!event.getNode("pressed").getValue())
60                         return;
61                 var key = event.getNode("key");
62                 var shift = event.getNode("modifier/shift").getValue();
63                 if (handle_key(key.getValue(), shift))
64                         key.setValue(-1);           # drop key event
65         });
66 }
67
68
69 var stop = func(save_history = 0) {
70         removelistener(listener);
71         if (save_history and (!size(history) or !streq(history[-1], text)))
72                 append(history, text);
73
74         history_pos = size(history);
75         gui.popdown();
76 }
77
78
79 var handle_key = func(key, shift) {
80         if (key == 357) {                  # up
81                 set_history(-1);
82
83         } elsif (key == 359) {             # down
84                 set_history(1);
85
86         } elsif (key == `\n` or key == `\r`) {
87                 if (state.error)
88                         return 1;
89                 if (state.value != nil)
90                         setprop(state.path, state.value);
91
92                 var n = props.globals.getNode(state.path);
93                 var s = state.path;
94                 if (n != nil) {
95                         print_prop(n);
96                         var v = n.getValue();
97                         s ~= " = " ~ (v == nil ? "<nil>" : v);
98                 } else {
99                         s ~= " does not exist";
100                 }
101                 screen.log.write(s, 1, 1, 1);
102                 stop(1);
103                 return 1;
104
105         } elsif (key == 27) {              # escape -> cancel
106                 stop(0);
107                 return 1;
108
109         } elsif (key == `\t`) {            # tab
110                 if (size(text) and text[0] == `/`) {
111                         text = complete(input, shift ? -1 : 1);
112                         build_completion(input);
113                         var n = call(func { props.globals.getNode(text) }, [], var err = []);
114                         if (!size(err) and n != nil and n.getAttribute("children") and size(completion) == 1)
115                                 handle_key(`/`, 0);
116                 }
117
118         } elsif (key == 8) {               # backspace
119                 if (shift) {               #     + shift: remove one path element
120                         input = text = state.parent.getPath();
121                         if (text == "")
122                                 handle_key(`/`, 0);
123                 } else {
124                         input = text = substr(text, 0, size(text) - 1);
125                         if (text == "")
126                                 stop(); # nothing in our field? close the dialog
127                 }
128                 completion_pos = -1;
129
130         } elsif (!string.isprint(key)) {
131                 return 0;                  # pass other funny events
132
133         } elsif (key == `?` and state.value == nil) {
134                 print("\n-- property search: '", text, "' ----------------------------------");
135                 search(props.globals, text);
136                 print("-- done --\n");
137                 stop(0);
138                 return 1;
139
140         } elsif (key == `!` and state.node != nil and state.value == nil) {
141                 if (!state.node.getPath()) {
142                         screen.property_display.reset();
143                         stop(0);
144                 } else {
145                         screen.property_display.add(state.node);
146                         stop(1);
147                 }
148                 return 1;
149
150         } elsif (key == `*` and state.node != nil and state.value == nil) {
151                 debug.tree(state.node);
152                 stop(1);
153                 return 1;
154
155         } elsif (key == `:` and state.node != nil and state.value == nil) {
156                 var n = state.node.getAttribute("children") ? state.node : state.parent;
157                 gui.property_browser(n);
158                 stop(1);
159                 return 1;
160
161         } else {
162                 text ~= chr(key);
163                 input = text;
164                 completion_pos = -1;
165                 history_pos = size(history);
166         }
167
168         state = parse_input(text);
169         build_completion(input);
170
171         var color = nil;
172         if (size(text) and text[0] != `/`)       # search mode (magenta)
173                 color = set_color(1, 0.4, 0.9);
174         elsif (state.error)                      # error mode (red)
175                 color = set_color(1, 0.4, 0.4);
176         elsif (state.value != nil)               # value edit mode (yellow)
177                 color = set_color(1, 0.8, 0);
178         elsif (state.node != nil)                # existing node (green)
179                 color = set_color(0.7, 1, 0.7);
180
181         gui.popupTip(text, 1000000, color);
182         return 1;                                # discard key event
183 }
184
185
186 var parse_input = func(expr) {
187         var path = expr;
188         var value = nil;
189
190         if ((var pos = find("=", expr)) >= 0) {
191                 path = substr(expr, 0, pos);
192                 value = substr(expr, pos + 1);
193         }
194
195         # split argument in parent and name
196         var last = 0;
197         while ((var pos = find("/", path, last + 1)) > 0)
198                 last = pos;
199         var parent = substr(path, 0, last); # without trailing /
200         var raw_name = substr(path, last + 1);
201         var name = raw_name;
202         if ((var pos = find("[", name)) >= 0)
203                 name = substr(name, 0, pos);
204         var node = nil;
205
206         # run dangerous operations in cage (the paths might be invalid)
207         call(func {
208                 parent = props.globals.getNode(parent);
209                 node = props.globals.getNode(path);
210         }, [], var error = []);
211
212         return {
213                 error: size(error),
214                 path: path,
215                 value: value,
216                 raw_name: raw_name,  # "binding[1"
217                 name: name,          # "binding"
218                 parent: parent,
219                 node: node,
220         };
221 }
222
223
224 var build_completion = func(in) {
225         completion = [];
226         var s = parse_input(in);
227         if (s.error or s.parent == nil)
228                 return;
229
230         foreach (var c; s.parent.getChildren()) {
231                 var index = c.getIndex();
232                 var name = c.getName();
233                 var fullname = name;
234                 if (index > 0)
235                         fullname ~= "[" ~ index ~ "]";
236                 if (substr(fullname, 0, size(s.raw_name)) == s.raw_name)
237                         append(completion, [fullname, name, index]);
238         }
239         completion = sort(completion, func(a, b) cmp(a[1], b[1]) or a[2] - b[2]);
240         #print(debug.string([completion_pos, completion]), "\n");
241 }
242
243
244 var complete = func(in, step) {
245         if (state.parent == nil or state.value != nil)
246                 return in;     # can't complete broken path or assignment
247
248         completion_pos += step;
249         if (completion_pos < 0)
250                 completion_pos = size(completion) - 1;
251         elsif (completion_pos >= size(completion))
252                 completion_pos = 0;
253
254         if (completion_pos < size(completion))
255                 in = state.parent.getPath() ~ "/" ~ completion[completion_pos][0];
256
257         return in;
258 }
259
260
261 var set_history = func(step) {
262         history_pos += step;
263         if (history_pos < 0) {
264                 history_pos = 0;
265         } elsif (history_pos >= size(history)) {
266                 history_pos = size(history);
267                 text = "";
268         } else {
269                 text = history[history_pos];
270         }
271         input = text;
272 }
273
274
275 var set_color = func(r, g, b) {
276         return { text: { color: { red: r, green: g, blue: b } }};
277 }
278
279
280 var print_prop = func(n) {
281         print(n.getPath(), " = ", debug.string(n.getValue()), "  ", debug.attributes(n));
282 }
283
284
285 var search = func(n, s) {
286         if (find(s, n.getPath()) >= 0)
287                 print_prop(n);
288         elsif (n.getType() != "NONE" and find(s, "" ~ n.getValue()) >= 0)
289                 print_prop(n);
290         foreach (var c; n.getChildren())
291                 search(c, s);
292 }
293
294
295 _setlistener("/sim/signals/nasal-dir-initialized", func {
296         foreach (var p; props.globals.getNode("/sim/gui/prop-key-handler/history", 1).getChildren("entry"))
297                 append(history, p.getValue());
298         var max = props.globals.initNode("/sim/gui/prop-key-handler/history-max-size", 30).getValue();
299         if (size(history) > max)
300                 history = subvec(history, size(history) - max);
301 });
302
303
304 _setlistener("/sim/signals/exit", func {
305         var max = props.globals.initNode("/sim/gui/prop-key-handler/history-max-size", 30).getValue();
306         if (size(history) > max)
307                 history = subvec(history, size(history) - max);
308         forindex (var i; history) {
309                 var p = props.globals.getNode("/sim/gui/prop-key-handler/history", 1).getChild("entry", i, 1);
310                 p.setValue(history[i]);
311                 p.setAttribute("userarchive", 1);
312         }
313 });
314
315