]> git.donarmstrong.com Git - dactyl.git/blob - common/content/mow.js
Import 1.0b7.1 supporting Firefox up to 8.*
[dactyl.git] / common / content / mow.js
1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
4 //
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
7 "use strict";
8
9 var MOW = Module("mow", {
10     init: function init() {
11
12         this._resize = Timer(20, 400, function _resize() {
13             if (this.visible)
14                 this.resize(false);
15
16             if (this.visible && isinstance(modes.main, modes.OUTPUT_MULTILINE))
17                 this.updateMorePrompt();
18         }, this);
19
20         this._timer = Timer(20, 400, function _timer() {
21             if (modes.have(modes.OUTPUT_MULTILINE)) {
22                 this.resize(true);
23
24                 if (options["more"] && this.isScrollable(1)) {
25                     // start the last executed command's output at the top of the screen
26                     let elements = this.document.getElementsByClassName("ex-command-output");
27                     elements[elements.length - 1].scrollIntoView(true);
28                 }
29                 else
30                     this.body.scrollTop = this.body.scrollHeight;
31
32                 dactyl.focus(this.window);
33                 this.updateMorePrompt();
34             }
35         }, this);
36
37         events.listen(window, this, "windowEvents");
38
39         modules.mow = this;
40         let fontSize = util.computedStyle(document.documentElement).fontSize;
41         styles.system.add("font-size", "dactyl://content/buffer.xhtml",
42                           "body { font-size: " + fontSize + "; } \
43                            html|html > xul|scrollbar { visibility: collapse !important; }",
44                           true);
45
46         XML.ignoreWhitespace = true;
47         util.overlayWindow(window, {
48             objects: {
49                 eventTarget: this
50             },
51             append: <e4x xmlns={XUL} xmlns:dactyl={NS}>
52                 <window id={document.documentElement.id}>
53                     <popupset>
54                         <menupopup id="dactyl-contextmenu" highlight="Events" events="contextEvents">
55                             <menuitem id="dactyl-context-copylink"
56                                       label={_("mow.contextMenu.copyLink")} dactyl:group="link"
57                                       oncommand="goDoCommand('cmd_copyLink');"/>
58                             <menuitem id="dactyl-context-copypath"
59                                       label={_("mow.contextMenu.copyPath")} dactyl:group="link path"
60                                       oncommand="dactyl.clipboardWrite(document.popupNode.getAttribute('path'));"/>
61                             <menuitem id="dactyl-context-copy"
62                                       label={_("mow.contextMenu.copy")} dactyl:group="selection"
63                                       command="cmd_copy"/>
64                             <menuitem id="dactyl-context-selectall"
65                                       label={_("mow.contextMenu.selectAll")}
66                                       command="cmd_selectAll"/>
67                         </menupopup>
68                     </popupset>
69                 </window>
70                 <vbox id={config.commandContainer}>
71                     <vbox class="dactyl-container" id="dactyl-multiline-output-container" hidden="false" collapsed="true">
72                         <iframe id="dactyl-multiline-output" src="dactyl://content/buffer.xhtml"
73                                 flex="1" hidden="false" collapsed="false" contextmenu="dactyl-contextmenu"
74                                 highlight="Events" />
75                     </vbox>
76                 </vbox>
77             </e4x>
78         });
79     },
80
81     __noSuchMethod__: function (meth, args) Buffer[meth].apply(Buffer, [this.body].concat(args)),
82
83     get widget() this.widgets.multilineOutput,
84     widgets: Class.memoize(function widgets() commandline.widgets),
85
86     body: Class.memoize(function body() this.widget.contentDocument.documentElement),
87     get document() this.widget.contentDocument,
88     get window() this.widget.contentWindow,
89
90     /**
91      * Display a multi-line message.
92      *
93      * @param {string} data
94      * @param {string} highlightGroup
95      */
96     echo: function echo(data, highlightGroup, silent) {
97         let body = this.document.body;
98
99         this.widgets.message = null;
100         if (!commandline.commandVisible)
101             commandline.hide();
102
103         if (modes.main != modes.OUTPUT_MULTILINE) {
104             modes.push(modes.OUTPUT_MULTILINE, null, {
105                 onKeyPress: this.closure.onKeyPress,
106                 leave: this.closure(function leave(stack) {
107                     if (stack.pop)
108                         for (let message in values(this.messages))
109                             if (message.leave)
110                                 message.leave(stack);
111                 })
112             });
113             this.messages = [];
114         }
115
116         // If it's already XML, assume it knows what it's doing.
117         // Otherwise, white space is significant.
118         // The problem elsewhere is that E4X tends to insert new lines
119         // after interpolated data.
120         XML.ignoreWhitespace = XML.prettyPrinting = false;
121
122         if (isObject(data) && !isinstance(data, _)) {
123             this.lastOutput = null;
124
125             var output = util.xmlToDom(<div class="ex-command-output" style="white-space: nowrap" highlight={highlightGroup}/>,
126                                        this.document);
127             data.document = this.document;
128             try {
129                 output.appendChild(data.message);
130             }
131             catch (e) {
132                 util.reportError(e);
133                 util.dump(data);
134             }
135             this.messages.push(data);
136         }
137         else {
138             let style = isString(data) ? "pre" : "nowrap";
139             this.lastOutput = <div class="ex-command-output" style={"white-space: " + style} highlight={highlightGroup}>{data}</div>;
140
141             var output = util.xmlToDom(this.lastOutput, this.document);
142         }
143
144         // FIXME: need to make sure an open MOW is closed when commands
145         //        that don't generate output are executed
146         if (!this.visible) {
147             this.body.scrollTop = 0;
148             body.textContent = "";
149         }
150
151         body.appendChild(output);
152
153         let str = typeof data !== "xml" && data.message || data;
154         if (!silent)
155             dactyl.triggerObserver("echoMultiline", data, highlightGroup, output);
156
157         this._timer.tell();
158         if (!this.visible)
159             this._timer.flush();
160     },
161
162     events: {
163         click: function onClick(event) {
164             if (event.getPreventDefault())
165                 return;
166
167             const openLink = function openLink(where) {
168                 event.preventDefault();
169                 dactyl.open(event.target.href, where);
170             };
171
172             if (event.target instanceof HTMLAnchorElement)
173                 switch (events.toString(event)) {
174                 case "<LeftMouse>":
175                     openLink(dactyl.CURRENT_TAB);
176                     break;
177                 case "<MiddleMouse>":
178                 case "<C-LeftMouse>":
179                 case "<C-M-LeftMouse>":
180                     openLink({ where: dactyl.NEW_TAB, background: true });
181                     break;
182                 case "<S-MiddleMouse>":
183                 case "<C-S-LeftMouse>":
184                 case "<C-M-S-LeftMouse>":
185                     openLink({ where: dactyl.NEW_TAB, background: false });
186                     break;
187                 case "<S-LeftMouse>":
188                     openLink(dactyl.NEW_WINDOW);
189                     break;
190                 }
191         },
192         unload: function onUnload(event) {
193             event.preventDefault();
194         }
195     },
196
197     windowEvents: {
198         resize: function onResize(event) {
199             this._resize.tell();
200         }
201     },
202
203     contextEvents: {
204         popupshowing: function onPopupShowing(event) {
205             let menu = commandline.widgets.contextMenu;
206             let enabled = {
207                 link: window.document.popupNode instanceof HTMLAnchorElement,
208                 path: window.document.popupNode.hasAttribute("path"),
209                 selection: !window.document.commandDispatcher.focusedWindow.getSelection().isCollapsed
210             };
211
212             for (let node in array.iterValues(menu.children)) {
213                 let group = node.getAttributeNS(NS, "group");
214                 node.hidden = group && !group.split(/\s+/).every(function (g) enabled[g]);
215             }
216         }
217     },
218
219     onKeyPress: function onKeyPress(eventList) {
220         const KILL = false, PASS = true;
221
222         if (options["more"] && mow.isScrollable(1))
223             this.updateMorePrompt(false, true);
224         else {
225             modes.pop();
226             events.feedevents(null, eventList);
227             return KILL;
228         }
229         return PASS;
230     },
231
232     /**
233      * Changes the height of the message window to fit in the available space.
234      *
235      * @param {boolean} open If true, the widget will be opened if it's not
236      *     already so.
237      */
238     resize: function resize(open, extra) {
239         if (!(open || this.visible))
240             return;
241
242         let doc = this.widget.contentDocument;
243
244         let availableHeight = config.outputHeight;
245         if (this.visible)
246             availableHeight += parseFloat(this.widgets.mowContainer.height || 0);
247         availableHeight -= extra || 0;
248
249         doc.body.style.minWidth = this.widgets.commandbar.commandline.scrollWidth + "px";
250         this.widgets.mowContainer.height = Math.min(doc.body.clientHeight, availableHeight) + "px";
251         this.timeout(function ()
252             this.widgets.mowContainer.height = Math.min(doc.body.clientHeight, availableHeight) + "px",
253             0);
254
255         doc.body.style.minWidth = "";
256
257         this.visible = true;
258     },
259
260     get spaceNeeded() {
261         let rect = this.widgets.commandbar.commandline.getBoundingClientRect();
262         let offset = rect.bottom - window.innerHeight;
263         return Math.max(0, offset);
264     },
265
266     /**
267      * Update or remove the multi-line output widget's "MORE" prompt.
268      *
269      * @param {boolean} force If true, "-- More --" is shown even if we're
270      *     at the end of the output.
271      * @param {boolean} showHelp When true, show the valid key sequences
272      *     and what they do.
273      */
274     updateMorePrompt: function updateMorePrompt(force, showHelp) {
275         if (!this.visible || !isinstance(modes.main, modes.OUTPUT_MULTILINE))
276             return this.widgets.message = null;
277
278         let elem = this.widget.contentDocument.documentElement;
279
280         if (showHelp)
281             this.widgets.message = ["MoreMsg", _("mow.moreHelp")];
282         else if (force || (options["more"] && Buffer.isScrollable(elem, 1)))
283             this.widgets.message = ["MoreMsg", _("mow.more")];
284         else
285             this.widgets.message = ["Question", _("mow.continue")];
286     },
287
288     visible: Modes.boundProperty({
289         get: function get_mowVisible() !this.widgets.mowContainer.collapsed,
290         set: function set_mowVisible(value) {
291             this.widgets.mowContainer.collapsed = !value;
292
293             let elem = this.widget;
294             if (!value && elem && elem.contentWindow == document.commandDispatcher.focusedWindow) {
295
296                 let focused = content.document.activeElement;
297                 if (Events.isInputElement(focused))
298                     focused.blur();
299
300                 document.commandDispatcher.focusedWindow = content;
301             }
302         }
303     })
304 }, {
305 }, {
306     mappings: function initMappings() {
307         const PASS = true;
308         const DROP = false;
309         const BEEP = {};
310
311         mappings.add([modes.COMMAND],
312             ["g<lt>"], "Redisplay the last command output",
313             function () {
314                 dactyl.assert(mow.lastOutput, _("mow.noPreviousOutput"));
315                 mow.echo(mow.lastOutput, "Normal");
316             });
317
318         let bind = function bind(keys, description, action, test, default_) {
319             mappings.add([modes.OUTPUT_MULTILINE],
320                 keys, description,
321                 function (args) {
322                     if (!options["more"])
323                         var res = PASS;
324                     else if (test && !test(args))
325                         res = default_;
326                     else
327                         res = action.call(this, args);
328
329                     if (res === PASS || res === DROP)
330                         modes.pop();
331                     else
332                         mow.updateMorePrompt();
333                     if (res === BEEP)
334                         dactyl.beep();
335                     else if (res === PASS)
336                         events.feedkeys(args.command);
337                 }, {
338                     count: action.length > 0
339                 });
340         };
341
342         bind(["j", "<C-e>", "<Down>"], "Scroll down one line",
343              function ({ count }) { mow.scrollVertical("lines", 1 * (count || 1)); },
344              function () mow.isScrollable(1), BEEP);
345
346         bind(["k", "<C-y>", "<Up>"], "Scroll up one line",
347              function ({ count }) { mow.scrollVertical("lines", -1 * (count || 1)); },
348              function () mow.isScrollable(-1), BEEP);
349
350         bind(["<C-j>", "<C-m>", "<Return>"], "Scroll down one line, exit on last line",
351              function ({ count }) { mow.scrollVertical("lines", 1 * (count || 1)); },
352              function () mow.isScrollable(1), DROP);
353
354         // half page down
355         bind(["<C-d>"], "Scroll down half a page",
356              function ({ count }) { mow.scrollVertical("pages", .5 * (count || 1)); },
357              function () mow.isScrollable(1), BEEP);
358
359         bind(["<C-f>", "<PageDown>"], "Scroll down one page",
360              function ({ count }) { mow.scrollVertical("pages", 1 * (count || 1)); },
361              function () mow.isScrollable(1), BEEP);
362
363         bind(["<Space>"], "Scroll down one page",
364              function ({ count }) { mow.scrollVertical("pages", 1 * (count || 1)); },
365              function () mow.isScrollable(1), DROP);
366
367         bind(["<C-u>"], "Scroll up half a page",
368              function ({ count }) { mow.scrollVertical("pages", -.5 * (count || 1)); },
369              function () mow.isScrollable(-1), BEEP);
370
371         bind(["<C-b>", "<PageUp>"], "Scroll up half a page",
372              function ({ count }) { mow.scrollVertical("pages", -1 * (count || 1)); },
373              function () mow.isScrollable(-1), BEEP);
374
375         bind(["gg"], "Scroll to the beginning of output",
376              function () { mow.scrollToPercent(null, 0); });
377
378         bind(["G"], "Scroll to the end of output",
379              function ({ count }) { mow.scrollToPercent(null, count || 100); });
380
381         // copy text to clipboard
382         bind(["<C-y>"], "Yank selection to clipboard",
383              function () { dactyl.clipboardWrite(buffer.getCurrentWord(mow.window)); });
384
385         // close the window
386         bind(["q"], "Close the output window",
387              function () {},
388              function () false, DROP);
389     },
390     options: function initOptions() {
391         options.add(["more"],
392             "Pause the message list window when the full output will not fit on one page",
393             "boolean", true);
394     }
395 });
396
397 // vim: set fdm=marker sw=4 ts=4 et: