string | "project-context-menu" |
| WORKING_SET_CONTEXT_MENU | string | "workingset-context-menu" |
| WORKING_SET_CONFIG_MENU | string | "workingset-configuration-menu" |
+| FILE_TABS_CONTEXT_MENU | string | "tabbar-context-menu" |
+| FILE_TABS_SETTINGS_MENU | string | "tabbar-settings-context-menu" |
| SPLITVIEW_MENU | string | "splitview-menu" |
diff --git a/docs/API-Reference/features/TaskManager.md b/docs/API-Reference/features/TaskManager.md
index d07490a63a..9d13cf300d 100644
--- a/docs/API-Reference/features/TaskManager.md
+++ b/docs/API-Reference/features/TaskManager.md
@@ -39,6 +39,7 @@ like `showStopIcon`, `hideStopIcon`, etc.
| [options.onRetryClick] | function | | Callback function triggered when the retry button is clicked. |
| [options.onSelect] | function | | Callback function triggered when the task is selected from the dropdown. |
| [options.progressPercent] | number | | Initial progress percentage of the task. |
+| [options.hideProgressBar] | boolean | | Hide progress for tasks waiting on a user action. |
| [options.noSpinnerNotification] | boolean | | If set to true, will not show the task spinners for this task. This can be used for silent background tasks where user attention is not needed. |
**Example**
diff --git a/docs/API-Reference/project/FileViewController.md b/docs/API-Reference/project/FileViewController.md
index 2d59a49e73..c18478a2ca 100644
--- a/docs/API-Reference/project/FileViewController.md
+++ b/docs/API-Reference/project/FileViewController.md
@@ -66,3 +66,24 @@ Opens the specified document with its associated external editor,
returns either WORKING_SET_VIEW or PROJECT_MANAGER
**Kind**: global function
+
+
+## setWorkingSetContext(file, [paneId])
+Remembers the file a working set or tab bar context menu was opened on. Pass null when the
+menu closes. Fires `workingSetContextChange` with the context so the views can mark the file
+while the menu is up.
+
+**Kind**: global function
+
+| Param | Type | Description |
+| --- | --- | --- |
+| file | File | the right-clicked file, or null to forget it |
+| [paneId] | string | the pane the file is shown or listed in, defaults to the active pane |
+
+
+
+## getWorkingSetContext() ⇒ Object
+The file a working set or tab bar context menu is currently open for
+
+**Kind**: global function
+**Returns**: Object - null when no such menu is up
diff --git a/docs/API-Reference/project/WorkingSetView.md b/docs/API-Reference/project/WorkingSetView.md
index 9b05a76404..92d5c64935 100644
--- a/docs/API-Reference/project/WorkingSetView.md
+++ b/docs/API-Reference/project/WorkingSetView.md
@@ -63,7 +63,14 @@ to update an existing item, all previously applied classes have been cleared.
-## getContext()
-Gets the filesystem object for the current context in the working set.
+## getContext() ⇒ File
+Gets the file a working set or tab bar context menu is open for. Right-clicking an item doesn't
+select it, so this is what the menu commands act on.
+
+**Kind**: global function
+
+
+## getContextPaneId() ⇒ string
+Gets the id of the pane the file of `getContext()` is shown or listed in
**Kind**: global function
diff --git a/docs/API-Reference/utils/DragAndDrop.md b/docs/API-Reference/utils/DragAndDrop.md
index 5800006e76..b1caeda9b8 100644
--- a/docs/API-Reference/utils/DragAndDrop.md
+++ b/docs/API-Reference/utils/DragAndDrop.md
@@ -18,7 +18,8 @@ Returns true if the drag and drop items contains valid drop objects.
## openDroppedFiles(paths) ⇒ Promise
-Open dropped files
+Open dropped files in order so the last file becomes active, regardless of load times.
+Continue processing the remaining paths if an earlier file cannot be opened.
**Kind**: global function
**Returns**: Promise - Promise that is resolved if all files are opened, or rejected
diff --git a/docs/API-Reference/view/MainViewManager.md b/docs/API-Reference/view/MainViewManager.md
index 0d38ca3362..5a45a86f80 100644
--- a/docs/API-Reference/view/MainViewManager.md
+++ b/docs/API-Reference/view/MainViewManager.md
@@ -44,7 +44,8 @@ This module dispatches several events:
(e, fileRemoved:File, suppressRedraw:boolean, paneId:string)
- workingSetRemoveList -- When multiple files are removed from the working set
(e, filesRemoved:Array."File", paneId:string)
- - workingSetSort -- When a pane's view array is reordered without additions or removals.
+ - workingSetSort -- When a pane's view array is reordered without additions or removals. Also fired when a
+ file is pinned or unpinned, as pinned files are kept at the front of the working set.
(e, paneId:string)
- workingSetUpdate -- When changes happen due to system events such as a file being deleted.
listeners should discard all working set info and rebuilt it from the pane
@@ -85,6 +86,8 @@ To listen for events, do something like this: (see EventDispatcher for details o
* [.addToWorkingSet(paneId, file, [index], [forceRedraw])](#module_view/MainViewManager..addToWorkingSet)
* [.addListToWorkingSet(paneId, fileList)](#module_view/MainViewManager..addListToWorkingSet)
* [.switchPaneFocus()](#module_view/MainViewManager..switchPaneFocus)
+ * [.setFilePinned(paneId, file, pinned)](#module_view/MainViewManager..setFilePinned) ⇒ boolean
+ * [.isPathPinned(paneId, fullPath)](#module_view/MainViewManager..isPathPinned) ⇒ boolean
* [.traverseToNextViewByMRU(direction)](#module_view/MainViewManager..traverseToNextViewByMRU) ⇒ Object
* [.traverseToNextViewInListOrder(direction)](#module_view/MainViewManager..traverseToNextViewInListOrder) ⇒ Object
* [.beginTraversal()](#module_view/MainViewManager..beginTraversal)
@@ -381,6 +384,36 @@ Adds the given file list to the end of the workingset.
Switch between panes
**Kind**: inner method of [view/MainViewManager](#module_view/MainViewManager)
+
+
+### view/MainViewManager.setFilePinned(paneId, file, pinned) ⇒ boolean
+Pins or unpins a file in a pane. Pinned files are kept at the front of the working set, in the order the
+user arranged them, and the bulk close commands (close others, close all...) leave them alone.
+Pinning a file that is only being previewed (not in the working set yet) adds it to the working set first.
+Fires `workingSetSort` for the pane when the pinned state changes.
+
+**Kind**: inner method of [view/MainViewManager](#module_view/MainViewManager)
+**Returns**: boolean - true if the pinned state changed
+
+| Param | Type | Description |
+| --- | --- | --- |
+| paneId | string | the id of the pane the file is in or ACTIVE_PANE |
+| file | File | the file to pin or unpin |
+| pinned | boolean | true to pin, false to unpin |
+
+
+
+### view/MainViewManager.isPathPinned(paneId, fullPath) ⇒ boolean
+Whether the file is pinned in the given pane
+
+**Kind**: inner method of [view/MainViewManager](#module_view/MainViewManager)
+**Returns**: boolean - true if the file is pinned
+
+| Param | Type | Description |
+| --- | --- | --- |
+| paneId | string | the id of the pane or ACTIVE_PANE |
+| fullPath | string | the full path of the file |
+
### view/MainViewManager.traverseToNextViewByMRU(direction) ⇒ Object
diff --git a/docs/API-Reference/view/Pane.md b/docs/API-Reference/view/Pane.md
index 2ca1cdab71..cf3b2c7d33 100644
--- a/docs/API-Reference/view/Pane.md
+++ b/docs/API-Reference/view/Pane.md
@@ -148,6 +148,7 @@ Hidden views can be later shown by calling `pane.showView(view)`
* [.ITEM_NOT_FOUND](#module_view/Pane..Pane+ITEM_NOT_FOUND)
* [.ITEM_FOUND_NO_SORT](#module_view/Pane..Pane+ITEM_FOUND_NO_SORT)
* [.ITEM_FOUND_NEEDS_SORT](#module_view/Pane..Pane+ITEM_FOUND_NEEDS_SORT)
+ * [.getTemporaryViewFile()](#module_view/Pane..Pane+getTemporaryViewFile) ⇒ File
* [.mergeFrom(other)](#module_view/Pane..Pane+mergeFrom)
* [.destroy()](#module_view/Pane..Pane+destroy)
* [.getViewList()](#module_view/Pane..Pane+getViewList) ⇒ Array.<File>
@@ -161,6 +162,9 @@ Hidden views can be later shown by calling `pane.showView(view)`
* [.makeViewMostRecent(file)](#module_view/Pane..Pane+makeViewMostRecent)
* [.sortViewList(compareFn)](#module_view/Pane..Pane+sortViewList)
* [.swapViewListIndexes(index1, index2)](#module_view/Pane..Pane+swapViewListIndexes) ⇒ boolean
+ * [.isPathPinned(fullPath)](#module_view/Pane..Pane+isPathPinned) ⇒ boolean
+ * [.getPinnedCount()](#module_view/Pane..Pane+getPinnedCount) ⇒ number
+ * [.setPathPinned(fullPath, pinned)](#module_view/Pane..Pane+setPathPinned) ⇒ boolean
* [.traverseViewListByMRU(direction, [current])](#module_view/Pane..Pane+traverseViewListByMRU) ⇒ File
* [.showInterstitial(show)](#module_view/Pane..Pane+showInterstitial)
* [.getViewForPath(path)](#module_view/Pane..Pane+getViewForPath) ⇒ boolean
@@ -198,6 +202,7 @@ Hidden views can be later shown by calling `pane.showView(view)`
* [.ITEM_NOT_FOUND](#module_view/Pane..Pane+ITEM_NOT_FOUND)
* [.ITEM_FOUND_NO_SORT](#module_view/Pane..Pane+ITEM_FOUND_NO_SORT)
* [.ITEM_FOUND_NEEDS_SORT](#module_view/Pane..Pane+ITEM_FOUND_NEEDS_SORT)
+ * [.getTemporaryViewFile()](#module_view/Pane..Pane+getTemporaryViewFile) ⇒ File
* [.mergeFrom(other)](#module_view/Pane..Pane+mergeFrom)
* [.destroy()](#module_view/Pane..Pane+destroy)
* [.getViewList()](#module_view/Pane..Pane+getViewList) ⇒ Array.<File>
@@ -211,6 +216,9 @@ Hidden views can be later shown by calling `pane.showView(view)`
* [.makeViewMostRecent(file)](#module_view/Pane..Pane+makeViewMostRecent)
* [.sortViewList(compareFn)](#module_view/Pane..Pane+sortViewList)
* [.swapViewListIndexes(index1, index2)](#module_view/Pane..Pane+swapViewListIndexes) ⇒ boolean
+ * [.isPathPinned(fullPath)](#module_view/Pane..Pane+isPathPinned) ⇒ boolean
+ * [.getPinnedCount()](#module_view/Pane..Pane+getPinnedCount) ⇒ number
+ * [.setPathPinned(fullPath, pinned)](#module_view/Pane..Pane+setPathPinned) ⇒ boolean
* [.traverseViewListByMRU(direction, [current])](#module_view/Pane..Pane+traverseViewListByMRU) ⇒ File
* [.showInterstitial(show)](#module_view/Pane..Pane+showInterstitial)
* [.getViewForPath(path)](#module_view/Pane..Pane+getViewForPath) ⇒ boolean
@@ -319,6 +327,14 @@ and the workingset needs to be resorted
**Kind**: instance constant of [Pane](#module_view/Pane..Pane)
**See**: [Pane#reorderItem](Pane#reorderItem)
+
+
+#### pane.getTemporaryViewFile() ⇒ File
+Gets the file of the current view when it isn't in the working set, a file opened from the file
+tree with a single click just to have a look at it. A file that was just closed stays up until
+the next one is ready but is on its way out, so it doesn't count
+
+**Kind**: instance method of [Pane](#module_view/Pane..Pane)
#### pane.mergeFrom(other)
@@ -443,7 +459,7 @@ Moves the specified file to the front of the MRU (Most Recently Used) list.
#### pane.sortViewList(compareFn)
-Sorts items in the pane's view list.
+Sorts items in the pane's view list. Pinned files are sorted amongst themselves and stay in front.
**Kind**: instance method of [Pane](#module_view/Pane..Pane)
@@ -464,6 +480,38 @@ Swaps two items in the file view list (used while dragging items in the working
| index1 | number | the index of the first item to swap |
| index2 | number | the index of the second item to swap |
+
+
+#### pane.isPathPinned(fullPath) ⇒ boolean
+Whether the file is pinned in this pane
+
+**Kind**: instance method of [Pane](#module_view/Pane..Pane)
+
+| Param | Type | Description |
+| --- | --- | --- |
+| fullPath | string | the full path of the file |
+
+
+
+#### pane.getPinnedCount() ⇒ number
+Returns the number of pinned files. As pinned files are always kept at the front of the view list,
+this is also the index of the first unpinned file.
+
+**Kind**: instance method of [Pane](#module_view/Pane..Pane)
+
+
+#### pane.setPathPinned(fullPath, pinned) ⇒ boolean
+Pins or unpins a file in the view list. Pinning moves the file to the end of the pinned block and
+unpinning moves it to the start of the unpinned block, so nothing else changes position.
+
+**Kind**: instance method of [Pane](#module_view/Pane..Pane)
+**Returns**: boolean - true if the pinned state changed
+
+| Param | Type | Description |
+| --- | --- | --- |
+| fullPath | string | the full path of the file, it must be in the view list |
+| pinned | boolean | true to pin, false to unpin |
+
#### pane.traverseViewListByMRU(direction, [current]) ⇒ File
diff --git a/src/command/Commands.js b/src/command/Commands.js
index 6366ef6594..314e565c2c 100644
--- a/src/command/Commands.js
+++ b/src/command/Commands.js
@@ -85,6 +85,9 @@ define(function (require, exports, module) {
/** Closes files from list */
exports.FILE_CLOSE_LIST = "file.close_list"; // DocumentCommandHandlers.js handleFileCloseList()
+ /** Pins or unpins a file in the working set */
+ exports.FILE_PIN = "file.pin"; // DocumentCommandHandlers.js handleFilePin()
+
/** Reopens last closed file */
exports.FILE_REOPEN_CLOSED = "file.reopen_closed"; // DocumentCommandHandlers.js handleReopenClosed()
@@ -267,6 +270,10 @@ define(function (require, exports, module) {
exports.TOGGLE_TABBAR = "view.toggleTabbar";
// extensionsIntegrated/TabBar/main.js
+ /** Toggles whether the tabbar animates when it scrolls to the active tab */
+ exports.TOGGLE_TABBAR_SMOOTH_SCROLL = "view.toggleTabbarSmoothScroll";
+ // extensionsIntegrated/TabBar/main.js
+
/** Zooms in the editor view */
exports.VIEW_ZOOM_IN = "view.zoomIn"; // ViewCommandHandlers.js _handleZoomIn()
@@ -282,6 +289,9 @@ define(function (require, exports, module) {
/** Submenu for Open in working set context menu */
exports.OPEN_IN_SUBMENU_WS = "file-open-in-submenu-ws";
+ /** Submenu for Open in file tab bar context menu */
+ exports.OPEN_IN_SUBMENU_FILE_TABS = "file-open-in-submenu-file-tabs";
+
/** Increases editor font size */
exports.VIEW_INCREASE_FONT_SIZE = "view.increaseFontSize"; // ViewCommandHandlers.js _handleIncreaseFontSize()
diff --git a/src/command/DefaultMenus.js b/src/command/DefaultMenus.js
index 6361f3cb91..05c1da9345 100644
--- a/src/command/DefaultMenus.js
+++ b/src/command/DefaultMenus.js
@@ -32,7 +32,10 @@ define(function (require, exports, module) {
Menus = require("command/Menus"),
Strings = require("strings"),
MainViewManager = require("view/MainViewManager"),
- CommandManager = require("command/CommandManager");
+ CommandManager = require("command/CommandManager"),
+ WorkingSetView = require("project/WorkingSetView"),
+ FileViewController = require("project/FileViewController"),
+ ProjectManager = require("project/ProjectManager");
/**
* Disables menu items present in items if enabled is true.
@@ -54,9 +57,10 @@ define(function (require, exports, module) {
/**
* Checks if file saved and present on system and
* disables menu items accordingly
+ * @param {?FileSystemEntry} contextEntry - the entry the menu was opened on, if any
*/
- function _setMenuItemsVisible() {
- var file = MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE);
+ function _setMenuItemsVisible(contextEntry) {
+ const file = contextEntry || MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE);
if (file) {
file.exists(function (err, isPresent) {
if (err) {
@@ -70,6 +74,23 @@ define(function (require, exports, module) {
}
}
+ /**
+ * The pin command toggles, so its menu label has to say what it will do to the file the menu was
+ * opened for: the right-clicked item of the working set or the tab bar, else the current file.
+ * @param {boolean} forFileTabs - true for the tab bar context menu, false for the working set one
+ */
+ function _updatePinMenuItemName(forFileTabs) {
+ const paneId = WorkingSetView.getContextPaneId() || MainViewManager.ACTIVE_PANE,
+ file = WorkingSetView.getContext() || MainViewManager.getCurrentlyViewedFile(paneId),
+ pinned = file && MainViewManager.isPathPinned(paneId, file.fullPath);
+
+ let name = pinned ? Strings.CMD_FILE_UNPIN : Strings.CMD_FILE_PIN;
+ if (forFileTabs) {
+ name = pinned ? Strings.UNPIN_TAB : Strings.PIN_TAB;
+ }
+ CommandManager.get(Commands.FILE_PIN).setName(name);
+ }
+
const isBrowser = !Phoenix.isNativeApp;
const isDesktop = Phoenix.isNativeApp;
const fileNewShortcut = isDesktop ? "Ctrl-N" : ""; // Ctrl-1 universal shortcut is set in keyboard.json
@@ -310,34 +331,69 @@ define(function (require, exports, module) {
// WorkingSet context menu - Unlike most context menus, we can't attach
// listeners here because the DOM nodes for each pane's working set are
// created dynamically. Each WorkingSetView attaches its own listeners.
+ // The editor tab bar shows the same working set, so its context menu gets exactly the same
+ // items. Extensions that add to one of these menus should add to the other too.
var workingset_cmenu = Menus.registerContextMenu(Menus.ContextMenuIds.WORKING_SET_CONTEXT_MENU);
- workingset_cmenu.addMenuItem(Commands.FILE_SAVE);
- workingset_cmenu.addMenuItem(Commands.NAVIGATE_SHOW_IN_FILE_TREE);
- if(Phoenix.isNativeApp){
- let subMenu = workingset_cmenu.addSubMenu(Strings.CMD_OPEN_IN, Commands.OPEN_IN_SUBMENU_WS);
- subMenu.addMenuItem(Commands.NAVIGATE_SHOW_IN_OS);
- subMenu.addMenuItem(Commands.NAVIGATE_OPEN_IN_TERMINAL);
- subMenu.addMenuItem(Commands.NAVIGATE_OPEN_IN_INTEGRATED_TERMINAL);
- if (brackets.platform === "win") {
- subMenu.addMenuItem(Commands.NAVIGATE_OPEN_IN_POWERSHELL);
+ const filetabs_cmenu = Menus.registerContextMenu(Menus.ContextMenuIds.FILE_TABS_CONTEXT_MENU);
+ [workingset_cmenu, filetabs_cmenu].forEach(function (cmenu) {
+ cmenu.addMenuItem(Commands.FILE_SAVE);
+ cmenu.addMenuItem(Commands.NAVIGATE_SHOW_IN_FILE_TREE);
+ if(Phoenix.isNativeApp){
+ const subMenuId = cmenu === workingset_cmenu
+ ? Commands.OPEN_IN_SUBMENU_WS
+ : Commands.OPEN_IN_SUBMENU_FILE_TABS;
+ let subMenu = cmenu.addSubMenu(Strings.CMD_OPEN_IN, subMenuId);
+ subMenu.addMenuItem(Commands.NAVIGATE_SHOW_IN_OS);
+ subMenu.addMenuItem(Commands.NAVIGATE_OPEN_IN_TERMINAL);
+ subMenu.addMenuItem(Commands.NAVIGATE_OPEN_IN_INTEGRATED_TERMINAL);
+ if (brackets.platform === "win") {
+ subMenu.addMenuItem(Commands.NAVIGATE_OPEN_IN_POWERSHELL);
+ }
+ subMenu.addMenuItem(Commands.NAVIGATE_OPEN_IN_DEFAULT_APP);
+ }
+ cmenu.addMenuDivider();
+ cmenu.addMenuItem(Commands.FILE_COPY);
+ cmenu.addMenuItem(Commands.FILE_COPY_PATH);
+ cmenu.addMenuItem(Commands.FILE_DUPLICATE);
+ cmenu.addMenuItem(Commands.FILE_DOWNLOAD, undefined, undefined, undefined, {
+ hideWhenCommandDisabled: true
+ });
+ cmenu.addMenuDivider();
+ cmenu.addMenuItem(Commands.FILE_RENAME);
+ cmenu.addMenuItem(Commands.FILE_DELETE);
+ cmenu.addMenuDivider();
+ cmenu.addMenuItem(Commands.CMD_FIND_IN_SUBTREE);
+ cmenu.addMenuItem(Commands.CMD_REPLACE_IN_SUBTREE);
+ cmenu.addMenuDivider();
+ // the CloseOthers extension adds its close commands right after FILE_CLOSE
+ cmenu.addMenuItem(Commands.FILE_CLOSE);
+ cmenu.addMenuDivider();
+ cmenu.addMenuItem(Commands.FILE_PIN);
+ cmenu.addMenuDivider();
+ cmenu.addMenuItem(Commands.FILE_REOPEN_CLOSED);
+ });
+ workingset_cmenu.on("beforeContextMenuOpen", function () {
+ _updatePinMenuItemName(false);
+ });
+ filetabs_cmenu.on("beforeContextMenuOpen", function () {
+ _updatePinMenuItemName(true);
+ });
+
+ // Both menus act on the right-clicked file without selecting it. That file is remembered for
+ // exactly as long as a menu is up. Menus get closed from all over the place (an item click,
+ // Escape, a click elsewhere, another menu opening) and not all of them tell the menu about it,
+ // so the open class on the menu itself is watched instead. Right-clicking another item closes
+ // the menu and reopens it in one go, which is why only the final state counts. The item
+ // handlers have run their command by the time the observer gets to look.
+ function _forgetWorkingSetContextWhenClosed() {
+ if (!workingset_cmenu.isOpen() && !filetabs_cmenu.isOpen()) {
+ FileViewController.setWorkingSetContext(null);
}
- subMenu.addMenuItem(Commands.NAVIGATE_OPEN_IN_DEFAULT_APP);
}
- workingset_cmenu.addMenuDivider();
- workingset_cmenu.addMenuItem(Commands.FILE_COPY);
- workingset_cmenu.addMenuItem(Commands.FILE_COPY_PATH);
- workingset_cmenu.addMenuItem(Commands.FILE_DUPLICATE);
- workingset_cmenu.addMenuItem(Commands.FILE_DOWNLOAD, undefined, undefined, undefined, {
- hideWhenCommandDisabled: true
+ const menuStateObserver = new window.MutationObserver(_forgetWorkingSetContextWhenClosed);
+ [workingset_cmenu, filetabs_cmenu].forEach(function (cmenu) {
+ menuStateObserver.observe($("#" + cmenu.id)[0], { attributes: true, attributeFilter: ["class"] });
});
- workingset_cmenu.addMenuDivider();
- workingset_cmenu.addMenuItem(Commands.FILE_RENAME);
- workingset_cmenu.addMenuItem(Commands.FILE_DELETE);
- workingset_cmenu.addMenuDivider();
- workingset_cmenu.addMenuItem(Commands.CMD_FIND_IN_SUBTREE);
- workingset_cmenu.addMenuItem(Commands.CMD_REPLACE_IN_SUBTREE);
- workingset_cmenu.addMenuDivider();
- workingset_cmenu.addMenuItem(Commands.FILE_CLOSE);
var splitview_menu = Menus.registerContextMenu(Menus.ContextMenuIds.SPLITVIEW_MENU);
splitview_menu.addMenuItem(Commands.CMD_SPLITVIEW_NONE);
@@ -484,7 +540,16 @@ define(function (require, exports, module) {
});
// Check the visibility of context menu items before opening the context menu.
// 'Rename', 'Show in file tree' and 'Show in explorer' items will be disabled for files that have not yet been saved to disk.
- Menus.getContextMenu(Menus.ContextMenuIds.WORKING_SET_CONTEXT_MENU).on("beforeContextMenuOpen", _setMenuItemsVisible);
- Menus.getContextMenu(Menus.ContextMenuIds.PROJECT_MENU).on("beforeContextMenuOpen", _setMenuItemsVisible);
+ // the file tree menu is opened on a tree entry, the other two on the right-clicked file. A file
+ // from a working set menu that this menu is replacing may still be remembered at this point
+ [Menus.ContextMenuIds.WORKING_SET_CONTEXT_MENU, Menus.ContextMenuIds.FILE_TABS_CONTEXT_MENU]
+ .forEach(function (id) {
+ Menus.getContextMenu(id).on("beforeContextMenuOpen", function () {
+ _setMenuItemsVisible(WorkingSetView.getContext());
+ });
+ });
+ Menus.getContextMenu(Menus.ContextMenuIds.PROJECT_MENU).on("beforeContextMenuOpen", function () {
+ _setMenuItemsVisible(ProjectManager.getContext());
+ });
});
});
diff --git a/src/command/Menus.js b/src/command/Menus.js
index 5c4ed88f0a..da48439327 100644
--- a/src/command/Menus.js
+++ b/src/command/Menus.js
@@ -72,6 +72,8 @@ define(function (require, exports, module) {
PROJECT_MENU: "project-context-menu",
WORKING_SET_CONTEXT_MENU: "workingset-context-menu",
WORKING_SET_CONFIG_MENU: "workingset-configuration-menu",
+ FILE_TABS_CONTEXT_MENU: "tabbar-context-menu",
+ FILE_TABS_SETTINGS_MENU: "tabbar-settings-context-menu",
SPLITVIEW_MENU: "splitview-menu"
};
@@ -744,7 +746,6 @@ define(function (require, exports, module) {
// CommandManager.execute() to trigger beforeExecuteCommand events consistently, whether invoked via
// menu click or keyboard shortcut.
// because there are listeners that uses beforeExecuteCommand to listen to save commands
- // (for ex: tabbar listens to save commands to add placeholder tabs to working set)
// Other commands use direct execution to preserve file tree context
if(commandId === Commands.FILE_SAVE ||
commandId === Commands.FILE_SAVE_AS ||
diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js
index 0f303e98ef..fdbf7f05e4 100644
--- a/src/document/DocumentCommandHandlers.js
+++ b/src/document/DocumentCommandHandlers.js
@@ -1288,6 +1288,18 @@ define(function (require, exports, module) {
return result.promise();
}
+ /**
+ * Saving a file that is only being looked at keeps it: it joins the working set of the pane showing it
+ * @param {!File} file - the file being saved
+ * @param {string} paneId - the pane the save is for
+ */
+ function _keepPreviewedFile(file, paneId) {
+ if (MainViewManager.getCurrentlyViewedPath(paneId) === file.fullPath &&
+ MainViewManager.findInWorkingSet(paneId, file.fullPath) === -1) {
+ MainViewManager.addToWorkingSet(paneId, file);
+ }
+ }
+
/**
* Saves the given file. If no file specified, assumes the current document.
* @param {?{doc: ?Document}} commandData Document to close, or null
@@ -1299,11 +1311,23 @@ define(function (require, exports, module) {
function handleFileSave(commandData) {
const activeEditor = EditorManager.getActiveEditor(),
activeDoc = activeEditor && activeEditor.document,
- doc = (commandData && commandData.doc) || activeDoc;
+ context = FileViewController.getWorkingSetContext(),
+ contextDoc = context && DocumentManager.getOpenDocumentForPath(context.file.fullPath),
+ doc = (commandData && commandData.doc) || contextDoc || activeDoc;
let settings;
+
+ // a working set or tab bar menu was opened on a file that was never loaded, nothing to save there
+ if (context && !contextDoc && !(commandData && commandData.doc)) {
+ return $.Deferred().resolve().promise();
+ }
if(ChangeHelper._onBeforeSave(doc)){
return $.Deferred().reject().promise();
}
+ // a save asked for by the user on the file they are looking at, not one of the saves a close or
+ // a save all hands over as commandData
+ if (doc && !(commandData && commandData.doc)) {
+ _keepPreviewedFile(doc.file, context ? context.paneId : MainViewManager.ACTIVE_PANE);
+ }
if (doc && !doc.isSaving) {
if (doc.isUntitled()) {
@@ -1414,6 +1438,12 @@ define(function (require, exports, module) {
* after all operations completed if any ONE of them failed.
*/
function handleFileSaveAll() {
+ MainViewManager.getPaneIdList().forEach(function (paneId) {
+ const file = MainViewManager.getCurrentlyViewedFile(paneId);
+ if (file) {
+ _keepPreviewedFile(file, paneId);
+ }
+ });
return saveAll();
}
@@ -1480,6 +1510,13 @@ define(function (require, exports, module) {
_spawnedRequest = commandData.spawnedRequest || false;
}
+ // from a working set or tab bar context menu the file to close is the right-clicked one
+ const context = FileViewController.getWorkingSetContext();
+ if (!file && context) {
+ file = context.file;
+ paneId = (commandData && commandData.paneId) || context.paneId;
+ }
+
// utility function for handleFileClose: closes document & removes from workingset
function doClose(file) {
if (!promptOnly) {
@@ -1924,13 +1961,28 @@ define(function (require, exports, module) {
function handleFileRename() {
// Prefer selected sidebar item (which could be a folder)
var entry = ProjectManager.getContext();
+ if (entry) {
+ ProjectManager.renameItemInline(entry);
+ return;
+ }
+
+ // Else the file a working set or tab bar menu was opened on, else the current file
+ // (not selected in ProjectManager if not visible in tree or workingset)
+ const context = FileViewController.getWorkingSetContext();
+ entry = (context && context.file) || MainViewManager.getCurrentlyViewedFile();
if (!entry) {
- // Else use current file (not selected in ProjectManager if not visible in tree or workingset)
- entry = MainViewManager.getCurrentlyViewedFile();
+ return;
}
- if (entry) {
+ if (!ProjectManager.isWithinProject(entry.fullPath)) {
ProjectManager.renameItemInline(entry);
+ return;
}
+
+ // the rename happens inline in the file tree, so the tree has to be on screen with the file in
+ // view first. This is what the working set and the tab bar context menus end up in.
+ revealInFileTree(entry).always(function () {
+ ProjectManager.renameItemInline(entry);
+ });
}
@@ -2006,8 +2058,10 @@ define(function (require, exports, module) {
* Reveal a file in the file tree the way the "Show in File Tree" command does: show the sidebar
* if it is hidden, switch it to the Files tab, select the file and scroll its row into view.
* @param {File} file - The file to reveal. Silently no-ops outside the project.
+ * @return {$.Promise} resolved once the file is selected in the tree, rejected if it never showed up
*/
function revealInFileTree(file) {
+ const result = new $.Deferred();
if (!SidebarView.isVisible()) {
CommandManager.execute(Commands.VIEW_HIDE_SIDEBAR);
}
@@ -2015,6 +2069,7 @@ define(function (require, exports, module) {
ProjectManager.showInTree(file).always(function () {
if (ProjectManager.isInFileTree(file.fullPath) || !ProjectManager.isWithinProject(file.fullPath)) {
_scrollFileTreeSelectionIntoView();
+ result.resolve();
return;
}
// A file created moments ago outside the FileSystem API is not in the tree yet, so the
@@ -2025,14 +2080,20 @@ define(function (require, exports, module) {
let retries = 0;
function selectWhenInTree() {
if (ProjectManager.isInFileTree(file.fullPath)) {
- ProjectManager.showInTree(file).always(_scrollFileTreeSelectionIntoView);
+ ProjectManager.showInTree(file).always(function () {
+ _scrollFileTreeSelectionIntoView();
+ result.resolve();
+ });
} else if (++retries < MAX_RETRIES) {
setTimeout(selectWhenInTree, RETRY_MS);
+ } else {
+ result.reject();
}
}
ProjectManager.refreshFileTree();
setTimeout(selectWhenInTree, RETRY_MS);
});
+ return result.promise();
}
/**
@@ -2049,9 +2110,10 @@ define(function (require, exports, module) {
}
function handleShowInTree() {
- let activeFile = MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE);
- if(activeFile){
- revealInFileTree(activeFile);
+ const context = FileViewController.getWorkingSetContext();
+ const file = (context && context.file) || MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE);
+ if (file) {
+ revealInFileTree(file);
}
}
@@ -2114,6 +2176,29 @@ define(function (require, exports, module) {
});
}
+ /**
+ * Pins or unpins a file in a pane. Without `pinned` in the command data the current state is toggled.
+ * @param {{file: File=, paneId: string=, pinned: boolean=}=} commandData
+ * file - the file to pin or unpin, defaults to the file shown in the pane
+ * paneId - the pane the file lives in, defaults to the active pane
+ * pinned - true to pin, false to unpin, leave out to toggle
+ */
+ function handleFilePin(commandData) {
+ const data = commandData || {},
+ context = FileViewController.getWorkingSetContext(),
+ paneId = data.paneId || (context && context.paneId) || MainViewManager.ACTIVE_PANE,
+ file = data.file || (context && context.file) || MainViewManager.getCurrentlyViewedFile(paneId);
+
+ if (!file) {
+ return;
+ }
+
+ const pinned = (typeof data.pinned === "boolean")
+ ? data.pinned
+ : !MainViewManager.isPathPinned(paneId, file.fullPath);
+ MainViewManager.setFilePinned(paneId, file, pinned);
+ }
+
/** Show the selected sidebar (tree or workingset) item in Finder/Explorer */
function handleShowInOS() {
var entry = ProjectManager.getSelectedItem();
@@ -2510,6 +2595,7 @@ define(function (require, exports, module) {
CommandManager.register(Strings.CMD_FILE_SAVE_AS, Commands.FILE_SAVE_AS, handleFileSaveAs, _designModeOpts);
CommandManager.register(Strings.CMD_FILE_RENAME, Commands.FILE_RENAME, handleFileRename);
CommandManager.register(Strings.CMD_FILE_DELETE, Commands.FILE_DELETE, handleFileDelete);
+ CommandManager.register(Strings.CMD_FILE_PIN, Commands.FILE_PIN, handleFilePin, _designModeOpts);
// Close Commands
CommandManager.register(Strings.CMD_FILE_CLOSE, Commands.FILE_CLOSE, handleFileClose, _designModeOpts);
diff --git a/src/editor/EditorManager.js b/src/editor/EditorManager.js
index 36e613070b..f5bf5d1de3 100644
--- a/src/editor/EditorManager.js
+++ b/src/editor/EditorManager.js
@@ -167,9 +167,24 @@ define(function (require, exports, module) {
* @param {!jQuery.Event} e - event
* @param {?File} file - current file (can be null)
*/
- function _handleCurrentFileChange(e, file) {
+ function _handleCurrentFileChange(e, file, paneId) {
var doc = file && DocumentManager.getOpenDocumentForPath(file.fullPath);
- _notifyActiveEditorChanged(doc && doc._masterEditor);
+ if (!doc) {
+ _notifyActiveEditorChanged(null);
+ return;
+ }
+
+ // The same document can be open in both panes. The editor that became active is the one the
+ // pane is showing, not necessarily the document's master editor: that is whichever of its
+ // editors was focused last and may sit in the other pane. Activating that pane instead would
+ // dispatch this event again for the other pane's file and bounce back and forth without end.
+ const paneEditor = paneId && MainViewManager.getCurrentlyViewedEditor(paneId);
+ if (paneEditor && paneEditor.document === doc) {
+ doc._toggleMasterEditor(paneEditor);
+ _notifyActiveEditorChanged(paneEditor);
+ } else {
+ _notifyActiveEditorChanged(doc._masterEditor);
+ }
}
/**
diff --git a/src/extensions/default/CloseOthers/main.js b/src/extensions/default/CloseOthers/main.js
index f09490f4b8..be8f0e853d 100644
--- a/src/extensions/default/CloseOthers/main.js
+++ b/src/extensions/default/CloseOthers/main.js
@@ -22,22 +22,43 @@
define(function (require, exports, module) {
- var Menus = brackets.getModule("command/Menus"),
+ const Menus = brackets.getModule("command/Menus"),
CommandManager = brackets.getModule("command/CommandManager"),
Commands = brackets.getModule("command/Commands"),
MainViewManager = brackets.getModule("view/MainViewManager"),
+ WorkingSetView = brackets.getModule("project/WorkingSetView"),
Strings = brackets.getModule("strings"),
PreferencesManager = brackets.getModule("preferences/PreferencesManager"),
- workingSetListCmenu = Menus.getContextMenu(Menus.ContextMenuIds.WORKING_SET_CONTEXT_MENU);
+ workingSetListCmenu = Menus.getContextMenu(Menus.ContextMenuIds.WORKING_SET_CONTEXT_MENU),
+ fileTabsCmenu = Menus.getContextMenu(Menus.ContextMenuIds.FILE_TABS_CONTEXT_MENU);
// Constants
- var closeOthers = "file.close_others",
+ const closeOthers = "file.close_others",
closeAbove = "file.close_above",
- closeBelow = "file.close_below";
+ closeBelow = "file.close_below",
+ closeAll = "file.close_all_in_pane";
+
+ // the order in which the items appear in the menus, right after "Close"
+ const MENU_ORDER = [closeAbove, closeOthers, closeBelow, closeAll];
+
+ // the working set is a vertical list and the tab bar a horizontal one, so the same commands get
+ // different names depending on which of the two menus is open
+ const workingSetNames = {
+ [closeAbove]: Strings.CMD_FILE_CLOSE_ABOVE,
+ [closeOthers]: Strings.CMD_FILE_CLOSE_OTHERS,
+ [closeBelow]: Strings.CMD_FILE_CLOSE_BELOW,
+ [closeAll]: Strings.CMD_FILE_CLOSE_ALL
+ };
+ const fileTabNames = {
+ [closeAbove]: Strings.CLOSE_TABS_TO_THE_LEFT,
+ [closeOthers]: Strings.CLOSE_OTHER_TABS,
+ [closeBelow]: Strings.CLOSE_TABS_TO_THE_RIGHT,
+ [closeAll]: Strings.CLOSE_ALL_TABS
+ };
// Global vars and preferences
- var prefs = PreferencesManager.getExtensionPrefs("closeOthers"),
- menuEntriesShown = {};
+ const prefs = PreferencesManager.getExtensionPrefs("closeOthers");
+ let menuEntriesShown = {};
prefs.definePreference("below", "boolean", true, {
description: Strings.DESCRIPTION_CLOSE_OTHERS_BELOW
@@ -51,130 +72,162 @@ define(function (require, exports, module) {
/**
- * Handle the different Close Other commands
- * @param {string} mode
+ * The files of the pane's working set that the command would close for the given target file.
+ * Pinned files are never closed by these commands. A file that is only previewed (not in the working
+ * set) counts as sitting after the last working set file, which is where the tab bar shows it, and
+ * "Close All" closes the preview as well.
+ * @param {string} paneId - the pane to look in, or ACTIVE_PANE
+ * @param {string} mode - one of the close command ids
+ * @param {string} targetPath - full path of the file the command is relative to
+ * @return {Array.