Added API to manipulate external launchers for MIME types.
[conkeror.git] / modules / hints.js
blob0d8e87e29ce1ec6a03400d6124881fd28db02daa
1 /**
2  * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
3  *
4  * Portions of this file are derived from Vimperator,
5  * (C) Copyright 2006-2007 Martin Stubenschrott.
6  *
7  * Use, modification, and distribution are subject to the terms specified in the
8  * COPYING file.
9 **/
11 define_variable("active_img_hint_background_color", "#88FF00", "Color for the active image hint background.");
12 define_variable("img_hint_background_color", "yellow", "Color for inactive image hint backgrounds.");
13 define_variable("active_hint_background_color", "#88FF00", "Color for the active hint background.");
14 define_variable("hint_background_color", "yellow", "Color for the inactive hint.");
16 /**
17  * Register hints style sheet
18  */
19 const hints_stylesheet = "chrome://conkeror/content/hints.css";
20 register_user_stylesheet(hints_stylesheet);
22 /**
23  * buffer is a content_buffer
24  *
25  */
26 function hint_manager(window, xpath_expr, focused_frame, focused_element)
28     this.window = window;
29     this.hints = [];
30     this.valid_hints = [];
31     this.xpath_expr = xpath_expr;
32     this.focused_frame = focused_frame;
33     this.focused_element = focused_element;
34     this.last_selected_hint = null;
36     // Generate
37     this.generate_hints();
40 hint_manager.prototype = {
41     current_hint_string : "",
42     current_hint_number : -1,
44     /**
45      * Create an initially hidden hint span element absolutely
46      * positioned over each element that matches
47      * hint_xpath_expression.  This is done recursively for all frames
48      * and iframes.  Information about the resulting hints are also
49      * stored in the hints array.
50      */
51     generate_hints : function () {
52         var topwin = this.window;
53         var top_height = topwin.innerHeight;
54         var top_width = topwin.innerWidth;
55         var hints = this.hints;
56         var xpath_expr = this.xpath_expr;
57         var focused_frame_hint = null, focused_element_hint = null;
58         var focused_frame = this.focused_frame;
59         var focused_element = this.focused_element;
60         function helper(window, offsetX, offsetY) {
61             var win_height = window.height;
62             var win_width = window.width;
64             // Bounds
65             var minX = offsetX < 0 ? -offsetX : 0;
66             var minY = offsetY < 0 ? -offsetY : 0;
67             var maxX = offsetX + win_width > top_width ? top_width - offsetX : top_width;
68             var maxY = offsetY + win_height > top_height ? top_height - offsetY : top_height;
70             var scrollX = window.scrollX;
71             var scrollY = window.scrollY;
73             var doc = window.document;
74             var res = doc.evaluate(xpath_expr, doc, xpath_lookup_namespace,
75                                    Ci.nsIDOMXPathResult.UNORDERED_NODE_ITERATOR_TYPE,
76                                    null /* existing results */);
78             var base_node = doc.createElementNS(XHTML_NS, "span");
79             base_node.className = "__conkeror_hint";
81             var fragment = doc.createDocumentFragment();
82             var rect, elem, text, node, show_text;
83             while (true)
84             {
85                 try {
86                     elem = res.iterateNext();
87                     if (!elem)
88                         break;
89                 } catch (e) {
90                     break; // Iterator may have been invalidated by page load activity
91                 }
92                 rect = elem.getBoundingClientRect();
93                 if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
94                     continue;
95                 rect = elem.getClientRects()[0];
96                 if (!rect)
97                     continue;
98                 show_text = false;
99                 if (elem instanceof Ci.nsIDOMHTMLInputElement || elem instanceof Ci.nsIDOMHTMLTextAreaElement)
100                     text = elem.value;
101                 else if (elem instanceof Ci.nsIDOMHTMLSelectElement) {
102                     if (elem.selectedIndex >= 0)
103                         text = elem.item(elem.selectedIndex).text;
104                     else
105                         text = "";
106                 } else if (elem instanceof Ci.nsIDOMHTMLFrameElement) {
107                     text = elem.name ? elem.name : "";
108                 } else if (/^\s*$/.test(elem.textContent) &&
109                            elem.childNodes.length == 1 &&
110                            elem.childNodes.item(0) instanceof Ci.nsIDOMHTMLImageElement) {
111                     text = elem.childNodes.item(0).alt;
112                     show_text = true;
113                 } else
114                     text = elem.textContent;
115                 text = text.toLowerCase();
117                 node = base_node.cloneNode(true);
118                 node.style.left = (rect.left + scrollX) + "px";
119                 node.style.top = (rect.top + scrollY) + "px";
120                 fragment.appendChild(node);
122                 let hint = {text: text,
123                             elem: elem,
124                             hint: node,
125                             img_hint: null,
126                             visible: false,
127                             show_text: show_text};
128                 if (elem.style) {
129                     hint.saved_color = elem.style.color;
130                     hint.saved_bgcolor = elem.style.backgroundColor;
131                 }
132                 hints.push(hint);
134                 if (elem == focused_element)
135                     focused_element_hint = hint;
136                 else if ((elem instanceof Ci.nsIDOMHTMLFrameElement ||
137                           elem instanceof Ci.nsIDOMHTMLIFrameElement) &&
138                          elem.contentWindow == focused_frame)
139                     focused_frame_hint = hint;
140             }
141             doc.documentElement.appendChild(fragment);
143             /* Recurse into any IFRAME or FRAME elements */
144             var frametag = "frame";
145             while (true) {
146                 var frames = doc.getElementsByTagName(frametag);
147                 for (var i = 0; i < frames.length; ++i)
148                 {
149                     elem = frames[i];
150                     rect = elem.getBoundingClientRect();
151                     if (!rect || rect.left > maxX || rect.right < minX || rect.top > maxY || rect.bottom < minY)
152                         continue;
153                     helper(elem.contentWindow, offsetX + rect.left, offsetY + rect.top);
154                 }
155                 if (frametag == "frame") frametag = "iframe"; else break;
156             }
157         }
158         helper(topwin, 0, 0);
159         this.last_selected_hint = focused_element_hint || focused_frame_hint;
160     },
162     /* Updates valid_hints and also re-numbers and re-displays all hints. */
163     update_valid_hints : function () {
164         this.valid_hints = [];
165         var active_number = this.current_hint_number;
167         var tokens = this.current_hint_string.split(" ");
168         var rect, h, text, img_hint, doc, scrollX, scrollY;
169         var hints = this.hints;
171     outer:
172         for (var i = 0; i < hints.length; ++i)
173         {
174             h = hints[i];
175             text = h.text;
176             for (var j = 0; j < tokens.length; ++j)
177             {
178                 if (text.indexOf(tokens[j]) == -1)
179                 {
180                     if (h.visible)
181                     {
182                         h.visible = false;
183                         h.hint.style.display = "none";
184                         if (h.img_hint)
185                             h.img_hint.style.display = "none";
186                         if (h.saved_color != null) {
187                             h.elem.style.backgroundColor = h.saved_bgcolor;
188                             h.elem.style.color = h.saved_color;
189                         }
190                     }
191                     continue outer;
192                 }
193             }
195             var cur_number = this.valid_hints.length + 1;
196             h.visible = true;
198             if (h == this.last_selected_hint && active_number == -1)
199                 this.current_hint_number = active_number = cur_number;
201             var img_elem = null;
203             if (text.length == 0 && h.elem.firstChild &&
204                 h.elem.firstChild instanceof Ci.nsIDOMHTMLImageElement)
205                 img_elem = h.elem.firstChild;
206             else if (h.elem instanceof Ci.nsIDOMHTMLImageElement)
207                 img_elem = h.elem;
209             if (img_elem)
210             {
211                 if (!h.img_hint)
212                 {
213                     rect = img_elem.getBoundingClientRect();
214                     if (rect) {
215                         doc = h.elem.ownerDocument;
216                         scrollX = doc.defaultView.scrollX;
217                         scrollY = doc.defaultView.scrollY;
218                         img_hint = doc.createElementNS(XHTML_NS, "span");
219                         img_hint.className = "__conkeror_img_hint";
220                         img_hint.style.left = (rect.left + scrollX) + "px";
221                         img_hint.style.top = (rect.top + scrollY) + "px";
222                         img_hint.style.width = (rect.right - rect.left) + "px";
223                         img_hint.style.height = (rect.bottom - rect.top) + "px";
224                         h.img_hint = img_hint;
225                         doc.documentElement.appendChild(img_hint);
226                     } else
227                         img_elem = null;
228                 }
229                 if (img_elem) {
230                     var bgcolor = (active_number == cur_number) ?
231                         active_img_hint_background_color : img_hint_background_color;
232                     h.img_hint.style.backgroundColor = bgcolor;
233                     h.img_hint.style.display = "inline";
234                 }
235             }
237             if (!h.img_hint && h.elem.style)
238                 h.elem.style.backgroundColor = (active_number == cur_number) ?
239                     active_hint_background_color : hint_background_color;
241             if (h.elem.style)
242                 h.elem.style.color = "black";
244             var label = "" + cur_number;
245             if (h.elem instanceof Ci.nsIDOMHTMLFrameElement) {
246                 label +=  " " + text;
247             } else if (h.show_text && !/^\s*$/.test(text)) {
248                 let substrs = [[0,4]];
249                 for (j = 0; j < tokens.length; ++j)
250                 {
251                     let pos = text.indexOf(tokens[j]);
252                     if(pos == -1) continue;
253                     splice_range(substrs, pos, pos + tokens[j].length + 2);
254                 }
255                 label += " " + substrs.map(function(x) {
256                     return text.substring(x[0],Math.min(x[1], text.length));
257                 }).join("..") + "..";
258             }
259             h.hint.textContent = label;
260             h.hint.style.display = "inline";
261             this.valid_hints.push(h);
262         }
264         if (active_number == -1)
265             this.select_hint(1);
266     },
268     select_hint : function (index) {
269         var old_index = this.current_hint_number;
270         if (index == old_index)
271             return;
272         var vh = this.valid_hints;
273         if (old_index >= 1 && old_index <= vh.length)
274         {
275             var h = vh[old_index - 1];
276             if (h.img_hint)
277                 h.img_hint.style.backgroundColor = img_hint_background_color;
278             if (h.elem.style)
279                 h.elem.style.backgroundColor = hint_background_color;
280         }
281         this.current_hint_number = index;
282         if (index >= 1 && index <= vh.length)
283         {
284             var h = vh[index - 1];
285             if (h.img_hint)
286                 h.img_hint.style.backgroundColor = active_img_hint_background_color;
287             if (h.elem.style)
288                 h.elem.style.backgroundColor = active_hint_background_color;
289             this.last_selected_hint = h;
290         }
291     },
293     hide_hints : function () {
294         for (var i = 0; i < this.hints.length; ++i)
295         {
296             var h = this.hints[i];
297             if (h.visible) {
298                 h.visible = false;
299                 if (h.saved_color != null)
300                 {
301                     h.elem.style.color = h.saved_color;
302                     h.elem.style.backgroundColor = h.saved_bgcolor;
303                 }
304                 if (h.img_hint)
305                     h.img_hint.style.display = "none";
306                 h.hint.style.display = "none";
307             }
308         }
309     },
311     remove : function () {
312         for (var i = 0; i < this.hints.length; ++i)
313         {
314             var h = this.hints[i];
315             if (h.visible && h.saved_color != null) {
316                 h.elem.style.color = h.saved_color;
317                 h.elem.style.backgroundColor = h.saved_bgcolor;
318             }
319             if (h.img_hint)
320                 h.img_hint.parentNode.removeChild(h.img_hint);
321             h.hint.parentNode.removeChild(h.hint);
322         }
323         this.hints = [];
324         this.valid_hints = [];
325     }
329  * keyword arguments:
331  * $prompt
332  * $callback
333  * $abort_callback
334  */
335 define_keywords("$keymap", "$auto", "$hint_xpath_expression", "$multiple");
336 function hints_minibuffer_state(continuation, buffer)
338     keywords(arguments, $keymap = hint_keymap, $auto);
339     basic_minibuffer_state.call(this, $prompt = arguments.$prompt);
340     this.original_prompt = arguments.$prompt;
341     this.continuation = continuation;
342     this.keymap = arguments.$keymap;
343     this.auto_exit = arguments.$auto ? true : false;
344     this.xpath_expr = arguments.$hint_xpath_expression;
345     this.auto_exit_timer_ID = null;
346     this.multiple = arguments.$multiple;
347     this.focused_element = buffer.focused_element;
348     this.focused_frame = buffer.focused_frame;
350 hints_minibuffer_state.prototype = {
351     __proto__: basic_minibuffer_state.prototype,
352     manager : null,
353     typed_string : "",
354     typed_number : "",
355     load : function (window) {
356         if (!this.manager) {
357             var buf = window.buffers.current;
358             this.manager = new hint_manager(buf.top_frame, this.xpath_expr,
359                                             this.focused_frame, this.focused_element);
360         }
361         this.manager.update_valid_hints();
362     },
363     unload : function (window) {
364         if (this.auto_exit_timer_ID) {
365             window.clearTimeout(this.auto_exit_timer_ID);
366             this.auto_exit_timer_ID = null;
367         }
368         this.manager.hide_hints();
369     },
370     destroy : function () {
371         if (this.auto_exit_timer_ID) {
372             this.manager.window.clearTimeout(this.auto_exit_timer_ID);
373             this.auto_exit_timer_ID = null;
374         }
375         this.manager.remove();
376     },
377     update_minibuffer : function (m) {
378         if (this.typed_number.length > 0)
379             m.prompt = this.original_prompt + " #" + this.typed_number;
380         else
381             m.prompt = this.original_prompt;
382     },
384     handle_auto_exit : function (m, delay) {
385         let window = m.window;
386         var num = this.manager.current_hint_number;
387         if (this.auto_exit_timer_ID)
388             window.clearTimeout(this.auto_exit_timer_ID);
389         let s = this;
390         this.auto_exit_timer_ID = window.setTimeout(function() { hints_exit(window, s); },
391                                                     delay);
392     },
394     handle_input : function (m) {
395         m._set_selection();
397         this.typed_number = "";
398         this.typed_string = m._input_text;
399         this.manager.current_hint_string = this.typed_string;
400         this.manager.current_hint_number = -1;
401         this.manager.update_valid_hints();
402         if (this.auto_exit) {
403             if (this.manager.valid_hints.length == 1)
404                 this.handle_auto_exit(m, hints_auto_exit_delay);
405             else if (this.manager.valid_hints.length > 1
406                      && hints_ambiguous_auto_exit_delay > 0)
407                 this.handle_auto_exit(m, hints_ambiguous_auto_exit_delay);
408         }
409         this.update_minibuffer(m);
410     }
413 define_variable("hints_auto_exit_delay", 500, "Delay (in milliseconds) after the most recent key stroke before a sole matching element is automatically selected.  If this is set to 0, automatic selection is disabled.");
415 define_variable("hints_ambiguous_auto_exit_delay", 0, "Delay (in milliseconds) after the most recent key stroke before the first of an ambiguous match is automatically selected.  If this is set to 0, automatic selection in ambiguous matches is disabled.");
417 interactive("hints-handle-number", null, function (I) {
418     let s = I.minibuffer.check_state(hints_minibuffer_state);
419     var ch = String.fromCharCode(I.event.charCode);
420     var auto_exit = false;
421     /* TODO: implement number escaping */
422     // Number entered
423     s.typed_number += ch;
425     s.manager.select_hint(parseInt(s.typed_number));
426     var num = s.manager.current_hint_number;
427     if (s.auto_exit) {
428         if (num > 0 && num <= s.manager.valid_hints.length) {
429             if (num * 10 > s.manager.valid_hints.length)
430                 auto_exit = hints_auto_exit_delay;
431             else if (hints_ambiguous_auto_exit_delay > 0)
432                 auto_exit = hints_ambiguous_auto_exit_delay;
433         }
434         if (num == 0) {
435             if (!s.multiple) {
436                 hints_exit(I.window, s);
437                 return;
438             }
439             auto_exit = hints_auto_exit_delay;
440         }
441     }
442     if (auto_exit)
443         s.handle_auto_exit(I.minibuffer, auto_exit);
444     s.update_minibuffer(I.minibuffer);
447 function hints_backspace(window, s) {
448     let m = window.minibuffer;
449     if (s.auto_exit_timer_ID) {
450         window.clearTimeout(s.auto_exit_timer_ID);
451         s.auto_exit_timer_ID = null;
452     }
453     if (s.typed_number.length > 0) {
454         s.typed_number = s.typed_number.substring(0, s.typed_number.length - 1);
455         var num = s.typed_number.length > 0 ? parseInt(s.typed_number) : 1;
456         s.manager.select_hint(num);
457     } else if (s.typed_string.length > 0) {
458         s.typed_string = s.typed_string.substring(0, s.typed_string.length - 1);
459         m._input_text = s.typed_string;
460         m._set_selection();
461         s.manager.current_hint_string = s.typed_string;
462         s.manager.current_hint_number = -1;
463         s.manager.update_valid_hints();
464     }
465     s.update_minibuffer(m);
467 interactive("hints-backspace", null, function (I) {
468     hints_backspace(I.window, I.minibuffer.check_state(hints_minibuffer_state));
471 function hints_next(window, s, count) {
472     if (s.auto_exit_timer_ID) {
473         window.clearTimeout(s.auto_exit_timer_ID);
474         s.auto_exit_timer_ID = null;
475     }
476     s.typed_number = "";
477     var cur = s.manager.current_hint_number - 1;
478     var vh = s.manager.valid_hints;
479     if (vh.length > 0) {
480         cur = (cur + count) % vh.length;
481         if (cur < 0)
482             cur += vh.length;
483         s.manager.select_hint(cur + 1);
484     }
485     s.update_minibuffer(window);
487 interactive("hints-next", null, function (I) {
488     hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), I.p);
491 interactive("hints-previous", null, function (I) {
492     hints_next(I.window, I.minibuffer.check_state(hints_minibuffer_state), -I.p);
495 function hints_exit(window, s)
497     if (s.auto_exit_timer_ID) {
498         window.clearTimeout(s.auto_exit_timer_ID);
499         s.auto_exit_timer_ID = null;
500     }
501     var cur = s.manager.current_hint_number;
502     var elem = null;
503     if (cur > 0 && cur <= s.manager.valid_hints.length)
504         elem = s.manager.valid_hints[cur - 1].elem;
505     else if (cur == 0)
506         elem = window.buffers.current.top_frame;
507     if (elem) {
508         var c = s.continuation;
509         delete s.continuation;
510         window.minibuffer.pop_state();
511         if (c)
512             c(elem);
513     }
516 interactive("hints-exit", null, function (I) {
517     hints_exit(I.window, I.minibuffer.check_state(hints_minibuffer_state));
521 /* FIXME: figure out why this needs to have a bunch of duplication */
522 define_variable(
523     "hints_xpath_expressions",
524     {
525         images: {def: "//img | //xhtml:img"},
526         frames: {def: "//iframe | //frame | //xhtml:iframe | //xhtml:frame"},
527         links: {def:
528                 "//*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
529                 "//input[not(@type='hidden')] | //a | //area | //iframe | //textarea | //button | //select | " +
530                 "//xhtml:*[@onclick or @onmouseover or @onmousedown or @onmouseup or @oncommand or @class='lk' or @class='s'] | " +
531                 "//xhtml:input[not(@type='hidden')] | //xhtml:a | //xhtml:area | //xhtml:iframe | //xhtml:textarea | " +
532                 "//xhtml:button | //xhtml:select"},
533         mathml: {def: "//m:math"}
534     },
535     "XPath expressions for each object class.");
537 minibuffer_auto_complete_preferences["media"] = true;
539 define_keywords("$buffer");
540 minibuffer.prototype.read_hinted_element = function () {
541     keywords(arguments);
542     var buf = arguments.$buffer;
543     var s = new hints_minibuffer_state((yield CONTINUATION), buf, forward_keywords(arguments));
544     this.push_state(s);
545     var result = yield SUSPEND;
546     yield co_return(result);