From 9ef83a1828d7785e4b0ed743a858ba8748e2dcf9 Mon Sep 17 00:00:00 2001 From: Peter Flynn Date: Mon, 20 May 2013 18:37:50 -0700 Subject: [PATCH 1/5] Initial PanelManager checkin: - PanelManager provides APIs to create panels below the editor area. JSLint and Find in Files now use these APIs. - Move editor-holder ht calculations from EditorManager to PanelManager - EditorManager listens to PanelManager for resize notifications. PanelManager listens for window resize (moved from EditorManager) and for Resizer events on all bottom panels. The old EditorManager.resizeEditor() API is still used for edge cases such as status & search bar show/hide, extensions that add fixed top panels, and extensions that don't use Resizer APIs to hide/show panels. - Since PanelManager is listening, Resizer no longer pings EditorManager directly about show/hide/resize. Resizer emits events to notify PanelManager about any panels it may have missed (legacy extensions that add panels via Resizer instead of PanelManager). - Add max-size support to Resizer in anticipation of PanelManager helping to fix bug #2015. --- src/editor/EditorManager.js | 71 ++++------ src/extensions/default/JSLint/main.js | 18 +-- src/htmlContent/main-view.html | 11 +- src/htmlContent/search-results.html | 8 ++ src/search/FindInFiles.js | 26 ++-- src/utils/Resizer.js | 31 +++-- src/view/PanelManager.js | 193 ++++++++++++++++++++++++++ src/widgets/StatusBar.js | 24 ++++ 8 files changed, 290 insertions(+), 92 deletions(-) create mode 100644 src/htmlContent/search-results.html create mode 100644 src/view/PanelManager.js diff --git a/src/editor/EditorManager.js b/src/editor/EditorManager.js index 92a59b884fd..5145d25e0b5 100644 --- a/src/editor/EditorManager.js +++ b/src/editor/EditorManager.js @@ -52,6 +52,7 @@ define(function (require, exports, module) { // Load dependent modules var Commands = require("command/Commands"), + PanelManager = require("view/PanelManager"), CommandManager = require("command/CommandManager"), DocumentManager = require("document/DocumentManager"), PerfUtils = require("utils/PerfUtils"), @@ -341,54 +342,44 @@ define(function (require, exports, module) { /** - * Calculates the available height for the full-size Editor (or the no-editor placeholder), - * accounting for the current size of all visible panels, toolbar, & status bar. - * @return {number} - */ - function _calcEditorHeight() { - var availableHt = $(".content").height(); - - _editorHolder.siblings().each(function (i, elem) { - var $elem = $(elem); - if ($elem.css("display") !== "none") { - availableHt -= $elem.outerHeight(); - } - }); - - // Clip value to 0 (it could be negative if a panel wants more space than we have) - return Math.max(availableHt, 0); - } - - /** - * Flag for resizeEditor() to always force refresh. + * Flag for _onEditorAreaResize() to always force refresh. * @const * @type {string} */ var REFRESH_FORCE = "force"; /** - * Flag for resizeEditor() to never refresh. + * Flag for _onEditorAreaResize() to never refresh. * @const * @type {string} */ var REFRESH_SKIP = "skip"; /** - * Resize the editor. This must be called any time the contents of the editor area are swapped - * or any time the editor area might change height. EditorManager takes care of calling this when - * the Editor is swapped, and on window resize. But anyone who changes size/visiblity of editor - * area siblings (toolbar, status bar, bottom panels) *must* manually call resizeEditor(). - * - * @param {string=} refreshFlag For internal use. Set to "force" to ensure the editor will refresh, - * "skip" to ensure the editor does not refresh, or leave undefined to let resizeEditor() determine - * whether it needs to refresh. + * Update the editor area's size: invokes PanelManager to compute the correct size and call us back with it. + * Anyone who changes the size/visibility of editor area siblings without going through PanelManager/Resizer + * *must* manually call resizeEditor(). */ - function resizeEditor(refreshFlag) { + function resizeEditor() { if (!_editorHolder) { return; // still too early during init } + PanelManager._notifyLayoutChange(); + } + + /** + * Update the current CodeMirror's editor's size. Must be called any time the contents of the editor area + * are swapped or any time the editor-holder area has changed height. EditorManager calls us in the swap + * case. PanelManager calls us in the most common height-change cases (panel and/or window resize), but + * some other cases are handled by external code calling resizeEditor() (e.g. ModalBar hide/show. + * + * @param {number} editorAreaHt + * @param {string=} refreshFlag For internal use. Set to "force" to ensure the editor will refresh, + * "skip" to ensure the editor does not refresh, or leave undefined to let _onEditorAreaResize() + * determine whether it needs to refresh. + */ + function _onEditorAreaResize(event, editorAreaHt, refreshFlag) { - var editorAreaHt = _calcEditorHeight(); _editorHolder.height(editorAreaHt); // affects size of "not-editor" placeholder as well if (_currentEditor) { @@ -412,15 +403,6 @@ define(function (require, exports, module) { } } - /** - * NJ's editor-resizing fix. Whenever the window resizes, we immediately adjust the editor's - * height. - */ - function _updateEditorDuringResize() { - // always skip the refresh since CodeMirror will call refresh() itself when it sees the resize event - resizeEditor(REFRESH_SKIP); - } - /** Updates _viewStateCache from the given editor's actual current state */ function _saveEditorViewState(editor) { @@ -465,13 +447,13 @@ define(function (require, exports, module) { _currentEditorsDocument = document; _currentEditor = document._masterEditor; - // Skip refreshing the editor since we're going to refresh it in resizeEditor() later. + // Skip refreshing the editor since we're going to refresh it more explicitly below _currentEditor.setVisible(true, false); _currentEditor.focus(); // Resize and refresh the editor, since it might have changed size or had other edits applied // since it was last visible. - resizeEditor(REFRESH_FORCE); + PanelManager._notifyLayoutChange(REFRESH_FORCE); } /** @@ -705,11 +687,8 @@ define(function (require, exports, module) { $(DocumentManager).on("currentDocumentChange", _onCurrentDocumentChange); $(DocumentManager).on("workingSetRemove", _onWorkingSetRemove); $(DocumentManager).on("workingSetRemoveList", _onWorkingSetRemoveList); + $(PanelManager).on("editorAreaResize", _onEditorAreaResize); - // Add this as a capture handler so we're guaranteed to run it before the editor does its own - // refresh on resize. - window.addEventListener("resize", _updateEditorDuringResize, true); - // For unit tests and internal use only exports._openInlineWidget = _openInlineWidget; exports._createFullEditorForDocument = _createFullEditorForDocument; diff --git a/src/extensions/default/JSLint/main.js b/src/extensions/default/JSLint/main.js index 89fa889d0aa..ae2b96ccadd 100644 --- a/src/extensions/default/JSLint/main.js +++ b/src/extensions/default/JSLint/main.js @@ -36,6 +36,7 @@ define(function (require, exports, module) { // Load dependent modules var Commands = brackets.getModule("command/Commands"), + PanelManager = brackets.getModule("view/PanelManager"), CommandManager = brackets.getModule("command/CommandManager"), Menus = brackets.getModule("command/Menus"), DocumentManager = brackets.getModule("document/DocumentManager"), @@ -156,7 +157,7 @@ define(function (require, exports, module) { EditorManager.focusEditor(); }); - $lintResults.show(); + Resizer.show($lintResults); if (JSLINT.errors.length === 1) { StatusBar.updateIndicator(INDICATOR_ID, true, "jslint-errors", Strings.JSLINT_ERROR_INFORMATION); } else { @@ -175,7 +176,7 @@ define(function (require, exports, module) { setGotoEnabled(true); } else { - $lintResults.hide(); + Resizer.hide($lintResults); StatusBar.updateIndicator(INDICATOR_ID, true, "jslint-valid", Strings.JSLINT_NO_ERRORS); setGotoEnabled(false); } @@ -184,12 +185,10 @@ define(function (require, exports, module) { } else { // JSLint is disabled or does not apply to the current file, hide the results - $lintResults.hide(); + Resizer.hide($lintResults); StatusBar.updateIndicator(INDICATOR_ID, true, "jslint-disabled", Strings.JSLINT_DISABLED); setGotoEnabled(false); } - - EditorManager.resizeEditor(); } /** @@ -263,20 +262,15 @@ define(function (require, exports, module) { ExtensionUtils.loadStyleSheet(module, "jslint.css"); var jsLintHtml = Mustache.render(JSLintTemplate, Strings); - $(jsLintHtml).insertBefore("#status-bar"); + var resultsPanel = PanelManager.createBottomPanel("jslint.results", $(jsLintHtml), 100); + $lintResults = $("#jslint-results"); var goldStarHtml = Mustache.render("
★
", Strings); $(goldStarHtml).insertBefore("#status-file"); - $lintResults = $("#jslint-results"); - StatusBar.addIndicator(INDICATOR_ID, $("#gold-star")); // Called on HTML ready to trigger the initial UI state setEnabled(_prefs.getValue("enabled")); - - // AppInit.htmlReady() has already executed before extensions are loaded - // so, for now, we need to call this ourself - Resizer.makeResizable($lintResults.get(0), "vert", "top", 100); }); }); diff --git a/src/htmlContent/main-view.html b/src/htmlContent/main-view.html index fff3db4d42a..a66abe19c62 100644 --- a/src/htmlContent/main-view.html +++ b/src/htmlContent/main-view.html @@ -62,7 +62,7 @@ Note: all children must be in a vertical stack with heights explicitly set in a fixed unit such as px (not percent/em/auto). If you change the height later, you must call EditorManager.resizeEditor() each time. Otherwise editor-holder's height will - not get set correctly. + not get set correctly. Use PanelManager to have this managed for you automatically. -->
@@ -83,14 +83,7 @@
-
-
-
{{SEARCH_RESULTS}}
-
- × -
-
-
+ diff --git a/src/htmlContent/search-results.html b/src/htmlContent/search-results.html new file mode 100644 index 00000000000..e46d823f469 --- /dev/null +++ b/src/htmlContent/search-results.html @@ -0,0 +1,8 @@ +
+
+
{{SEARCH_RESULTS}}
+
+ × +
+
+
diff --git a/src/search/FindInFiles.js b/src/search/FindInFiles.js index 756cfc6805f..0ef38d3b96c 100644 --- a/src/search/FindInFiles.js +++ b/src/search/FindInFiles.js @@ -22,7 +22,7 @@ */ /*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ -/*global define, $, PathUtils, window */ +/*global define, $, PathUtils, window, Mustache */ /* * Adds a "find in files" command to allow the user to find all occurances of a string in all files in @@ -43,6 +43,7 @@ define(function (require, exports, module) { "use strict"; var Async = require("utils/Async"), + Resizer = require("utils/Resizer"), CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), Strings = require("strings"), @@ -50,12 +51,15 @@ define(function (require, exports, module) { ProjectManager = require("project/ProjectManager"), DocumentManager = require("document/DocumentManager"), EditorManager = require("editor/EditorManager"), + PanelManager = require("view/PanelManager"), FileIndexManager = require("project/FileIndexManager"), FileUtils = require("file/FileUtils"), KeyEvent = require("utils/KeyEvent"), AppInit = require("utils/AppInit"), StatusBar = require("widgets/StatusBar"), ModalBar = require("widgets/ModalBar").ModalBar; + + var searchResultsTemplate = require("text!htmlContent/search-results.html"); var searchResults = []; @@ -64,8 +68,8 @@ define(function (require, exports, module) { currentQuery = "", currentScope; - // Div holding the search results. Initialized in htmlReady(). - var $searchResultsDiv; + // Bottom panel holding the search results. Initialized in htmlReady(). + var searchResultsPanel; function _getQueryRegExp(query) { // Clear any pending RegEx error message @@ -193,9 +197,8 @@ define(function (require, exports, module) { }; function _hideSearchResults() { - if ($searchResultsDiv.is(":visible")) { - $searchResultsDiv.hide(); - EditorManager.resizeEditor(); + if (searchResultsPanel.isVisible()) { + searchResultsPanel.hide(); } } @@ -340,12 +343,10 @@ define(function (require, exports, module) { _hideSearchResults(); }); - $searchResultsDiv.show(); + searchResultsPanel.show(); } else { _hideSearchResults(); } - - EditorManager.resizeEditor(); } /** @@ -444,11 +445,12 @@ define(function (require, exports, module) { // Initialize items dependent on HTML DOM AppInit.htmlReady(function () { - $searchResultsDiv = $("#search-results"); + var panelHtml = Mustache.render(searchResultsTemplate, Strings); + searchResultsPanel = PanelManager.createBottomPanel("search-results", $(panelHtml)); }); function _fileNameChangeHandler(event, oldName, newName) { - if ($searchResultsDiv.is(":visible")) { + if (searchResultsPanel.isVisible()) { // Update the search results searchResults.forEach(function (item) { item.fullPath = item.fullPath.replace(oldName, newName); @@ -458,7 +460,7 @@ define(function (require, exports, module) { } function _pathDeletedHandler(event, path) { - if ($searchResultsDiv.is(":visible")) { + if (searchResultsPanel.isVisible()) { // Update the search results searchResults.forEach(function (item, idx) { if (FileUtils.isAffectedWhenRenaming(item.fullPath, path)) { diff --git a/src/utils/Resizer.js b/src/utils/Resizer.js index f8d967cd2b4..77c8bef2216 100644 --- a/src/utils/Resizer.js +++ b/src/utils/Resizer.js @@ -60,8 +60,7 @@ define(function (require, exports, module) { // Load dependent modules var AppInit = require("utils/AppInit"), - PreferencesManager = require("preferences/PreferencesManager"), - EditorManager = require("editor/EditorManager"); + PreferencesManager = require("preferences/PreferencesManager"); /** * @private @@ -145,11 +144,13 @@ define(function (require, exports, module) { * @param {?string} forceLeft CSS selector indicating element whose 'left' should be locked to the * the resizable element's size (useful for siblings laid out to the right of * the element). Must lie in element's parent's subtree. + * @param {?boolean} createdByPanelManager For internal use only */ - function makeResizable(element, direction, position, minSize, collapsible, forceLeft) { + function makeResizable(element, direction, position, minSize, collapsible, forceLeft, createdByPanelManager) { var $resizer = $('
'), $element = $(element), + $parent = $element.parent(), $resizableElement = $($element.find(".resizable-content:first")[0]), $body = $(window.document.body), elementID = $element.attr("id"), @@ -166,9 +167,15 @@ define(function (require, exports, module) { $element.prepend($resizer); + + if ($parent[0] && $parent.is(".content") && !createdByPanelManager) { + console.warn("Deprecated: resizable panels should be created via PanelManager.createBottomPanel(). Using Resizer directly will stop working in the future. \nElement:", element); + $(exports).triggerHandler("deprecatedPanelAdded", [$element]); + } + function adjustSibling(size) { if (forceLeft !== undefined) { - $(forceLeft, $element.parent()).css("left", size); + $(forceLeft, $parent).css("left", size); } } @@ -205,9 +212,6 @@ define(function (require, exports, module) { adjustSibling(elementSize); - // Vertical resize affects editor directly; horizontal resize could change height of top toolbar - EditorManager.resizeEditor(); - $element.trigger("panelExpanded", [elementSize]); _prefs.setValue(elementID, elementPrefs); }); @@ -230,9 +234,6 @@ define(function (require, exports, module) { adjustSibling(0); - // Vertical resize affects editor directly; horizontal resize could change height of top toolbar - EditorManager.resizeEditor(); - $element.trigger("panelCollapsed", [elementSize]); _prefs.setValue(elementID, elementPrefs); }); @@ -307,9 +308,6 @@ define(function (require, exports, module) { $element.trigger("panelResizeStart", newSize); } } - - // Vertical resize affects editor directly; horizontal resize could change height of top toolbar - EditorManager.resizeEditor(); } animationRequest = window.webkitRequestAnimationFrame(doRedraw); @@ -319,6 +317,13 @@ define(function (require, exports, module) { // calculate newSize adding to startSize the difference // between starting and current position, capped at minSize newSize = Math.max(startSize + directionIncrement * (startPosition - e[directionProperty]), minSize); + + // respect max size if one provided (e.g. by PanelManager) + var maxSize = $element.data("maxsize"); + if (maxSize !== undefined) { + newSize = Math.min(newSize, maxSize); + } + e.preventDefault(); if (animationRequest === null) { diff --git a/src/view/PanelManager.js b/src/view/PanelManager.js new file mode 100644 index 00000000000..fe7edb0b1bc --- /dev/null +++ b/src/view/PanelManager.js @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ +/*global define, window, $, brackets */ + +/** + * Manages panels surrounding the editor area. + * + * Updates panel sizes when the window is resized. Maintains the max resizing limits for panels, based on + * currently available window size. Persists & restores panel sizes across sessions. + * + * Events: + * - editorAreaResize -- When editor-holder's size changes for any reason (including panel show/hide + * panel resize, or the window resize). + * The 2nd arg is the new editor-holder height. + * The 3rd arg is a refreshHint flag for internal EditorManager use. + */ +define(function (require, exports, module) { + "use strict"; + + var AppInit = require("utils/AppInit"), + EditorManager = require("editor/EditorManager"), + Resizer = require("utils/Resizer"); + + + /** @type {jQueryObject} The ".content" vertical stack (editor + all header/footer panels) */ + var $windowContent; + + /** @type {jQueryObject} The "#editor-holder": has only one visible child, the current CodeMirror + instance (or the no-editor placeholder) */ + var $editorHolder; + + + /** + * Calculates the available height for the full-size Editor (or the no-editor placeholder), + * accounting for the current size of all visible panels, toolbar, & status bar. + * @return {number} + */ + function _calcEditorHeight() { + var availableHt = $windowContent.height(); + + $editorHolder.siblings().each(function (i, elem) { + var $elem = $(elem); + if ($elem.css("display") !== "none") { + availableHt -= $elem.outerHeight(); + } + }); + + // Clip value to 0 (it could be negative if a panel wants more space than we have) + return Math.max(availableHt, 0); + } + + + /** + * Calculates a new size for editor-holder and dispatches a change event. Does not actually update + * editor-holder's size (EditorManager does that in response to our event). + * + * @param {string=} refreshHint One of "skip", "force", or undefined. See EditorManager docs. + */ + function triggerEditorResize(refreshHint) { + var editorAreaHeight = _calcEditorHeight(); + + // TODO: update panel max heights + + $(exports).trigger("editorAreaResize", [editorAreaHeight, refreshHint]); + } + + + /** Trigger editor area resize whenever the window is resized */ + function handleWindowResize() { + // Immediately adjust editor's height, but skip the refresh since CodeMirror will call refresh() + // itself when it sees the window resize event + triggerEditorResize("skip"); + } + + /** Trigger editor area resize whenever the given panel is shown/hidden/resized */ + function listenToResize($panel) { + $panel.on("panelCollapsed panelExpanded panelResizeUpdate", function () { + triggerEditorResize(); + }); + } + + + /** + * Represents a panel below the editor area (a child of ".content"). + * + * @param {!jQueryObject} $panel The entire panel, including any chrome, already in the DOM. + * @param {number=} minSize Minimum height of panel in px; default is 0 + */ + function Panel($panel, minSize) { + this.$panel = $panel; + + Resizer.makeResizable($panel[0], Resizer.DIRECTION_VERTICAL, Resizer.POSITION_TOP, minSize, false, undefined, true); + listenToResize($panel); + } + + /** @type {jQueryObject} */ + Panel.prototype.$panel = null; + + Panel.prototype.isVisible = function () { + return this.$panel.is(":visible"); + }; + + Panel.prototype.show = function () { + Resizer.show(this.$panel[0]); + }; + Panel.prototype.hide = function () { + Resizer.hide(this.$panel[0]); + }; + + Panel.prototype.setVisible = function (visible) { + if (visible) { + Resizer.show(this.$panel[0]); + } else { + Resizer.hide(this.$panel[0]); + } + }; + + + /** + * Creates a new panel beneath the editor area and above the status bar footer. Panel is initially invisible. + * + * @param {!string} id Unique id for this panel. Use package-style naming, e.g. "myextension.feature.panelname" + * @param {!jQueryObject} $panel DOM content to use as the panel. Need not be in the document yet. + * @param {number=} minSize Minimum height of panel in px; default is 0 + * @return {!Panel} + */ + function createBottomPanel(id, $panel, minSize) { // TODO: allow retrieval by id... + $panel.insertBefore("#status-bar"); + $panel.hide(); + return new Panel($panel, minSize); + } + + + /** + * Force PanelManager to recalculate the editor-holder size and trigger an event, in cases that our normal panel + * and window listeners wouldn't detect. + * + * For internal use only: most code should call EditorManager.resizeEditor() for this purpose. + */ + function _notifyLayoutChange(refreshHint) { + triggerEditorResize(refreshHint); + } + + + // Attach to key parts of the overall UI, once created + AppInit.htmlReady(function () { + $windowContent = $(".content"); + $editorHolder = $("#editor-holder"); + + // Sidebar is a special case: a side panel rather than a bottom panel. It could still affect the + // editor-holder's height if changing .content's width causes the inBrowser titlebar to wrap/unwrap. + if (brackets.inBrowser) { + listenToResize($("#sidebar")); + } + }); + + // If someone adds a panel in the .content stack the old way, make sure we still listen for resize/show/hide + // (Resizer emits a deprecation warning for us - no need to log anything here) + $(Resizer).on("deprecatedPanelAdded", function (event, $panel) { + listenToResize($panel); + }); + + // Add this as a capture handler so we're guaranteed to run it before the editor does its own + // refresh on resize. + window.addEventListener("resize", handleWindowResize, true); + + + // Define public API + exports.createBottomPanel = createBottomPanel; + exports._notifyLayoutChange = _notifyLayoutChange; +}); diff --git a/src/widgets/StatusBar.js b/src/widgets/StatusBar.js index d195611c24b..deec2e13a60 100644 --- a/src/widgets/StatusBar.js +++ b/src/widgets/StatusBar.js @@ -1,3 +1,27 @@ +/* + * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + + /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, brackets, window, document, Mustache */ From 6486337d8436f3994098dafc60b1d3b4a88c7f73 Mon Sep 17 00:00:00 2001 From: Peter Flynn Date: Tue, 21 May 2013 12:00:21 -0700 Subject: [PATCH 2/5] Fix part of #2015 by keeping panel resize limits up to date as other panels resize or overall available `.content` height changes (window resize). Change resizer min/max limits to operate in terms of panel outer height. --- src/utils/Resizer.js | 6 ++++- src/view/PanelManager.js | 49 +++++++++++++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/utils/Resizer.js b/src/utils/Resizer.js index 77c8bef2216..c97f7d38748 100644 --- a/src/utils/Resizer.js +++ b/src/utils/Resizer.js @@ -138,7 +138,8 @@ define(function (require, exports, module) { * @param {!string} direction Direction of the resize action: one of the DIRECTION_* constants. * @param {!string} position Which side of the element can be dragged: one of the POSITION_* constants * (TOP/BOTTOM for vertical resizing or LEFT/RIGHT for horizontal). - * @param {?number} minSize Minimum size (width or height) of the element. Defaults to 0. + * @param {?number} minSize Minimum size (width or height) of the element's outer dimensions, including + * border & padding. Defaults to 0. * @param {?boolean} collapsible Indicates the panel is collapsible on double click on the * resizer. Defaults to false. * @param {?string} forceLeft CSS selector indicating element whose 'left' should be locked to the @@ -167,7 +168,10 @@ define(function (require, exports, module) { $element.prepend($resizer); + // Important so min/max sizes behave predictably + $element.css("box-sizing", "border-box"); + // Detect legacy cases where panels in the editor area are created without using PanelManager APIs if ($parent[0] && $parent.is(".content") && !createdByPanelManager) { console.warn("Deprecated: resizable panels should be created via PanelManager.createBottomPanel(). Using Resizer directly will stop working in the future. \nElement:", element); $(exports).triggerHandler("deprecatedPanelAdded", [$element]); diff --git a/src/view/PanelManager.js b/src/view/PanelManager.js index fe7edb0b1bc..bbe4f169dbb 100644 --- a/src/view/PanelManager.js +++ b/src/view/PanelManager.js @@ -28,7 +28,7 @@ * Manages panels surrounding the editor area. * * Updates panel sizes when the window is resized. Maintains the max resizing limits for panels, based on - * currently available window size. Persists & restores panel sizes across sessions. + * currently available window size. * * Events: * - editorAreaResize -- When editor-holder's size changes for any reason (including panel show/hide @@ -51,13 +51,16 @@ define(function (require, exports, module) { instance (or the no-editor placeholder) */ var $editorHolder; + /** @type {boolean} Have we already started listening for the end of the ongoing window resize? */ + var windowResizing = false; + /** * Calculates the available height for the full-size Editor (or the no-editor placeholder), * accounting for the current size of all visible panels, toolbar, & status bar. * @return {number} */ - function _calcEditorHeight() { + function calcEditorHeight() { var availableHt = $windowContent.height(); $editorHolder.siblings().each(function (i, elem) { @@ -71,6 +74,20 @@ define(function (require, exports, module) { return Math.max(availableHt, 0); } + /** Updates panel resize limits to disallow making panels big enough to shrink editor area below 0 */ + function updateResizeLimits() { + var editorAreaHeight = $editorHolder.height(); + + $editorHolder.siblings().each(function (i, elem) { + var $elem = $(elem); + if ($elem.css("display") === "none") { + $elem.data("maxsize", editorAreaHeight); + } else { + $elem.data("maxsize", editorAreaHeight + $elem.outerHeight()); + } + }); + } + /** * Calculates a new size for editor-holder and dispatches a change event. Does not actually update @@ -79,10 +96,10 @@ define(function (require, exports, module) { * @param {string=} refreshHint One of "skip", "force", or undefined. See EditorManager docs. */ function triggerEditorResize(refreshHint) { - var editorAreaHeight = _calcEditorHeight(); - - // TODO: update panel max heights + // Find how much space is left for the editor + var editorAreaHeight = calcEditorHeight(); + // Resize editor to fill the space $(exports).trigger("editorAreaResize", [editorAreaHeight, refreshHint]); } @@ -92,13 +109,30 @@ define(function (require, exports, module) { // Immediately adjust editor's height, but skip the refresh since CodeMirror will call refresh() // itself when it sees the window resize event triggerEditorResize("skip"); + + if (!windowResizing) { + windowResizing = true; + + // We don't need any fancy debouncing here - we just need to react before the user can start + // resizing any panels at the new window size. So just listen for first mousemove once the + // window resize releases mouse capture. + $(window.document).one("mousemove", function () { + windowResizing = false; + updateResizeLimits(); + }); + } } /** Trigger editor area resize whenever the given panel is shown/hidden/resized */ function listenToResize($panel) { + // Update editor height when shown/hidden, & continuously as panel is resized $panel.on("panelCollapsed panelExpanded panelResizeUpdate", function () { triggerEditorResize(); }); + // Update max size of sibling panels when shown/hidden, & at *end* of resize gesture + $panel.on("panelCollapsed panelExpanded panelResizeEnd", function () { + updateResizeLimits(); + }); } @@ -146,9 +180,11 @@ define(function (require, exports, module) { * @param {number=} minSize Minimum height of panel in px; default is 0 * @return {!Panel} */ - function createBottomPanel(id, $panel, minSize) { // TODO: allow retrieval by id... + function createBottomPanel(id, $panel, minSize) { $panel.insertBefore("#status-bar"); $panel.hide(); + updateResizeLimits(); // initialize panel's max size + return new Panel($panel, minSize); } @@ -161,6 +197,7 @@ define(function (require, exports, module) { */ function _notifyLayoutChange(refreshHint) { triggerEditorResize(refreshHint); + updateResizeLimits(); } From 61c0dbe47d9e294a548033e01ffea78c597c019f Mon Sep 17 00:00:00 2001 From: Peter Flynn Date: Tue, 21 May 2013 15:41:59 -0700 Subject: [PATCH 3/5] Code review: clarify docs, update panel id --- src/editor/EditorManager.js | 12 ++++++------ src/search/FindInFiles.js | 2 +- src/view/PanelManager.js | 7 ++----- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/editor/EditorManager.js b/src/editor/EditorManager.js index ff7b5f94b21..b798a61ed8f 100644 --- a/src/editor/EditorManager.js +++ b/src/editor/EditorManager.js @@ -355,23 +355,23 @@ define(function (require, exports, module) { */ var REFRESH_SKIP = "skip"; - /** - * Update the editor area's size: invokes PanelManager to compute the correct size and call us back with it. - * Anyone who changes the size/visibility of editor area siblings without going through PanelManager/Resizer - * *must* manually call resizeEditor(). + /** + * Must be called whenever the size/visibility of editor area siblings is changed without going through + * PanelManager or Resizer. Resizable panels created via PanelManager do not require this manual call. */ function resizeEditor() { if (!_editorHolder) { return; // still too early during init } + // PanelManager computes the correct editor-holder size & calls us back with it, via _onEditorAreaResize() PanelManager._notifyLayoutChange(); } /** - * Update the current CodeMirror's editor's size. Must be called any time the contents of the editor area + * Update the current CodeMirror editor's size. Must be called any time the contents of the editor area * are swapped or any time the editor-holder area has changed height. EditorManager calls us in the swap * case. PanelManager calls us in the most common height-change cases (panel and/or window resize), but - * some other cases are handled by external code calling resizeEditor() (e.g. ModalBar hide/show. + * some other cases are handled by external code calling resizeEditor() (e.g. ModalBar hide/show). * * @param {number} editorAreaHt * @param {string=} refreshFlag For internal use. Set to "force" to ensure the editor will refresh, diff --git a/src/search/FindInFiles.js b/src/search/FindInFiles.js index 0ef38d3b96c..832dbd01197 100644 --- a/src/search/FindInFiles.js +++ b/src/search/FindInFiles.js @@ -446,7 +446,7 @@ define(function (require, exports, module) { // Initialize items dependent on HTML DOM AppInit.htmlReady(function () { var panelHtml = Mustache.render(searchResultsTemplate, Strings); - searchResultsPanel = PanelManager.createBottomPanel("search-results", $(panelHtml)); + searchResultsPanel = PanelManager.createBottomPanel("find-in-files.results", $(panelHtml)); }); function _fileNameChangeHandler(event, oldName, newName) { diff --git a/src/view/PanelManager.js b/src/view/PanelManager.js index bbe4f169dbb..22fb418a739 100644 --- a/src/view/PanelManager.js +++ b/src/view/PanelManager.js @@ -40,7 +40,6 @@ define(function (require, exports, module) { "use strict"; var AppInit = require("utils/AppInit"), - EditorManager = require("editor/EditorManager"), Resizer = require("utils/Resizer"); @@ -190,10 +189,8 @@ define(function (require, exports, module) { /** - * Force PanelManager to recalculate the editor-holder size and trigger an event, in cases that our normal panel - * and window listeners wouldn't detect. - * - * For internal use only: most code should call EditorManager.resizeEditor() for this purpose. + * Used by EditorManager to notify us of layout changes our normal panel/window listeners wouldn't detect. + * For internal use only: most code should call EditorManager.resizeEditor(). */ function _notifyLayoutChange(refreshHint) { triggerEditorResize(refreshHint); From 246d72ca7a2f6a9281a6e26fe4aa976375b7776a Mon Sep 17 00:00:00 2001 From: Peter Flynn Date: Tue, 21 May 2013 16:42:08 -0700 Subject: [PATCH 4/5] * Fix unit tests by ensuring PanelManager is fully initialized before createMockEditor() calls EditorManager.setEditorHolder() to inject mock DOM. * Cleanup: move responsibility for setting editor-holder height from EditorManager into PanelManager (cleaner division of labor). --- src/editor/EditorManager.js | 2 -- src/view/PanelManager.js | 15 +++++++++--- test/spec/EditorManager-test.js | 42 +++++++++++++++++++-------------- test/spec/SpecRunnerUtils.js | 5 +++- 4 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/editor/EditorManager.js b/src/editor/EditorManager.js index b798a61ed8f..06dcf721e9c 100644 --- a/src/editor/EditorManager.js +++ b/src/editor/EditorManager.js @@ -380,8 +380,6 @@ define(function (require, exports, module) { */ function _onEditorAreaResize(event, editorAreaHt, refreshFlag) { - _editorHolder.height(editorAreaHt); // affects size of "not-editor" placeholder as well - if (_currentEditor) { var curRoot = _currentEditor.getRootElement(), curWidth = $(curRoot).width(); diff --git a/src/view/PanelManager.js b/src/view/PanelManager.js index 22fb418a739..28f39c5d806 100644 --- a/src/view/PanelManager.js +++ b/src/view/PanelManager.js @@ -25,7 +25,7 @@ /*global define, window, $, brackets */ /** - * Manages panels surrounding the editor area. + * Manages layout of panels surrounding the editor area, and size of the editor area (but not its contents). * * Updates panel sizes when the window is resized. Maintains the max resizing limits for panels, based on * currently available window size. @@ -89,8 +89,8 @@ define(function (require, exports, module) { /** - * Calculates a new size for editor-holder and dispatches a change event. Does not actually update - * editor-holder's size (EditorManager does that in response to our event). + * Calculates a new size for editor-holder and resizes it accordingly, then and dispatches the "editorAreaResize" + * event. (The editors within are resized by EditorManager, in response to that event). * * @param {string=} refreshHint One of "skip", "force", or undefined. See EditorManager docs. */ @@ -98,6 +98,8 @@ define(function (require, exports, module) { // Find how much space is left for the editor var editorAreaHeight = calcEditorHeight(); + $editorHolder.height(editorAreaHeight); // affects size of "not-editor" placeholder as well + // Resize editor to fill the space $(exports).trigger("editorAreaResize", [editorAreaHeight, refreshHint]); } @@ -210,6 +212,12 @@ define(function (require, exports, module) { } }); + // Unit test only: allow passing in mock DOM notes, e.g. for use with SpecRunnerUtils.createMockEditor() + function _setMockDOM($mockWindowContent, $mockEditorHolder) { + $windowContent = $mockWindowContent; + $editorHolder = $mockEditorHolder; + } + // If someone adds a panel in the .content stack the old way, make sure we still listen for resize/show/hide // (Resizer emits a deprecation warning for us - no need to log anything here) $(Resizer).on("deprecatedPanelAdded", function (event, $panel) { @@ -224,4 +232,5 @@ define(function (require, exports, module) { // Define public API exports.createBottomPanel = createBottomPanel; exports._notifyLayoutChange = _notifyLayoutChange; + exports._setMockDOM = _setMockDOM; }); diff --git a/test/spec/EditorManager-test.js b/test/spec/EditorManager-test.js index 464127bb5ea..b682694c722 100644 --- a/test/spec/EditorManager-test.js +++ b/test/spec/EditorManager-test.js @@ -29,6 +29,7 @@ define(function (require, exports, module) { 'use strict'; var EditorManager = require("editor/EditorManager"), + PanelManager = require("view/PanelManager"), SpecRunnerUtils = require("spec/SpecRunnerUtils"); describe("EditorManager", function () { @@ -37,20 +38,25 @@ define(function (require, exports, module) { var testEditor, testDoc, $root, $fakeContentDiv; beforeEach(function () { - var mock = SpecRunnerUtils.createMockEditor(""); - testEditor = mock.editor; - testDoc = mock.doc; - $root = $(testEditor.getRootElement()); - // Normally the editor holder would be created inside a "content" div, which is // used in the available height calculation. We create a fake content div just to - // hold the height, and move the editor holder into it. + // hold the height, and we'll place the editor holder in it. $fakeContentDiv = $("
") .css("height", "200px") .appendTo(document.body); + + // createMockEditor() creates the mock-editor-holder, and links EditorManager/PanelManager + // to it (and links PanelManager to our content div) + var mock = SpecRunnerUtils.createMockEditor(""); + + // move newly created mock-editor-holder into the content div we created above $("#mock-editor-holder") .appendTo($fakeContentDiv); + testEditor = mock.editor; + testDoc = mock.doc; + $root = $(testEditor.getRootElement()); + testDoc._masterEditor = testEditor; EditorManager._doShow(testDoc); }); @@ -72,7 +78,7 @@ define(function (require, exports, module) { EditorManager.resizeEditor(); // cache the width spyOn(testEditor, "refreshAll"); - EditorManager.resizeEditor(EditorManager.REFRESH_FORCE); + PanelManager._notifyLayoutChange(EditorManager.REFRESH_FORCE); expect(testEditor.refreshAll).toHaveBeenCalled(); }); @@ -83,7 +89,7 @@ define(function (require, exports, module) { $root.width(300); // change the width spyOn(testEditor, "refreshAll"); - EditorManager.resizeEditor(EditorManager.REFRESH_FORCE); + PanelManager._notifyLayoutChange(EditorManager.REFRESH_FORCE); expect(testEditor.refreshAll).toHaveBeenCalled(); }); @@ -94,7 +100,7 @@ define(function (require, exports, module) { $root.height(300); // change the height (to be different from content div) spyOn(testEditor, "refreshAll"); - EditorManager.resizeEditor(EditorManager.REFRESH_FORCE); + PanelManager._notifyLayoutChange(EditorManager.REFRESH_FORCE); expect(testEditor.refreshAll).toHaveBeenCalled(); }); @@ -106,7 +112,7 @@ define(function (require, exports, module) { $root.width(300); // change the width spyOn(testEditor, "refreshAll"); - EditorManager.resizeEditor(EditorManager.REFRESH_FORCE); + PanelManager._notifyLayoutChange(EditorManager.REFRESH_FORCE); expect(testEditor.refreshAll).toHaveBeenCalled(); }); @@ -117,7 +123,7 @@ define(function (require, exports, module) { EditorManager.resizeEditor(); // cache the width spyOn(testEditor, "refreshAll"); - EditorManager.resizeEditor(EditorManager.REFRESH_SKIP); + PanelManager._notifyLayoutChange(EditorManager.REFRESH_SKIP); expect(testEditor.refreshAll).not.toHaveBeenCalled(); }); @@ -128,7 +134,7 @@ define(function (require, exports, module) { $root.width(300); // change the width spyOn(testEditor, "refreshAll"); - EditorManager.resizeEditor(EditorManager.REFRESH_SKIP); + PanelManager._notifyLayoutChange(EditorManager.REFRESH_SKIP); expect(testEditor.refreshAll).not.toHaveBeenCalled(); }); @@ -139,7 +145,7 @@ define(function (require, exports, module) { $root.height(300); // change the height (to be different from content div) spyOn(testEditor, "refreshAll"); - EditorManager.resizeEditor(EditorManager.REFRESH_SKIP); + PanelManager._notifyLayoutChange(EditorManager.REFRESH_SKIP); expect(testEditor.refreshAll).not.toHaveBeenCalled(); }); @@ -151,7 +157,7 @@ define(function (require, exports, module) { $root.width(300); // change the width spyOn(testEditor, "refreshAll"); - EditorManager.resizeEditor(EditorManager.REFRESH_SKIP); + PanelManager._notifyLayoutChange(EditorManager.REFRESH_SKIP); expect(testEditor.refreshAll).not.toHaveBeenCalled(); }); @@ -162,7 +168,7 @@ define(function (require, exports, module) { EditorManager.resizeEditor(); // cache the width spyOn(testEditor, "refreshAll"); - EditorManager.resizeEditor(); + PanelManager._notifyLayoutChange(); expect(testEditor.refreshAll).not.toHaveBeenCalled(); }); @@ -173,7 +179,7 @@ define(function (require, exports, module) { $root.width(300); // change the width spyOn(testEditor, "refreshAll"); - EditorManager.resizeEditor(); + PanelManager._notifyLayoutChange(); expect(testEditor.refreshAll).toHaveBeenCalled(); }); @@ -184,7 +190,7 @@ define(function (require, exports, module) { $root.height(300); // change the height (to be different from content div) spyOn(testEditor, "refreshAll"); - EditorManager.resizeEditor(); + PanelManager._notifyLayoutChange(); expect(testEditor.refreshAll).toHaveBeenCalled(); }); @@ -196,7 +202,7 @@ define(function (require, exports, module) { $root.width(300); // change the width spyOn(testEditor, "refreshAll"); - EditorManager.resizeEditor(); + PanelManager._notifyLayoutChange(); expect(testEditor.refreshAll).toHaveBeenCalled(); }); }); diff --git a/test/spec/SpecRunnerUtils.js b/test/spec/SpecRunnerUtils.js index bb7c726d98b..7a7fb4be084 100644 --- a/test/spec/SpecRunnerUtils.js +++ b/test/spec/SpecRunnerUtils.js @@ -34,6 +34,7 @@ define(function (require, exports, module) { DocumentManager = require("document/DocumentManager"), Editor = require("editor/Editor").Editor, EditorManager = require("editor/EditorManager"), + PanelManager = require("view/PanelManager"), ExtensionLoader = require("utils/ExtensionLoader"), UrlParams = require("utils/UrlParams").UrlParams, LanguageManager = require("language/LanguageManager"); @@ -210,8 +211,10 @@ define(function (require, exports, module) { * @return {!{doc:{Document}, editor:{Editor}}} */ function createMockEditor(initialContent, languageId, visibleRange) { - // Initialize EditorManager and position the editor-holder offscreen + // Initialize EditorManager/PanelManager and position the editor-holder offscreen + // (".content" may not exist, but that's ok for headless tests where editor height doesn't matter) var $editorHolder = createMockElement().attr("id", "mock-editor-holder"); + PanelManager._setMockDOM($(".content"), $editorHolder); EditorManager.setEditorHolder($editorHolder); // create dummy Document for the Editor From 427914df4b477337d88d0329e4e23bf2fde244cd Mon Sep 17 00:00:00 2001 From: Peter Flynn Date: Tue, 21 May 2013 17:18:20 -0700 Subject: [PATCH 5/5] Hoist out a util for creating a mock Editor given an already-created document, so JS code hints unit tests can share SpecRunnerUtils' mock-Editor creation code instead of duplicating it. --- .../default/JavaScriptCodeHints/unittests.js | 17 +++--------- test/spec/SpecRunnerUtils.js | 27 ++++++++++++------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/extensions/default/JavaScriptCodeHints/unittests.js b/src/extensions/default/JavaScriptCodeHints/unittests.js index 9c0599cde7b..8bf4e50963a 100644 --- a/src/extensions/default/JavaScriptCodeHints/unittests.js +++ b/src/extensions/default/JavaScriptCodeHints/unittests.js @@ -49,26 +49,15 @@ define(function (require, exports, module) { DocumentManager.setCurrentDocument(doc); }); }); + /** - * Returns an Editor suitable for use in isolation, given a Document. (Unlike - * SpecRunnerUtils.createMockEditor(), which is given text and creates the Document - * for you). + * Returns an Editor suitable for use in isolation, given a Document. * * @param {Document} doc - the document to be contained by the new Editor * @return {Editor} - the mock editor object */ function createMockEditor(doc) { - // Initialize EditorManager - var $editorHolder = $("
"); - EditorManager.setEditorHolder($editorHolder); - $("body").append($editorHolder); - - // create Editor instance - var editor = new Editor(doc, true, $editorHolder.get(0)); - - EditorManager._notifyActiveEditorChanged(editor); - - return editor; + return SpecRunnerUtils.createMockEditorForDocument(doc); } describe("JavaScript Code Hinting", function () { diff --git a/test/spec/SpecRunnerUtils.js b/test/spec/SpecRunnerUtils.js index 7a7fb4be084..29b26a716d8 100644 --- a/test/spec/SpecRunnerUtils.js +++ b/test/spec/SpecRunnerUtils.js @@ -205,26 +205,34 @@ define(function (require, exports, module) { } /** - * Returns a Document and Editor suitable for use with an Editor in - * isolation: i.e., a Document that will never be set as the - * currentDocument or added to the working set. - * @return {!{doc:{Document}, editor:{Editor}}} + * Returns an Editor tied to the given Document, but suitable for use in isolation + * (without being placed inside the surrounding Brackets UI). + * @return {!Editor} */ - function createMockEditor(initialContent, languageId, visibleRange) { + function createMockEditorForDocument(doc, visibleRange) { // Initialize EditorManager/PanelManager and position the editor-holder offscreen // (".content" may not exist, but that's ok for headless tests where editor height doesn't matter) var $editorHolder = createMockElement().attr("id", "mock-editor-holder"); PanelManager._setMockDOM($(".content"), $editorHolder); EditorManager.setEditorHolder($editorHolder); - // create dummy Document for the Editor - var doc = createMockDocument(initialContent, languageId); - // create Editor instance var editor = new Editor(doc, true, $editorHolder.get(0), visibleRange); EditorManager._notifyActiveEditorChanged(editor); - return { doc: doc, editor: editor }; + return editor; + } + + /** + * Returns a Document and Editor suitable for use in isolation: i.e., the Document + * will never be set as the currentDocument or added to the working set and the + * Editor does not live inside a full-blown Brackets UI layout. + * @return {!{doc:!Document, editor:!Editor}} + */ + function createMockEditor(initialContent, languageId, visibleRange) { + // create dummy Document, then Editor tied to it + var doc = createMockDocument(initialContent, languageId); + return { doc: doc, editor: createMockEditorForDocument(doc, visibleRange) }; } /** @@ -944,6 +952,7 @@ define(function (require, exports, module) { exports.createMockDocument = createMockDocument; exports.createMockActiveDocument = createMockActiveDocument; exports.createMockElement = createMockElement; + exports.createMockEditorForDocument = createMockEditorForDocument; exports.createMockEditor = createMockEditor; exports.createTestWindowAndRun = createTestWindowAndRun; exports.closeTestWindow = closeTestWindow;