diff --git a/src/htmlContent/pane.html b/src/htmlContent/pane.html index 92f55abaae6..87dd1fbf692 100644 --- a/src/htmlContent/pane.html +++ b/src/htmlContent/pane.html @@ -1,5 +1,9 @@
-
+
+
+
+
+
diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 9428f8d6b7e..7a1f3685e91 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -461,7 +461,10 @@ define({ "BASEURL_ERROR_HASH_DISALLOWED" : "The base URL can't contain hashes like \"{0}\".", "BASEURL_ERROR_INVALID_CHAR" : "Special characters like '{0}' must be %-encoded.", "BASEURL_ERROR_UNKNOWN_ERROR" : "Unknown error parsing Base URL", + + //Strings for Pane.js "EMPTY_VIEW_HEADER" : "Open a file while this pane has focus", + "FLIPVIEW_BTN_TOOLTIP" : "Flip this view to {0} pane", // Strings for themes-settings.html and themes-general.html "CURRENT_THEME" : "Current Theme", @@ -767,6 +770,8 @@ define({ "DESCRIPTION_FONT_SMOOTHING" : "Mac-only: \"subpixel-antialiased\" to enable sub-pixel antialiasing or \"antialiased\" for gray scale antialiasing", "DESCRIPTION_OPEN_PREFS_IN_SPLIT_VIEW" : "false to disable opening preferences file in split view", "DESCRIPTION_OPEN_USER_PREFS_IN_SECOND_PANE" : "false to open user preferences file in left/top pane", + "DESCRIPTION_MERGE_PANES_WHEN_LAST_FILE_CLOSED" : "true to collapse panes after the last file from the pane is closed via pane header close button", + "DESCRIPTION_SHOW_PANE_HEADER_BUTTONS" : "Toggle when to show the close and flip-view buttons on the header.", "DEFAULT_PREFERENCES_JSON_HEADER_COMMENT" : "/*\n * This is a read-only file with the preferences supported\n * by {APP_NAME}.\n * Use this file as a reference to modify your preferences\n * file \"brackets.json\" opened in the other pane.\n * For more information on how to use preferences inside\n * {APP_NAME}, refer to the web page at https://github.com/adobe/brackets/wiki/How-to-Use-Brackets#preferences\n */", "DEFAULT_PREFERENCES_JSON_DEFAULT" : "Default" }); diff --git a/src/styles/brackets.less b/src/styles/brackets.less index 6d5f092614f..1ee9b733770 100644 --- a/src/styles/brackets.less +++ b/src/styles/brackets.less @@ -486,12 +486,97 @@ a, img { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - + + &-text { + display: inline; + } + .dark & { background-color: #1d1f21; // not using a variable on purpose. border-bottom-color: rgba(255, 255, 255, 0.05); } + + &-flipview-btn { + position: relative; + display: none; + top: 0px; + padding-top: 2px; + padding-right: 4px; + padding-left: 4px; + margin-left: 0; + margin-bottom: 0; + .sprite-icon(0, 0, 13px, 13px, "images/flip-view-icons.svg"); + background-origin: content-box; + -webkit-transform: translateZ(0); // forces GPU mode for better filter rendering on retina + transform: translateZ(0); // future proofing + -webkit-filter: drop-shadow(0 1px 0 rgba(0,0,0,0.36)); + filter: drop-shadow(0 1px 0 rgba(0,0,0,0.36)); + z-index: 1; + vertical-align: middle; + + &:hover { + background-image: url("images/flip-view-icons-hover.svg") + } + + &.flipview-icon-none { + display: none; + } + + &.flipview-icon-top { + background-position: center 1px; + } + + &.flipview-icon-right { + background-position: center -18px; + } + + &.flipview-icon-bottom { + background-position: center -35px; + } + + &.flipview-icon-left { + background-position: center -54px; + } + } + + &-close-btn { + position: relative; + display: none; + height: 16px; + width: 16px; + float: right; + margin-top: -2px; + + &:before { + color: rgba(0, 0, 0, 0.5); + } + + &:hover:before { + color: rgba(0, 0, 0, 0.8); + } + + .dark & { + &:before { + color: rgba(255, 255, 255, 0.5); + } + + &:hover:before { + color: rgba(255, 255, 255, 0.8); + } + } + } + + &:hover, &.always-show-header-buttons { + > .pane-header-flipview-btn:not(.flipview-icon-none) { + display: inline-block; + } + + > .pane-header-close-btn { + display: inline; + } + } } + .active-pane { .pane-header { @@ -755,6 +840,7 @@ a, img { -webkit-filter: drop-shadow(0 1px 0 rgba(0,0,0,0.36)); z-index: 1; } + .splitview-icon-none { background-position: center 1px; } @@ -765,6 +851,8 @@ a, img { background-position: center -41px; } + + // Show splitview icons on the button's dropdown menu too #splitview-menu ul.dropdown-menu > li { .menu-name::before { diff --git a/src/styles/images/flip-view-icons-dark.svg b/src/styles/images/flip-view-icons-dark.svg new file mode 100644 index 00000000000..638b4e139e0 --- /dev/null +++ b/src/styles/images/flip-view-icons-dark.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/styles/images/flip-view-icons-hover.svg b/src/styles/images/flip-view-icons-hover.svg new file mode 100644 index 00000000000..c37917f8136 --- /dev/null +++ b/src/styles/images/flip-view-icons-hover.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/styles/images/flip-view-icons.svg b/src/styles/images/flip-view-icons.svg new file mode 100644 index 00000000000..8666b68b7b7 --- /dev/null +++ b/src/styles/images/flip-view-icons.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/view/Pane.js b/src/view/Pane.js index e20d06eb3c8..3422cd295b6 100644 --- a/src/view/Pane.js +++ b/src/view/Pane.js @@ -161,14 +161,44 @@ define(function (require, exports, module) { InMemoryFile = require("document/InMemoryFile"), ViewStateManager = require("view/ViewStateManager"), MainViewManager = require("view/MainViewManager"), + PreferencesManager = require("preferences/PreferencesManager"), DocumentManager = require("document/DocumentManager"), CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), Strings = require("strings"), + StringUtils = require("utils/StringUtils"), ViewUtils = require("utils/ViewUtils"), ProjectManager = require("project/ProjectManager"), paneTemplate = require("text!htmlContent/pane.html"); + /** + * Internal pane id + * @const + * @private + */ + var FIRST_PANE = "first-pane"; + + /** + * Internal pane id + * @const + * @private + */ + var SECOND_PANE = "second-pane"; + + // Define showPaneHeaderButtons, which controls when to show close and flip-view buttons + // on the header. + PreferencesManager.definePreference("pane.showPaneHeaderButtons", "string", "hover", { + description: Strings.DESCRIPTION_SHOW_PANE_HEADER_BUTTONS, + values: ["hover", "always", "never"] + }); + + // Define mergePanesWhenLastFileClosed, which controls if a split view pane should be + // closed when the last file is closed, skipping the "Open a file while this pane has focus" + // step completely. + PreferencesManager.definePreference("pane.mergePanesWhenLastFileClosed", "boolean", false, { + description: Strings.DESCRIPTION_MERGE_PANES_WHEN_LAST_FILE_CLOSED + }); + /** * Make an index request object * @param {boolean} requestIndex - true to request an index, false if not @@ -198,14 +228,51 @@ define(function (require, exports, module) { // Setup the container and the element we're inserting var self = this, + showPaneHeaderButtonsPref = PreferencesManager.get("pane.showPaneHeaderButtons"), $el = $container.append(Mustache.render(paneTemplate, {id: id})).find("#" + id), $header = $el.find(".pane-header"), + $headerText = $header.find(".pane-header-text"), + $headerFlipViewBtn = $header.find(".pane-header-flipview-btn"), + $headerCloseBtn = $header.find(".pane-header-close-btn"), $content = $el.find(".pane-content"); $el.on("focusin.pane", function (e) { self._lastFocusedElement = e.target; }); + // Flips the current file to the other pane when clicked + $headerFlipViewBtn.on("click.pane", function (e) { + var currentFile = self.getCurrentlyViewedFile(); + var otherPaneId = self.id === FIRST_PANE ? SECOND_PANE : FIRST_PANE; + var otherPane = MainViewManager._getPane(otherPaneId); + + MainViewManager._moveView(self.id, otherPaneId, currentFile).always(function () { + CommandManager.execute(Commands.FILE_OPEN, {fullPath: currentFile.fullPath, + paneId: otherPaneId}).always(function () { + otherPane.trigger("viewListChange"); + self.trigger("viewListChange"); + }); + }); + }); + + // Closes the current view on the pane when clicked. If pane has no files, merge + // panes. + $headerCloseBtn.on("click.pane", function () { + //set clicked pane as active to ensure that this._currentView is updated before closing + MainViewManager.setActivePaneId(self.id); + var file = self.getCurrentlyViewedFile(); + + if (file) { + CommandManager.execute(Commands.FILE_CLOSE, {File: file}); + + if (!self.getCurrentlyViewedFile() && PreferencesManager.get("pane.mergePanesWhenLastFileClosed")) { + MainViewManager.setLayoutScheme(1, 1); + } + } else { + MainViewManager.setLayoutScheme(1, 1); + } + }); + this._lastFocusedElement = $el[0]; // Make these properties read only @@ -236,6 +303,33 @@ define(function (require, exports, module) { } }); + Object.defineProperty(this, "$headerText", { + get: function () { + return $headerText; + }, + set: function () { + console.error("cannot change the DOM node of a working pane"); + } + }); + + Object.defineProperty(this, "$headerFlipViewBtn", { + get: function () { + return $headerFlipViewBtn; + }, + set: function () { + console.error("cannot change the DOM node of a working pane"); + } + }); + + Object.defineProperty(this, "$headerCloseBtn", { + get: function () { + return $headerCloseBtn; + }, + set: function () { + console.error("cannot change the DOM node of a working pane"); + } + }); + Object.defineProperty(this, "$content", { get: function () { return $content; @@ -256,6 +350,16 @@ define(function (require, exports, module) { this.updateHeaderText(); + switch (showPaneHeaderButtonsPref) { + case "always": + this.$header.addClass("always-show-header-buttons"); + break; + case "never": + this.$headerFlipViewBtn.css("display", "none"); + this.$headerCloseBtn.css("display", "none"); + break; + } + // Listen to document events so we can update ourself DocumentManager.on(this._makeEventName("fileNameChange"), _.bind(this._handleFileNameChange, this)); DocumentManager.on(this._makeEventName("pathDeleted"), _.bind(this._handleFileDeleted, this)); @@ -264,6 +368,7 @@ define(function (require, exports, module) { MainViewManager.on(this._makeEventName("workingSetRemove"), _.bind(this.updateHeaderText, this)); MainViewManager.on(this._makeEventName("workingSetAddList"), _.bind(this.updateHeaderText, this)); MainViewManager.on(this._makeEventName("workingSetRemoveList"), _.bind(this.updateHeaderText, this)); + MainViewManager.on(this._makeEventName("paneLayoutChange"), _.bind(this.updateFlipViewIcon, this)); } EventDispatcher.makeEventDispatcher(Pane.prototype); @@ -289,12 +394,33 @@ define(function (require, exports, module) { Pane.prototype.$el = null; /** - * the wrapped DOM node that contains name of current view, or informational string if there is no view + * the wrapped DOM node container that contains name of current view and the switch view button, or informational string if there is no view * @readonly * @type {JQuery} */ Pane.prototype.$header = null; - + + /** + * the wrapped DOM node that contains name of current view, or informational string if there is no view + * @readonly + * @type {JQuery} + */ + Pane.prototype.$headerText = null; + + /** + * the wrapped DOM node that is used to flip the view to another pane + * @readonly + * @type {JQuery} + */ + Pane.prototype.$headerFlipViewBtn = null; + + /** + * close button of the pane + * @readonly + * @type {JQuery} + */ + Pane.prototype.$headerCloseBtn = null; + /** * the wrapped DOM node that contains views * @readonly @@ -853,6 +979,30 @@ define(function (require, exports, module) { return ViewUtils.traverseViewArray(this._viewListMRUOrder, index, direction); }; + /** + * Updates flipview icon in pane header + * @private + */ + Pane.prototype.updateFlipViewIcon = function () { + var paneID = this.id, + directionIndex = 0, + ICON_CLASSES = ["flipview-icon-none", "flipview-icon-top", "flipview-icon-right", "flipview-icon-bottom", "flipview-icon-left"], + DIRECTION_STRINGS = ["", Strings.TOP, Strings.RIGHT, Strings.BOTTOM, Strings.LEFT], + layoutScheme = MainViewManager.getLayoutScheme(), + hasFile = this.getCurrentlyViewedFile(); + + if (layoutScheme.columns > 1 && hasFile) { + directionIndex = paneID === FIRST_PANE ? 2 : 4; + } else if (layoutScheme.rows > 1 && hasFile) { + directionIndex = paneID === FIRST_PANE ? 3 : 1; + } + + this.$headerFlipViewBtn.removeClass(ICON_CLASSES.join(" ")) + .addClass(ICON_CLASSES[directionIndex]); + + this.$headerFlipViewBtn.attr("title", StringUtils.format(Strings.FLIPVIEW_BTN_TOOLTIP, DIRECTION_STRINGS[directionIndex].toLowerCase())); + }; + /** * Updates text in pane header * @private @@ -861,22 +1011,24 @@ define(function (require, exports, module) { var file = this.getCurrentlyViewedFile(), files, displayName; - + if (file) { files = MainViewManager.getAllOpenFiles().filter(function (item) { return (item.name === file.name); }); if (files.length < 2) { - this.$header.text(file.name); + this.$headerText.text(file.name); } else { displayName = ProjectManager.makeProjectRelativeIfPossible(file.fullPath); - this.$header.text(displayName); + this.$headerText.text(displayName); } } else { - this.$header.html(Strings.EMPTY_VIEW_HEADER); + this.$headerText.html(Strings.EMPTY_VIEW_HEADER); } + + this.updateFlipViewIcon(); }; - + /** * Event handler when a file changes name * @private diff --git a/test/spec/MainViewManager-test.js b/test/spec/MainViewManager-test.js index 8be3f9bbbc8..465f89d8932 100644 --- a/test/spec/MainViewManager-test.js +++ b/test/spec/MainViewManager-test.js @@ -412,6 +412,43 @@ define(function (require, exports, module) { expect(EditorManager.getCurrentFullEditor().document.file.name).toEqual("test.css"); }); }); + it("should flip the view to the other pane", function () { + runs(function () { + MainViewManager.setLayoutScheme(1, 2); + }); + runs(function () { + promise = CommandManager.execute(Commands.FILE_OPEN, { fullPath: testPath + "/test.js", + paneId: "first-pane" }); + waitsForDone(promise, Commands.FILE_OPEN); + }); + runs(function () { + expect(MainViewManager._getPaneIdForPath(testPath + "/test.js")).toEqual("first-pane"); + }); + runs(function () { + MainViewManager.setActivePaneId("first-pane"); + expect(MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE).name).toEqual("test.js"); + MainViewManager.setActivePaneId("second-pane"); + expect(MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE)).toEqual(null); + }); + runs(function () { + MainViewManager._getPane("first-pane").$headerFlipViewBtn.trigger("click"); + }); + runs(function () { + MainViewManager.setActivePaneId("first-pane"); + expect(MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE)).toEqual(null); + MainViewManager.setActivePaneId("second-pane"); + expect(MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE).name).toEqual("test.js"); + }); + runs(function () { + MainViewManager._getPane("second-pane").$headerFlipViewBtn.trigger("click"); + }); + runs(function () { + MainViewManager.setActivePaneId("first-pane"); + expect(MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE).name).toEqual("test.js"); + MainViewManager.setActivePaneId("second-pane"); + expect(MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE)).toEqual(null); + }); + }); it("should merge two panes to the right", function () { runs(function () { MainViewManager.setLayoutScheme(1, 2); @@ -465,6 +502,41 @@ define(function (require, exports, module) { expect(MainViewManager._getPaneIdForPath(testPath + "/test.css")).toEqual(null); }); }); + it("should close the view when clicked", function () { + runs(function () { + MainViewManager.setLayoutScheme(1, 2); + }); + runs(function () { + promise = CommandManager.execute(Commands.FILE_OPEN, { fullPath: testPath + "/test.js", + paneId: "first-pane" }); + waitsForDone(promise, Commands.FILE_OPEN); + }); + runs(function () { + expect(MainViewManager._getPaneIdForPath(testPath + "/test.js")).toEqual("first-pane"); + }); + runs(function () { + MainViewManager.setActivePaneId("first-pane"); + expect(MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE).name).toEqual("test.js"); + }); + runs(function () { + MainViewManager._getPane("first-pane").$headerCloseBtn.trigger("click"); + }); + runs(function () { + MainViewManager.setActivePaneId("first-pane"); + expect(MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE)).toEqual(null); + }); + }); + it("should collapse the panes when close button is clicked on a pane with no files", function () { + runs(function () { + MainViewManager.setLayoutScheme(1, 2); + }); + runs(function () { + MainViewManager._getPane("first-pane").$headerCloseBtn.trigger("click"); + }); + runs(function () { + expect(MainViewManager.getLayoutScheme()).toEqual({rows: 1, columns: 1}); + }); + }); it("should activate pane when editor gains focus", function () { var editors = {}, handler = function (e, doc, editor, paneId) {