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) {