Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 "use strict";
8 const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 1048576; // 1 MB in bytes
9 const SOURCE_URL_DEFAULT_MAX_LENGTH = 64; // chars
10 const STACK_FRAMES_SOURCE_URL_MAX_LENGTH = 15; // chars
11 const STACK_FRAMES_SOURCE_URL_TRIM_SECTION = "center";
12 const STACK_FRAMES_SCROLL_DELAY = 100; // ms
13 const BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH = 1000; // chars
14 const BREAKPOINT_CONDITIONAL_POPUP_POSITION = "before_start";
15 const BREAKPOINT_CONDITIONAL_POPUP_OFFSET_X = 7; // px
16 const BREAKPOINT_CONDITIONAL_POPUP_OFFSET_Y = -3; // px
17 const RESULTS_PANEL_POPUP_POSITION = "before_end";
18 const RESULTS_PANEL_MAX_RESULTS = 10;
19 const FILE_SEARCH_ACTION_MAX_DELAY = 300; // ms
20 const GLOBAL_SEARCH_EXPAND_MAX_RESULTS = 50;
21 const GLOBAL_SEARCH_LINE_MAX_LENGTH = 300; // chars
22 const GLOBAL_SEARCH_ACTION_MAX_DELAY = 1500; // ms
23 const FUNCTION_SEARCH_ACTION_MAX_DELAY = 400; // ms
24 const SEARCH_GLOBAL_FLAG = "!";
25 const SEARCH_FUNCTION_FLAG = "@";
26 const SEARCH_TOKEN_FLAG = "#";
27 const SEARCH_LINE_FLAG = ":";
28 const SEARCH_VARIABLE_FLAG = "*";
29 const SEARCH_AUTOFILL = [SEARCH_GLOBAL_FLAG, SEARCH_FUNCTION_FLAG, SEARCH_TOKEN_FLAG];
30 const EDITOR_VARIABLE_HOVER_DELAY = 350; // ms
31 const EDITOR_VARIABLE_POPUP_POSITION = "topcenter bottomleft";
32 const TOOLBAR_ORDER_POPUP_POSITION = "topcenter bottomleft";
34 /**
35 * Object defining the debugger view components.
36 */
37 let DebuggerView = {
38 /**
39 * Initializes the debugger view.
40 *
41 * @return object
42 * A promise that is resolved when the view finishes initializing.
43 */
44 initialize: function() {
45 if (this._startup) {
46 return this._startup;
47 }
49 let deferred = promise.defer();
50 this._startup = deferred.promise;
52 this._initializePanes();
53 this.Toolbar.initialize();
54 this.Options.initialize();
55 this.Filtering.initialize();
56 this.FilteredSources.initialize();
57 this.FilteredFunctions.initialize();
58 this.StackFrames.initialize();
59 this.StackFramesClassicList.initialize();
60 this.Sources.initialize();
61 this.VariableBubble.initialize();
62 this.Tracer.initialize();
63 this.WatchExpressions.initialize();
64 this.EventListeners.initialize();
65 this.GlobalSearch.initialize();
66 this._initializeVariablesView();
67 this._initializeEditor(deferred.resolve);
69 document.title = L10N.getStr("DebuggerWindowTitle");
71 return deferred.promise;
72 },
74 /**
75 * Destroys the debugger view.
76 *
77 * @return object
78 * A promise that is resolved when the view finishes destroying.
79 */
80 destroy: function() {
81 if (this._shutdown) {
82 return this._shutdown;
83 }
85 let deferred = promise.defer();
86 this._shutdown = deferred.promise;
88 this.Toolbar.destroy();
89 this.Options.destroy();
90 this.Filtering.destroy();
91 this.FilteredSources.destroy();
92 this.FilteredFunctions.destroy();
93 this.StackFrames.destroy();
94 this.StackFramesClassicList.destroy();
95 this.Sources.destroy();
96 this.VariableBubble.destroy();
97 this.Tracer.destroy();
98 this.WatchExpressions.destroy();
99 this.EventListeners.destroy();
100 this.GlobalSearch.destroy();
101 this._destroyPanes();
102 this._destroyEditor(deferred.resolve);
104 return deferred.promise;
105 },
107 /**
108 * Initializes the UI for all the displayed panes.
109 */
110 _initializePanes: function() {
111 dumpn("Initializing the DebuggerView panes");
113 this._body = document.getElementById("body");
114 this._editorDeck = document.getElementById("editor-deck");
115 this._sourcesPane = document.getElementById("sources-pane");
116 this._instrumentsPane = document.getElementById("instruments-pane");
117 this._instrumentsPaneToggleButton = document.getElementById("instruments-pane-toggle");
119 this.showEditor = this.showEditor.bind(this);
120 this.showBlackBoxMessage = this.showBlackBoxMessage.bind(this);
121 this.showProgressBar = this.showProgressBar.bind(this);
122 this.maybeShowBlackBoxMessage = this.maybeShowBlackBoxMessage.bind(this);
124 this._onTabSelect = this._onInstrumentsPaneTabSelect.bind(this);
125 this._instrumentsPane.tabpanels.addEventListener("select", this._onTabSelect);
127 this._collapsePaneString = L10N.getStr("collapsePanes");
128 this._expandPaneString = L10N.getStr("expandPanes");
130 this._sourcesPane.setAttribute("width", Prefs.sourcesWidth);
131 this._instrumentsPane.setAttribute("width", Prefs.instrumentsWidth);
132 this.toggleInstrumentsPane({ visible: Prefs.panesVisibleOnStartup });
134 // Side hosts requires a different arrangement of the debugger widgets.
135 if (gHostType == "side") {
136 this.handleHostChanged(gHostType);
137 }
138 },
140 /**
141 * Destroys the UI for all the displayed panes.
142 */
143 _destroyPanes: function() {
144 dumpn("Destroying the DebuggerView panes");
146 if (gHostType != "side") {
147 Prefs.sourcesWidth = this._sourcesPane.getAttribute("width");
148 Prefs.instrumentsWidth = this._instrumentsPane.getAttribute("width");
149 }
151 this._sourcesPane = null;
152 this._instrumentsPane = null;
153 this._instrumentsPaneToggleButton = null;
154 },
156 /**
157 * Initializes the VariablesView instance and attaches a controller.
158 */
159 _initializeVariablesView: function() {
160 this.Variables = new VariablesView(document.getElementById("variables"), {
161 searchPlaceholder: L10N.getStr("emptyVariablesFilterText"),
162 emptyText: L10N.getStr("emptyVariablesText"),
163 onlyEnumVisible: Prefs.variablesOnlyEnumVisible,
164 searchEnabled: Prefs.variablesSearchboxVisible,
165 eval: (variable, value) => {
166 let string = variable.evaluationMacro(variable, value);
167 DebuggerController.StackFrames.evaluate(string);
168 },
169 lazyEmpty: true
170 });
172 // Attach the current toolbox to the VView so it can link DOMNodes to
173 // the inspector/highlighter
174 this.Variables.toolbox = DebuggerController._toolbox;
176 // Attach a controller that handles interfacing with the debugger protocol.
177 VariablesViewController.attach(this.Variables, {
178 getEnvironmentClient: aObject => gThreadClient.environment(aObject),
179 getObjectClient: aObject => {
180 return aObject instanceof DebuggerController.Tracer.WrappedObject
181 ? DebuggerController.Tracer.syncGripClient(aObject.object)
182 : gThreadClient.pauseGrip(aObject)
183 }
184 });
186 // Relay events from the VariablesView.
187 this.Variables.on("fetched", (aEvent, aType) => {
188 switch (aType) {
189 case "scopes":
190 window.emit(EVENTS.FETCHED_SCOPES);
191 break;
192 case "variables":
193 window.emit(EVENTS.FETCHED_VARIABLES);
194 break;
195 case "properties":
196 window.emit(EVENTS.FETCHED_PROPERTIES);
197 break;
198 }
199 });
200 },
202 /**
203 * Initializes the Editor instance.
204 *
205 * @param function aCallback
206 * Called after the editor finishes initializing.
207 */
208 _initializeEditor: function(aCallback) {
209 dumpn("Initializing the DebuggerView editor");
211 let extraKeys = {};
212 bindKey("_doTokenSearch", "tokenSearchKey");
213 bindKey("_doGlobalSearch", "globalSearchKey", { alt: true });
214 bindKey("_doFunctionSearch", "functionSearchKey");
215 extraKeys[Editor.keyFor("jumpToLine")] = false;
217 function bindKey(func, key, modifiers = {}) {
218 let key = document.getElementById(key).getAttribute("key");
219 let shortcut = Editor.accel(key, modifiers);
220 extraKeys[shortcut] = () => DebuggerView.Filtering[func]();
221 }
223 this.editor = new Editor({
224 mode: Editor.modes.text,
225 readOnly: true,
226 lineNumbers: true,
227 showAnnotationRuler: true,
228 gutters: [ "breakpoints" ],
229 extraKeys: extraKeys,
230 contextMenu: "sourceEditorContextMenu"
231 });
233 this.editor.appendTo(document.getElementById("editor")).then(() => {
234 this.editor.extend(DebuggerEditor);
235 this._loadingText = L10N.getStr("loadingText");
236 this._onEditorLoad(aCallback);
237 });
239 this.editor.on("gutterClick", (ev, line) => {
240 if (this.editor.hasBreakpoint(line)) {
241 this.editor.removeBreakpoint(line);
242 } else {
243 this.editor.addBreakpoint(line);
244 }
245 });
246 },
248 /**
249 * The load event handler for the source editor, also executing any necessary
250 * post-load operations.
251 *
252 * @param function aCallback
253 * Called after the editor finishes loading.
254 */
255 _onEditorLoad: function(aCallback) {
256 dumpn("Finished loading the DebuggerView editor");
258 DebuggerController.Breakpoints.initialize().then(() => {
259 window.emit(EVENTS.EDITOR_LOADED, this.editor);
260 aCallback();
261 });
262 },
264 /**
265 * Destroys the Editor instance and also executes any necessary
266 * post-unload operations.
267 *
268 * @param function aCallback
269 * Called after the editor finishes destroying.
270 */
271 _destroyEditor: function(aCallback) {
272 dumpn("Destroying the DebuggerView editor");
274 DebuggerController.Breakpoints.destroy().then(() => {
275 window.emit(EVENTS.EDITOR_UNLOADED, this.editor);
276 this.editor.destroy();
277 this.editor = null;
278 aCallback();
279 });
280 },
282 /**
283 * Display the source editor.
284 */
285 showEditor: function() {
286 this._editorDeck.selectedIndex = 0;
287 },
289 /**
290 * Display the black box message.
291 */
292 showBlackBoxMessage: function() {
293 this._editorDeck.selectedIndex = 1;
294 },
296 /**
297 * Display the progress bar.
298 */
299 showProgressBar: function() {
300 this._editorDeck.selectedIndex = 2;
301 },
303 /**
304 * Show or hide the black box message vs. source editor depending on if the
305 * selected source is black boxed or not.
306 */
307 maybeShowBlackBoxMessage: function() {
308 let { source } = DebuggerView.Sources.selectedItem.attachment;
309 if (gThreadClient.source(source).isBlackBoxed) {
310 this.showBlackBoxMessage();
311 } else {
312 this.showEditor();
313 }
314 },
316 /**
317 * Sets the currently displayed text contents in the source editor.
318 * This resets the mode and undo stack.
319 *
320 * @param string aTextContent
321 * The source text content.
322 */
323 _setEditorText: function(aTextContent = "") {
324 this.editor.setMode(Editor.modes.text);
325 this.editor.setText(aTextContent);
326 this.editor.clearDebugLocation();
327 this.editor.clearHistory();
328 },
330 /**
331 * Sets the proper editor mode (JS or HTML) according to the specified
332 * content type, or by determining the type from the url or text content.
333 *
334 * @param string aUrl
335 * The source url.
336 * @param string aContentType [optional]
337 * The source content type.
338 * @param string aTextContent [optional]
339 * The source text content.
340 */
341 _setEditorMode: function(aUrl, aContentType = "", aTextContent = "") {
342 // Avoid setting the editor mode for very large files.
343 // Is this still necessary? See bug 929225.
344 if (aTextContent.length >= SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE) {
345 return void this.editor.setMode(Editor.modes.text);
346 }
348 // Use JS mode for files with .js and .jsm extensions.
349 if (SourceUtils.isJavaScript(aUrl, aContentType)) {
350 return void this.editor.setMode(Editor.modes.js);
351 }
353 // Use HTML mode for files in which the first non whitespace character is
354 // <, regardless of extension.
355 if (aTextContent.match(/^\s*</)) {
356 return void this.editor.setMode(Editor.modes.html);
357 }
359 // Unknown language, use text.
360 this.editor.setMode(Editor.modes.text);
361 },
363 /**
364 * Sets the currently displayed source text in the editor.
365 *
366 * You should use DebuggerView.updateEditor instead. It updates the current
367 * caret and debug location based on a requested url and line.
368 *
369 * @param object aSource
370 * The source object coming from the active thread.
371 * @param object aFlags
372 * Additional options for setting the source. Supported options:
373 * - force: boolean allowing whether we can get the selected url's
374 * text again.
375 * @return object
376 * A promise that is resolved after the source text has been set.
377 */
378 _setEditorSource: function(aSource, aFlags={}) {
379 // Avoid setting the same source text in the editor again.
380 if (this._editorSource.url == aSource.url && !aFlags.force) {
381 return this._editorSource.promise;
382 }
383 let transportType = gClient.localTransport ? "_LOCAL" : "_REMOTE";
384 let histogramId = "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE" + transportType + "_MS";
385 let histogram = Services.telemetry.getHistogramById(histogramId);
386 let startTime = Date.now();
388 let deferred = promise.defer();
390 this._setEditorText(L10N.getStr("loadingText"));
391 this._editorSource = { url: aSource.url, promise: deferred.promise };
393 DebuggerController.SourceScripts.getText(aSource).then(([, aText, aContentType]) => {
394 // Avoid setting an unexpected source. This may happen when switching
395 // very fast between sources that haven't been fetched yet.
396 if (this._editorSource.url != aSource.url) {
397 return;
398 }
400 this._setEditorText(aText);
401 this._setEditorMode(aSource.url, aContentType, aText);
403 // Synchronize any other components with the currently displayed source.
404 DebuggerView.Sources.selectedValue = aSource.url;
405 DebuggerController.Breakpoints.updateEditorBreakpoints();
407 histogram.add(Date.now() - startTime);
409 // Resolve and notify that a source file was shown.
410 window.emit(EVENTS.SOURCE_SHOWN, aSource);
411 deferred.resolve([aSource, aText, aContentType]);
412 },
413 ([, aError]) => {
414 let msg = L10N.getStr("errorLoadingText") + DevToolsUtils.safeErrorString(aError);
415 this._setEditorText(msg);
416 Cu.reportError(msg);
417 dumpn(msg);
419 // Reject and notify that there was an error showing the source file.
420 window.emit(EVENTS.SOURCE_ERROR_SHOWN, aSource);
421 deferred.reject([aSource, aError]);
422 });
424 return deferred.promise;
425 },
427 /**
428 * Update the source editor's current caret and debug location based on
429 * a requested url and line.
430 *
431 * @param string aUrl
432 * The target source url.
433 * @param number aLine [optional]
434 * The target line in the source.
435 * @param object aFlags [optional]
436 * Additional options for showing the source. Supported options:
437 * - charOffset: character offset for the caret or debug location
438 * - lineOffset: line offset for the caret or debug location
439 * - columnOffset: column offset for the caret or debug location
440 * - noCaret: don't set the caret location at the specified line
441 * - noDebug: don't set the debug location at the specified line
442 * - align: string specifying whether to align the specified line
443 * at the "top", "center" or "bottom" of the editor
444 * - force: boolean allowing whether we can get the selected url's
445 * text again
446 * @return object
447 * A promise that is resolved after the source text has been set.
448 */
449 setEditorLocation: function(aUrl, aLine = 0, aFlags = {}) {
450 // Avoid trying to set a source for a url that isn't known yet.
451 if (!this.Sources.containsValue(aUrl)) {
452 return promise.reject(new Error("Unknown source for the specified URL."));
453 }
455 // If the line is not specified, default to the current frame's position,
456 // if available and the frame's url corresponds to the requested url.
457 if (!aLine) {
458 let cachedFrames = DebuggerController.activeThread.cachedFrames;
459 let currentDepth = DebuggerController.StackFrames.currentFrameDepth;
460 let frame = cachedFrames[currentDepth];
461 if (frame && frame.where.url == aUrl) {
462 aLine = frame.where.line;
463 }
464 }
466 let sourceItem = this.Sources.getItemByValue(aUrl);
467 let sourceForm = sourceItem.attachment.source;
469 // Make sure the requested source client is shown in the editor, then
470 // update the source editor's caret position and debug location.
471 return this._setEditorSource(sourceForm, aFlags).then(([,, aContentType]) => {
472 // Record the contentType learned from fetching
473 sourceForm.contentType = aContentType;
474 // Line numbers in the source editor should start from 1. If invalid
475 // or not specified, then don't do anything.
476 if (aLine < 1) {
477 window.emit(EVENTS.EDITOR_LOCATION_SET);
478 return;
479 }
480 if (aFlags.charOffset) {
481 aLine += this.editor.getPosition(aFlags.charOffset).line;
482 }
483 if (aFlags.lineOffset) {
484 aLine += aFlags.lineOffset;
485 }
486 if (!aFlags.noCaret) {
487 let location = { line: aLine -1, ch: aFlags.columnOffset || 0 };
488 this.editor.setCursor(location, aFlags.align);
489 }
490 if (!aFlags.noDebug) {
491 this.editor.setDebugLocation(aLine - 1);
492 }
493 window.emit(EVENTS.EDITOR_LOCATION_SET);
494 }).then(null, console.error);
495 },
497 /**
498 * Gets the visibility state of the instruments pane.
499 * @return boolean
500 */
501 get instrumentsPaneHidden()
502 this._instrumentsPane.hasAttribute("pane-collapsed"),
504 /**
505 * Gets the currently selected tab in the instruments pane.
506 * @return string
507 */
508 get instrumentsPaneTab()
509 this._instrumentsPane.selectedTab.id,
511 /**
512 * Sets the instruments pane hidden or visible.
513 *
514 * @param object aFlags
515 * An object containing some of the following properties:
516 * - visible: true if the pane should be shown, false to hide
517 * - animated: true to display an animation on toggle
518 * - delayed: true to wait a few cycles before toggle
519 * - callback: a function to invoke when the toggle finishes
520 * @param number aTabIndex [optional]
521 * The index of the intended selected tab in the details pane.
522 */
523 toggleInstrumentsPane: function(aFlags, aTabIndex) {
524 let pane = this._instrumentsPane;
525 let button = this._instrumentsPaneToggleButton;
527 ViewHelpers.togglePane(aFlags, pane);
529 if (aFlags.visible) {
530 button.removeAttribute("pane-collapsed");
531 button.setAttribute("tooltiptext", this._collapsePaneString);
532 } else {
533 button.setAttribute("pane-collapsed", "");
534 button.setAttribute("tooltiptext", this._expandPaneString);
535 }
537 if (aTabIndex !== undefined) {
538 pane.selectedIndex = aTabIndex;
539 }
540 },
542 /**
543 * Sets the instruments pane visible after a short period of time.
544 *
545 * @param function aCallback
546 * A function to invoke when the toggle finishes.
547 */
548 showInstrumentsPane: function(aCallback) {
549 DebuggerView.toggleInstrumentsPane({
550 visible: true,
551 animated: true,
552 delayed: true,
553 callback: aCallback
554 }, 0);
555 },
557 /**
558 * Handles a tab selection event on the instruments pane.
559 */
560 _onInstrumentsPaneTabSelect: function() {
561 if (this._instrumentsPane.selectedTab.id == "events-tab") {
562 DebuggerController.Breakpoints.DOM.scheduleEventListenersFetch();
563 }
564 },
566 /**
567 * Handles a host change event issued by the parent toolbox.
568 *
569 * @param string aType
570 * The host type, either "bottom", "side" or "window".
571 */
572 handleHostChanged: function(aType) {
573 let newLayout = "";
575 if (aType == "side") {
576 newLayout = "vertical";
577 this._enterVerticalLayout();
578 } else {
579 newLayout = "horizontal";
580 this._enterHorizontalLayout();
581 }
583 this._hostType = aType;
584 this._body.setAttribute("layout", newLayout);
585 window.emit(EVENTS.LAYOUT_CHANGED, newLayout);
586 },
588 /**
589 * Switches the debugger widgets to a horizontal layout.
590 */
591 _enterVerticalLayout: function() {
592 let normContainer = document.getElementById("debugger-widgets");
593 let vertContainer = document.getElementById("vertical-layout-panes-container");
595 // Move the soruces and instruments panes in a different container.
596 let splitter = document.getElementById("sources-and-instruments-splitter");
597 vertContainer.insertBefore(this._sourcesPane, splitter);
598 vertContainer.appendChild(this._instrumentsPane);
600 // Make sure the vertical layout container's height doesn't repeatedly
601 // grow or shrink based on the displayed sources, variables etc.
602 vertContainer.setAttribute("height",
603 vertContainer.getBoundingClientRect().height);
604 },
606 /**
607 * Switches the debugger widgets to a vertical layout.
608 */
609 _enterHorizontalLayout: function() {
610 let normContainer = document.getElementById("debugger-widgets");
611 let vertContainer = document.getElementById("vertical-layout-panes-container");
613 // The sources and instruments pane need to be inserted at their
614 // previous locations in their normal container.
615 let splitter = document.getElementById("sources-and-editor-splitter");
616 normContainer.insertBefore(this._sourcesPane, splitter);
617 normContainer.appendChild(this._instrumentsPane);
619 // Revert to the preferred sources and instruments widths, because
620 // they flexed in the vertical layout.
621 this._sourcesPane.setAttribute("width", Prefs.sourcesWidth);
622 this._instrumentsPane.setAttribute("width", Prefs.instrumentsWidth);
623 },
625 /**
626 * Handles any initialization on a tab navigation event issued by the client.
627 */
628 handleTabNavigation: function() {
629 dumpn("Handling tab navigation in the DebuggerView");
631 this.Filtering.clearSearch();
632 this.FilteredSources.clearView();
633 this.FilteredFunctions.clearView();
634 this.GlobalSearch.clearView();
635 this.StackFrames.empty();
636 this.Sources.empty();
637 this.Variables.empty();
638 this.EventListeners.empty();
640 if (this.editor) {
641 this.editor.setMode(Editor.modes.text);
642 this.editor.setText("");
643 this.editor.clearHistory();
644 this._editorSource = {};
645 }
647 this.Sources.emptyText = L10N.getStr("loadingSourcesText");
648 },
650 _startup: null,
651 _shutdown: null,
652 Toolbar: null,
653 Options: null,
654 Filtering: null,
655 FilteredSources: null,
656 FilteredFunctions: null,
657 GlobalSearch: null,
658 StackFrames: null,
659 Sources: null,
660 Tracer: null,
661 Variables: null,
662 VariableBubble: null,
663 WatchExpressions: null,
664 EventListeners: null,
665 editor: null,
666 _editorSource: {},
667 _loadingText: "",
668 _body: null,
669 _editorDeck: null,
670 _sourcesPane: null,
671 _instrumentsPane: null,
672 _instrumentsPaneToggleButton: null,
673 _collapsePaneString: "",
674 _expandPaneString: ""
675 };
677 /**
678 * A custom items container, used for displaying views like the
679 * FilteredSources, FilteredFunctions etc., inheriting the generic WidgetMethods.
680 */
681 function ResultsPanelContainer() {
682 }
684 ResultsPanelContainer.prototype = Heritage.extend(WidgetMethods, {
685 /**
686 * Sets the anchor node for this container panel.
687 * @param nsIDOMNode aNode
688 */
689 set anchor(aNode) {
690 this._anchor = aNode;
692 // If the anchor node is not null, create a panel to attach to the anchor
693 // when showing the popup.
694 if (aNode) {
695 if (!this._panel) {
696 this._panel = document.createElement("panel");
697 this._panel.id = "results-panel";
698 this._panel.setAttribute("level", "top");
699 this._panel.setAttribute("noautofocus", "true");
700 this._panel.setAttribute("consumeoutsideclicks", "false");
701 document.documentElement.appendChild(this._panel);
702 }
703 if (!this.widget) {
704 this.widget = new SimpleListWidget(this._panel);
705 this.autoFocusOnFirstItem = false;
706 this.autoFocusOnSelection = false;
707 this.maintainSelectionVisible = false;
708 }
709 }
710 // Cleanup the anchor and remove the previously created panel.
711 else {
712 this._panel.remove();
713 this._panel = null;
714 this.widget = null;
715 }
716 },
718 /**
719 * Gets the anchor node for this container panel.
720 * @return nsIDOMNode
721 */
722 get anchor() {
723 return this._anchor;
724 },
726 /**
727 * Sets the container panel hidden or visible. It's hidden by default.
728 * @param boolean aFlag
729 */
730 set hidden(aFlag) {
731 if (aFlag) {
732 this._panel.hidden = true;
733 this._panel.hidePopup();
734 } else {
735 this._panel.hidden = false;
736 this._panel.openPopup(this._anchor, this.position, this.left, this.top);
737 }
738 },
740 /**
741 * Gets this container's visibility state.
742 * @return boolean
743 */
744 get hidden()
745 this._panel.state == "closed" ||
746 this._panel.state == "hiding",
748 /**
749 * Removes all items from this container and hides it.
750 */
751 clearView: function() {
752 this.hidden = true;
753 this.empty();
754 },
756 /**
757 * Selects the next found item in this container.
758 * Does not change the currently focused node.
759 */
760 selectNext: function() {
761 let nextIndex = this.selectedIndex + 1;
762 if (nextIndex >= this.itemCount) {
763 nextIndex = 0;
764 }
765 this.selectedItem = this.getItemAtIndex(nextIndex);
766 },
768 /**
769 * Selects the previously found item in this container.
770 * Does not change the currently focused node.
771 */
772 selectPrev: function() {
773 let prevIndex = this.selectedIndex - 1;
774 if (prevIndex < 0) {
775 prevIndex = this.itemCount - 1;
776 }
777 this.selectedItem = this.getItemAtIndex(prevIndex);
778 },
780 /**
781 * Customization function for creating an item's UI.
782 *
783 * @param string aLabel
784 * The item's label string.
785 * @param string aBeforeLabel
786 * An optional string shown before the label.
787 * @param string aBelowLabel
788 * An optional string shown underneath the label.
789 */
790 _createItemView: function(aLabel, aBelowLabel, aBeforeLabel) {
791 let container = document.createElement("vbox");
792 container.className = "results-panel-item";
794 let firstRowLabels = document.createElement("hbox");
795 let secondRowLabels = document.createElement("hbox");
797 if (aBeforeLabel) {
798 let beforeLabelNode = document.createElement("label");
799 beforeLabelNode.className = "plain results-panel-item-label-before";
800 beforeLabelNode.setAttribute("value", aBeforeLabel);
801 firstRowLabels.appendChild(beforeLabelNode);
802 }
804 let labelNode = document.createElement("label");
805 labelNode.className = "plain results-panel-item-label";
806 labelNode.setAttribute("value", aLabel);
807 firstRowLabels.appendChild(labelNode);
809 if (aBelowLabel) {
810 let belowLabelNode = document.createElement("label");
811 belowLabelNode.className = "plain results-panel-item-label-below";
812 belowLabelNode.setAttribute("value", aBelowLabel);
813 secondRowLabels.appendChild(belowLabelNode);
814 }
816 container.appendChild(firstRowLabels);
817 container.appendChild(secondRowLabels);
819 return container;
820 },
822 _anchor: null,
823 _panel: null,
824 position: RESULTS_PANEL_POPUP_POSITION,
825 left: 0,
826 top: 0
827 });