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 ...
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
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
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 '/')
40 # For example, to open the property browser in /position/, type '/p<TAB>:'.
44 var input = nil; # what the user typed (doesn't contain unconfirmed autocompleted parts)
45 var text = nil; # what is shown in the popup
49 var completion_pos = -1;
55 state = parse_input(text = "");
58 listener = setlistener("/devices/status/keyboard/event", func(event) {
59 if (!event.getNode("pressed").getValue())
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
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);
74 history_pos = size(history);
79 var handle_key = func(key, shift) {
80 if (key == 357) { # up
83 } elsif (key == 359) { # down
86 } elsif (key == `\n` or key == `\r`) {
89 if (state.value != nil)
90 setprop(state.path, state.value);
92 var n = props.globals.getNode(state.path);
97 s ~= " = " ~ (v == nil ? "<nil>" : v);
99 s ~= " does not exist";
101 screen.log.write(s, 1, 1, 1);
105 } elsif (key == 27) { # escape -> cancel
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)
118 } elsif (key == 8) { # backspace
119 if (shift) { # + shift: remove one path element
120 input = text = state.parent.getPath();
124 input = text = substr(text, 0, size(text) - 1);
126 stop(); # nothing in our field? close the dialog
130 } elsif (!string.isprint(key)) {
131 return 0; # pass other funny events
133 } elsif (key == `?` and state.value == nil) {
134 print("\n-- property search: '", text, "' ----------------------------------");
135 search(props.globals, text);
136 print("-- done --\n");
140 } elsif (key == `!` and state.node != nil and state.value == nil) {
141 if (!state.node.getPath()) {
142 screen.property_display.reset();
145 screen.property_display.add(state.node);
150 } elsif (key == `*` and state.node != nil and state.value == nil) {
151 debug.tree(state.node);
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);
165 history_pos = size(history);
168 state = parse_input(text);
169 build_completion(input);
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);
181 gui.popupTip(text, 1000000, color);
182 return 1; # discard key event
186 var parse_input = func(expr) {
190 if ((var pos = find("=", expr)) >= 0) {
191 path = substr(expr, 0, pos);
192 value = substr(expr, pos + 1);
195 # split argument in parent and name
197 while ((var pos = find("/", path, last + 1)) > 0)
199 var parent = substr(path, 0, last); # without trailing /
200 var raw_name = substr(path, last + 1);
202 if ((var pos = find("[", name)) >= 0)
203 name = substr(name, 0, pos);
206 # run dangerous operations in cage (the paths might be invalid)
208 parent = props.globals.getNode(parent);
209 node = props.globals.getNode(path);
210 }, [], var error = []);
216 raw_name: raw_name, # "binding[1"
217 name: name, # "binding"
224 var build_completion = func(in) {
226 var s = parse_input(in);
227 if (s.error or s.parent == nil)
230 foreach (var c; s.parent.getChildren()) {
231 var index = c.getIndex();
232 var name = c.getName();
235 fullname ~= "[" ~ index ~ "]";
236 if (substr(fullname, 0, size(s.raw_name)) == s.raw_name)
237 append(completion, [fullname, name, index]);
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");
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
248 completion_pos += step;
249 if (completion_pos < 0)
250 completion_pos = size(completion) - 1;
251 elsif (completion_pos >= size(completion))
254 if (completion_pos < size(completion))
255 in = state.parent.getPath() ~ "/" ~ completion[completion_pos][0];
261 var set_history = func(step) {
263 if (history_pos < 0) {
265 } elsif (history_pos >= size(history)) {
266 history_pos = size(history);
269 text = history[history_pos];
275 var set_color = func(r, g, b) {
276 return { text: { color: { red: r, green: g, blue: b } }};
280 var print_prop = func(n) {
281 print(n.getPath(), " = ", debug.string(n.getValue()), " ", debug.attributes(n));
285 var search = func(n, s) {
286 if (find(s, n.getPath()) >= 0)
288 elsif (n.getType() != "NONE" and find(s, "" ~ n.getValue()) >= 0)
290 foreach (var c; n.getChildren())
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);
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);