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