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