-
+
diff --git a/src/language/CodeInspection.js b/src/language/CodeInspection.js
index 8cbb15d89f..6e5103bcdc 100644
--- a/src/language/CodeInspection.js
+++ b/src/language/CodeInspection.js
@@ -1263,7 +1263,7 @@ define(function (require, exports, module) {
Editor.registerGutter(CODE_INSPECTION_GUTTER, CODE_INSPECTION_GUTTER_PRIORITY);
// Create bottom panel to list error details
var panelHtml = Mustache.render(PanelTemplate, Strings);
- problemsPanel = WorkspaceManager.createBottomPanel("errors", $(panelHtml), 100);
+ problemsPanel = WorkspaceManager.createBottomPanel("errors", $(panelHtml), 100, Strings.CMD_VIEW_TOGGLE_PROBLEMS);
$problemsPanel = $("#problems-panel");
$fixAllBtn = $problemsPanel.find(".problems-fix-all-btn");
$fixAllBtn.click(()=>{
@@ -1354,11 +1354,6 @@ define(function (require, exports, module) {
}
});
- $("#problems-panel .close").click(function () {
- toggleCollapsed(true);
- MainViewManager.focusActivePane();
- });
-
// Status bar indicator - icon & tooltip updated by run()
var statusIconHtml = Mustache.render("
", Strings);
StatusBar.addIndicator(INDICATOR_ID, $(statusIconHtml), true, "", "", "status-indent");
@@ -1370,6 +1365,14 @@ define(function (require, exports, module) {
}
});
+ // When the panel tab is closed externally (e.g. via the × button),
+ // update the collapsed state so the panel doesn't auto-reopen.
+ WorkspaceManager.on(WorkspaceManager.EVENT_WORKSPACE_PANEL_HIDDEN, function (event, panelID) {
+ if (panelID === "errors") {
+ _collapsed = true;
+ }
+ });
+
// Set initial UI state
toggleEnabled(prefs.get(PREF_ENABLED), true);
toggleCollapsed(prefs.get(PREF_COLLAPSED), true);
diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js
index cde5aeabfb..71aaa7bf13 100644
--- a/src/nls/root/strings.js
+++ b/src/nls/root/strings.js
@@ -1249,6 +1249,13 @@ define({
"REFERENCES_IN_FILES": "references",
"REFERENCE_IN_FILES": "reference",
"REFERENCES_NO_RESULTS": "No References available for current cursor position",
+ "REFERENCES_PANEL_TITLE": "References",
+ "SEARCH_RESULTS_PANEL_TITLE": "Search Results",
+ "BOTTOM_PANEL_HIDE": "Hide Panel",
+ "BOTTOM_PANEL_SHOW": "Show Bottom Panel",
+ "BOTTOM_PANEL_HIDE_TOGGLE": "Hide Bottom Panel",
+ "BOTTOM_PANEL_DEFAULT_TITLE": "Quick Access",
+ "BOTTOM_PANEL_DEFAULT_HEADING": "Open a Panel",
"CMD_FIND_DOCUMENT_SYMBOLS": "Find Document Symbols",
"CMD_FIND_PROJECT_SYMBOLS": "Find Project Symbols",
@@ -1396,6 +1403,7 @@ define({
"BUTTON_CANCEL": "Cancel",
"CHECKOUT_COMMIT": "Checkout",
"CHECKOUT_COMMIT_DETAIL": "
Commit Message: {0}
Commit hash: {1}",
+ "GIT_PANEL_TITLE": "Git",
"GIT_CLONE": "Clone",
"BUTTON_CLOSE": "Close",
"BUTTON_COMMIT": "Commit",
@@ -1696,6 +1704,7 @@ define({
"CUSTOM_SNIPPETS_ADD_PANEL_TITLE": "Add Snippet",
"CUSTOM_SNIPPETS_EDIT_PANEL_TITLE": "Edit Snippet",
"CUSTOM_SNIPPETS_ADD_NEW_TITLE": "Add new snippet",
+ "CUSTOM_SNIPPETS_ADD_BTN_LABEL": "Add",
"CUSTOM_SNIPPETS_BACK_TO_LIST_TITLE": "Back to snippets list",
"CUSTOM_SNIPPETS_BACK": "Back",
"CUSTOM_SNIPPETS_FILTER_PLACEHOLDER": "Filter...",
diff --git a/src/search/FindInFilesUI.js b/src/search/FindInFilesUI.js
index 6a02cd7e19..8ce87f25a4 100644
--- a/src/search/FindInFilesUI.js
+++ b/src/search/FindInFilesUI.js
@@ -111,7 +111,7 @@ define(function (require, exports, module) {
}
} else {
- _resultsView.close();
+ _resultsView.showNoResults();
if (_findBar) {
var showMessage = false;
@@ -536,7 +536,8 @@ define(function (require, exports, module) {
// Initialize items dependent on HTML DOM
AppInit.htmlReady(function () {
var model = FindInFiles.searchModel;
- _resultsView = new SearchResultsView(model, "find-in-files-results", "find-in-files.results");
+ _resultsView = new SearchResultsView(model, "find-in-files-results", "find-in-files.results",
+ undefined, Strings.SEARCH_RESULTS_PANEL_TITLE);
_resultsView
.on("replaceBatch", function () {
_finishReplaceBatch(model);
diff --git a/src/search/SearchResultsView.js b/src/search/SearchResultsView.js
index 54751a67e3..e79a86116b 100644
--- a/src/search/SearchResultsView.js
+++ b/src/search/SearchResultsView.js
@@ -76,12 +76,13 @@ define(function (require, exports, module) {
* @param {string} panelID The CSS ID to use for the panel.
* @param {string} panelName The name to use for the panel, as passed to WorkspaceManager.createBottomPanel().
* @param {string} type type to identify if it is reference search or string match serach
+ * @param {string=} title Display title for the panel tab.
*/
- function SearchResultsView(model, panelID, panelName, type) {
+ function SearchResultsView(model, panelID, panelName, type, title) {
const self = this;
let panelHtml = Mustache.render(searchPanelTemplate, {panelID: panelID});
- this._panel = WorkspaceManager.createBottomPanel(panelName, $(panelHtml), 100);
+ this._panel = WorkspaceManager.createBottomPanel(panelName, $(panelHtml), 100, title);
this._$summary = this._panel.$panel.find(".title");
this._$table = this._panel.$panel.find(".table-container");
this._$previewEditor = this._panel.$panel.find(".search-editor-preview");
@@ -96,12 +97,10 @@ define(function (require, exports, module) {
}).observe(this._panel.$panel[0]);
function _showPanelIfResultsAvailable(_e, shownPanelID) {
- if(self._model.numMatches === 0){
- self._panel.hide();
- }
- if(shownPanelID === self._panel.panelID && !self._model.isReplace){
- // If it is replace, _handleModelChange will close the find bar as we dont
- // do replace if there is a model change. So we wont enter this flow if it is a replace operation
+ if (shownPanelID === self._panel.panelID && self._model.numMatches > 0 && !self._model.isReplace) {
+ // Refresh results when the tab is re-activated (they may have changed
+ // while the panel was in a background tab). Skip when numMatches is 0
+ // so the "no results" state isn't disturbed.
self._handleModelChange();
}
}
@@ -328,12 +327,6 @@ define(function (require, exports, module) {
var self = this;
this._panel.$panel
.off(".searchResults") // Remove the old events
- .on("dblclick.searchResults", ".toolbar", function() {
- self._panel.hide();
- })
- .on("click.searchResults", ".close", function () {
- self._panel.hide();
- })
// The link to go the first page
.on("click.searchResults", ".first-page:not(.disabled)", function () {
self._currentStart = 0;
@@ -530,18 +523,18 @@ define(function (require, exports, module) {
let self = this;
let count = self._model.countFilesMatches(),
lastIndex = self._getLastIndex(count.matches),
- typeStr = (count.matches > 1) ? Strings.FIND_IN_FILES_MATCHES : Strings.FIND_IN_FILES_MATCH,
+ typeStr = (count.matches !== 1) ? Strings.FIND_IN_FILES_MATCHES : Strings.FIND_IN_FILES_MATCH,
filesStr,
summary;
if(this._searchResultsType === "reference") {
- typeStr = (count.matches > 1) ? Strings.REFERENCES_IN_FILES : Strings.REFERENCE_IN_FILES;
+ typeStr = (count.matches !== 1) ? Strings.REFERENCES_IN_FILES : Strings.REFERENCE_IN_FILES;
}
filesStr = StringUtils.format(
Strings.FIND_NUM_FILES,
count.files,
- (count.files > 1 ? Strings.FIND_IN_FILES_FILES : Strings.FIND_IN_FILES_FILE)
+ (count.files !== 1 ? Strings.FIND_IN_FILES_FILES : Strings.FIND_IN_FILES_FILE)
);
// This text contains some formatting, so all the strings are assumed to be already escaped
@@ -576,8 +569,17 @@ define(function (require, exports, module) {
* Shows the current set of results.
*/
SearchResultsView.prototype._render = function () {
+ let count = this._model.countFilesMatches();
+ if (count.matches === 0 && this._model.queryInfo) {
+ // Only redirect to showNoResults() when the model has a valid query
+ // (i.e. this is a real "no results" state, not a transient clear).
+ this.showNoResults();
+ return;
+ }
+
+ this._panel.$panel.removeClass("search-no-results");
+
let searchItems, match, i, item, multiLine,
- count = this._model.countFilesMatches(),
searchFiles = this._model.prioritizeOpenFile(this._initialFilePath),
lastIndex = this._getLastIndex(count.matches),
matchesCounter = 0,
@@ -823,12 +825,43 @@ define(function (require, exports, module) {
this._model.on("change.SearchResultsView", this._handleModelChange.bind(this));
};
+ /**
+ * Opens the panel and displays a "no results" message instead of closing it.
+ * Keeps the tab visible so the user gets clear feedback without jarring tab switches.
+ * @param {string=} message Optional message to display. Defaults to Strings.FIND_NO_RESULTS.
+ */
+ SearchResultsView.prototype.showNoResults = function (message) {
+ this._currentStart = 0;
+ this._$selectedRow = null;
+ this._allChecked = false;
+
+ if (this._timeoutID) {
+ window.clearTimeout(this._timeoutID);
+ this._timeoutID = null;
+ }
+
+ this._$table.empty();
+ this._closePreviewEditor();
+
+ this._panel.$panel.addClass("search-no-results");
+ this._showSummary();
+ this._$table.append(
+ $('
').text(message || Strings.FIND_NO_RESULTS)
+ );
+
+ this._panel.$panel.off(".searchResults");
+ this._model.off("change.SearchResultsView");
+
+ this._panel.show();
+ };
+
/**
* Hides the Search Results Panel and unregisters listeners.
*/
SearchResultsView.prototype.close = function () {
if (this._panel && this._panel.isVisible()) {
this._$table.empty();
+ this._panel.$panel.removeClass("search-no-results");
this._panel.hide();
this._panel.$panel.off(".searchResults");
this._model.off("change.SearchResultsView");
diff --git a/src/styles/Extn-BottomPanelTabs.less b/src/styles/Extn-BottomPanelTabs.less
new file mode 100644
index 0000000000..d2bb588270
--- /dev/null
+++ b/src/styles/Extn-BottomPanelTabs.less
@@ -0,0 +1,319 @@
+/*
+ * GNU AGPL-3.0 License
+ *
+ * Copyright (c) 2021 - present core.ai . All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
+ *
+ */
+
+/* Bottom panel tab bar — switches between tabbed bottom panels.
+ * Visual style mirrors the file tab bar (Extn-TabBar.less) for consistency. */
+
+#bottom-panel-container {
+ background-color: @bc-panel-bg;
+ border-top: 1px solid @bc-panel-border;
+ display: flex;
+ flex-direction: column;
+
+ .dark & {
+ background-color: @dark-bc-panel-bg;
+ border-top: 1px solid @dark-bc-panel-border;
+ }
+
+ .bottom-panel {
+ display: none !important;
+ flex: 1;
+ min-height: 0;
+ border-top: none;
+ height: auto !important;
+
+ &.active-bottom-panel {
+ display: flex !important;
+ flex-direction: column;
+ }
+
+ .toolbar {
+ box-shadow: none;
+ }
+
+ .resizable-content {
+ flex: 1;
+ min-height: 0;
+ height: auto !important;
+ }
+ }
+}
+
+#bottom-panel-tab-bar {
+ display: flex;
+ align-items: center;
+ height: 2rem;
+ min-height: 2rem;
+ background-color: #f5f5f5;
+ border-bottom: 1px solid #d6d6d6;
+ overflow: hidden;
+ user-select: none;
+
+ .dark & {
+ background-color: #222222;
+ border-bottom: 1px solid #333;
+ }
+}
+
+.bottom-panel-tabs-overflow {
+ flex: 1;
+ display: flex;
+ overflow-x: auto;
+ overflow-y: hidden;
+ height: 100%;
+
+ /* Hide scrollbar but allow scrolling */
+ &::-webkit-scrollbar {
+ display: none;
+ }
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
+
+.bottom-panel-tab {
+ display: inline-flex;
+ align-items: center;
+ padding: 0 0.4rem 0 0.8rem;
+ height: 100%;
+ cursor: pointer;
+ position: relative;
+ flex: 0 0 auto;
+ min-width: fit-content;
+ color: #555;
+ background-color: #f1f1f1;
+ border-right: 1px solid rgba(0, 0, 0, 0.05);
+ font-size: 1rem;
+ font-weight: 500;
+ letter-spacing: 0.5px;
+ white-space: nowrap;
+ transition: color 0.12s ease-out, background-color 0.12s ease-out;
+
+ .dark & {
+ color: #aaa;
+ background-color: #292929;
+ border-right: 1px solid rgba(255, 255, 255, 0.05);
+ }
+
+ &:hover {
+ background-color: #e0e0e0;
+
+ .dark & {
+ background-color: #3b3a3a;
+ }
+ }
+
+ &.active {
+ color: #333;
+ background-color: #fff;
+
+ .dark & {
+ color: #dedede;
+ background-color: #1D1F21;
+ }
+
+ &::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 0.1rem;
+ background-color: #0078D7;
+
+ .dark & {
+ background-color: #75BEFF;
+ }
+ }
+ }
+}
+
+.bottom-panel-tab-title {
+ pointer-events: none;
+}
+
+.bottom-panel-tab-close-btn {
+ margin-left: 0.55rem;
+ border-radius: 3px;
+ cursor: pointer;
+ color: #999;
+ font-size: 1.25rem;
+ font-weight: 500;
+ padding: 0 4px;
+ line-height: 1;
+ opacity: 0;
+ transition: opacity 0.12s ease, color 0.12s ease, background-color 0.12s ease;
+
+ .dark & {
+ color: #666;
+ }
+
+ .bottom-panel-tab:hover & {
+ opacity: 1;
+ color: #666;
+
+ .dark & {
+ color: #888;
+ }
+ }
+
+ .bottom-panel-tab.active & {
+ opacity: 1;
+ color: #666;
+
+ .dark & {
+ color: #888;
+ }
+ }
+
+ &:hover {
+ opacity: 1;
+ color: #333;
+ background-color: rgba(0, 0, 0, 0.1);
+
+ .dark & {
+ color: #fff;
+ background-color: rgba(255, 255, 255, 0.12);
+ }
+ }
+}
+
+.bottom-panel-tab-bar-actions {
+ display: flex;
+ align-items: center;
+ height: 100%;
+ margin-left: auto;
+ padding: 0 0.25rem;
+ flex: 0 0 auto;
+}
+
+.bottom-panel-hide-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 1.6rem;
+ height: 1.4rem;
+ margin-right: 0.4rem;
+ border-radius: 3px;
+ cursor: pointer;
+ color: #666;
+ font-size: 0.7rem;
+ -webkit-text-stroke: 0.4px;
+ line-height: 1;
+ transition: color 0.12s ease, background-color 0.12s ease;
+
+ .dark & {
+ color: #aaa;
+ }
+
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.1);
+ color: #333;
+
+ .dark & {
+ background-color: rgba(255, 255, 255, 0.12);
+ color: #eee;
+ }
+ }
+}
+
+.default-panel-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ padding: 16px;
+ gap: 12px;
+ user-select: none;
+}
+
+.default-panel-heading {
+ font-size: 14px;
+ letter-spacing: 0.6px;
+ word-spacing: 1px;
+ font-weight: 500;
+ color: #555;
+ margin-bottom: 4px;
+
+ .dark & {
+ color: #bbb;
+ }
+}
+
+.default-panel-buttons {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 8px;
+}
+
+.default-panel-btn {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ width: 120px;
+ height: 72px;
+ border: 1px solid rgba(0, 0, 0, 0.12);
+ border-radius: 6px;
+ background: rgba(0, 0, 0, 0.03);
+ color: #444;
+ cursor: pointer;
+ transition: background-color 0.15s, border-color 0.15s;
+
+ i {
+ font-size: 20px;
+ }
+
+ .default-panel-btn-label {
+ font-size: 11px;
+ text-align: center;
+ line-height: 1.2;
+ max-width: 110px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ &:hover {
+ background: rgba(0, 0, 0, 0.07);
+ border-color: rgba(0, 0, 0, 0.25);
+ }
+
+ &:active {
+ background: rgba(0, 0, 0, 0.12);
+ }
+
+ .dark & {
+ border-color: rgba(255, 255, 255, 0.15);
+ background: rgba(255, 255, 255, 0.05);
+ color: #ccc;
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.1);
+ border-color: rgba(255, 255, 255, 0.3);
+ }
+
+ &:active {
+ background: rgba(255, 255, 255, 0.15);
+ }
+ }
+}
diff --git a/src/styles/Extn-CustomSnippets.less b/src/styles/Extn-CustomSnippets.less
index 57e6b32966..9f14524cf2 100644
--- a/src/styles/Extn-CustomSnippets.less
+++ b/src/styles/Extn-CustomSnippets.less
@@ -27,42 +27,30 @@
align-items: center;
gap: 10px;
flex-wrap: wrap;
+ margin-left: 4px;
}
.buttons {
gap: 4px;
flex-wrap: wrap;
align-items: center;
+ margin: 0;
}
}
-.toolbar-title {
- color: @bc-text;
- font-size: 15px;
- font-weight: 500;
-
- .dark & {
- color: @dark-bc-text;
- }
-}
-
-.snippets-count {
- color: @bc-text;
- font-size: 15px;
- font-weight: 500;
-
- .dark & {
- color: @dark-bc-text;
- }
-}
.custom-snippet-btn button {
- height: 26px;
+ height: 21px;
border-radius: 4px;
background-color: @bc-btn-bg;
color: @bc-text;
border: 1px solid @bc-btn-border;
box-shadow: inset 0 1px @bc-highlight;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 0 8px;
+ font-size: 12px;
.dark & {
background-color: @dark-bc-btn-bg;
@@ -72,11 +60,6 @@
}
}
-.custom-snippet-btn button {
- display: flex;
- align-items: center;
-}
-
.custom-snippet-btn .back-btn-left-icon {
position: relative;
top: 0.6px;
@@ -93,14 +76,6 @@
top: 0.6px;
}
-#custom-snippets-panel .close {
- padding-top: 2.5px;
-}
-
-#custom-snippets-panel .close:hover {
- cursor: pointer;
-}
-
.filter-snippets-panel {
display: inline-block;
}
@@ -113,8 +88,7 @@
height: 14px;
min-width: 120px;
margin-bottom: 0;
- margin-top: -4px;
- margin-right: 30px;
+ margin-right: 4px;
}
#custom-snippets-list.hidden {
diff --git a/src/styles/Extn-DisplayShortcuts.less b/src/styles/Extn-DisplayShortcuts.less
index 77765ffbf0..a326162b06 100644
--- a/src/styles/Extn-DisplayShortcuts.less
+++ b/src/styles/Extn-DisplayShortcuts.less
@@ -106,15 +106,15 @@
}
#shortcuts-panel .toolbar {
- display: block;
- padding-right: 28px;
+ display: flex;
+ justify-content: flex-end;
+ padding-right: 4px;
}
#shortcuts-panel .toolbar button.reset-to-default,
#shortcuts-panel .toolbar button.presetPicker,
#shortcuts-panel .toolbar .filter {
- float: right;
- margin: 4px 10px;
+ margin: 1px 8px 1px 6px;
padding-top: 2px;
padding-bottom: 2px;
}
diff --git a/src/styles/brackets.less b/src/styles/brackets.less
index a0f0c46faa..72fbe70317 100644
--- a/src/styles/brackets.less
+++ b/src/styles/brackets.less
@@ -47,6 +47,7 @@
@import "Extn-CustomSnippets.less";
@import "Extn-CollapseFolders.less";
@import "Extn-SidebarTabs.less";
+@import "Extn-BottomPanelTabs.less";
@import "Extn-AIChatPanel.less";
@import "UserProfile.less";
@import "phoenix-pro.less";
@@ -647,6 +648,14 @@ a, img {
animation: brightenFade 2s ease-in-out;
}
+#status-panel-toggle {
+ cursor: pointer;
+}
+
+#status-panel-toggle.flash {
+ animation: brightenFade 800ms ease-in-out;
+}
+
@keyframes brightenFade {
0% {
background-color: transparent;
@@ -1899,11 +1908,14 @@ a, img {
/* Find in Files results panel - temporary UI, to be replaced with a richer search feature later */
+.search-results .toolbar {
+ padding: 5px 8px;
+}
.search-results .title {
.sane-box-model;
- padding-right: 20px;
+ margin-left: 4px;
width: 100%;
- line-height: 25px;
+ line-height: 26px;
.flex-box;
.contracting-col {
@@ -1969,6 +1981,31 @@ a, img {
}
}
+.search-results.search-no-results {
+ .table-container {
+ width: 100% !important;
+ }
+ .search-editor-preview {
+ display: none !important;
+ }
+}
+
+.search-no-results-message {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ color: #888;
+ font-size: 15px;
+ letter-spacing: 0.5px;
+ word-spacing: 1px;
+ user-select: none;
+
+ .dark & {
+ color: #999;
+ }
+}
+
.search-results .disclosure-triangle,
#problems-panel .disclosure-triangle {
.expand-collapse-triangle();
@@ -2365,6 +2402,7 @@ a, img {
.search-input-container {
display: inline-flex;
+ vertical-align: top;
}
.filter-container{
margin-left: -11px;
@@ -3032,6 +3070,12 @@ textarea.exclusions-editor {
#problems-panel {
.user-select(text); // allow selecting error messages for easy web searching
+ .toolbar {
+ padding: 9px 8px;
+ }
+ .title {
+ margin-left: 4px;
+ }
.line {
text-align: right; // make line number line up with editor line numbers
}
diff --git a/src/view/DefaultPanelView.js b/src/view/DefaultPanelView.js
new file mode 100644
index 0000000000..b342d24c9c
--- /dev/null
+++ b/src/view/DefaultPanelView.js
@@ -0,0 +1,204 @@
+/*
+ * GNU AGPL-3.0 License
+ *
+ * Copyright (c) 2021 - present core.ai . All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU Affero General Public License as published by the Free
+ * Software Foundation, either version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License along
+ * with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
+ *
+ */
+
+/**
+ * DefaultPanelView - A launcher panel shown in the bottom panel area when no
+ * other panels are open. Provides quick-access buttons for common panels
+ * (Problems, Find in Files, Git, Custom Snippets, Keyboard Shortcuts) and a
+ * link to the documentation.
+ *
+ * @module view/DefaultPanelView
+ */
+define(function (require, exports, module) {
+
+ const AppInit = require("utils/AppInit"),
+ Commands = require("command/Commands"),
+ CommandManager = require("command/CommandManager"),
+ Strings = require("strings"),
+ WorkspaceManager = require("view/WorkspaceManager"),
+ PanelView = require("view/PanelView");
+
+ /**
+ * Descriptors for each launcher button.
+ */
+ const _panelButtons = [
+ {
+ id: "problems",
+ icon: "fa-solid fa-triangle-exclamation",
+ label: Strings.CMD_VIEW_TOGGLE_PROBLEMS || "Problems",
+ commandID: Commands.VIEW_TOGGLE_PROBLEMS
+ },
+ {
+ id: "search",
+ icon: "fa-solid fa-magnifying-glass",
+ label: Strings.CMD_FIND_IN_FILES || "Find in Files",
+ commandID: Commands.CMD_FIND_IN_FILES
+ },
+ {
+ id: "git",
+ icon: "fa-solid fa-code-branch",
+ label: Strings.GIT_PANEL_TITLE || "Git",
+ commandID: Commands.CMD_GIT_TOGGLE_PANEL
+ },
+ {
+ id: "snippets",
+ icon: "fa-solid fa-code",
+ label: Strings.CUSTOM_SNIPPETS_PANEL_TITLE || "Custom Snippets",
+ commandID: "custom_snippets"
+ },
+ {
+ id: "shortcuts",
+ icon: "fa-solid fa-keyboard",
+ label: Strings.KEYBOARD_SHORTCUT_PANEL_TITLE || "Keyboard Shortcuts",
+ commandID: Commands.HELP_TOGGLE_SHORTCUTS_PANEL
+ }
+ ];
+
+ /** @type {Panel} The default panel instance */
+ let _panel;
+
+ /** @type {jQueryObject} The panel DOM element */
+ let _$panel;
+
+ /**
+ * Build the panel DOM.
+ * @return {jQueryObject}
+ * @private
+ */
+ function _buildPanelHTML() {
+ let $panel = $('
');
+ let $content = $('
');
+ let $heading = $('
')
+ .text(Strings.BOTTOM_PANEL_DEFAULT_HEADING);
+ $content.append($heading);
+
+ let $buttonsRow = $('
');
+
+ _panelButtons.forEach(function (btn) {
+ let $button = $('
')
+ .attr("data-command", btn.commandID)
+ .attr("data-btn-id", btn.id)
+ .attr("title", btn.label);
+ let $icon = $('
').addClass(btn.icon);
+ let $label = $('
').text(btn.label);
+ $button.append($icon).append($label);
+ $buttonsRow.append($button);
+ });
+
+ $content.append($buttonsRow);
+
+ $panel.append($content);
+ return $panel;
+ }
+
+ /**
+ * Check whether Git is available for the current project.
+ * The Git extension hides its toolbar icon with the "forced-hidden" class
+ * when Git is not available (no binary, not a repo, extension disabled, etc.).
+ * @return {boolean}
+ * @private
+ */
+ function _isGitAvailable() {
+ const $gitIcon = $("#git-toolbar-icon");
+ return $gitIcon.length > 0 && !$gitIcon.hasClass("forced-hidden");
+ }
+
+ /**
+ * Check whether there are currently lint errors/warnings.
+ * The status bar indicator gets the "inspection-errors" class when problems exist.
+ * @return {boolean}
+ * @private
+ */
+ function _hasProblems() {
+ const $indicator = $("#status-inspection");
+ return $indicator.length > 0 && $indicator.hasClass("inspection-errors");
+ }
+
+ /**
+ * Show or hide the Git and Problems buttons based on current state.
+ * @private
+ */
+ function _updateButtonVisibility() {
+ if (!_$panel) {
+ return;
+ }
+ _$panel.find('.default-panel-btn[data-btn-id="git"]').toggle(_isGitAvailable());
+ _$panel.find('.default-panel-btn[data-btn-id="problems"]').toggle(_hasProblems());
+ }
+
+ /**
+ * Set up MutationObservers on the Git toolbar icon and status-inspection
+ * indicator so that button visibility updates live.
+ * @private
+ */
+ function _observeStateChanges() {
+ // Watch Git toolbar icon for class changes (forced-hidden added/removed)
+ const gitIcon = document.getElementById("git-toolbar-icon");
+ if (gitIcon) {
+ const gitObserver = new MutationObserver(_updateButtonVisibility);
+ gitObserver.observe(gitIcon, {attributes: true, attributeFilter: ["class"]});
+ }
+
+ // Watch status-inspection indicator for class changes (inspection-errors)
+ const statusInspection = document.getElementById("status-inspection");
+ if (statusInspection) {
+ const inspectionObserver = new MutationObserver(_updateButtonVisibility);
+ inspectionObserver.observe(statusInspection, {attributes: true, attributeFilter: ["class"]});
+ }
+ }
+
+ /**
+ * Initialise the default panel. Called once at appReady.
+ * @private
+ */
+ function _init() {
+ _$panel = _buildPanelHTML();
+ _panel = WorkspaceManager.createBottomPanel(
+ WorkspaceManager.DEFAULT_PANEL_ID,
+ _$panel,
+ undefined,
+ Strings.BOTTOM_PANEL_DEFAULT_TITLE
+ );
+
+ // Button click handler: execute the command to open the target panel.
+ // The auto-hide listener (EVENT_PANEL_SHOWN) will close the default panel.
+ _$panel.on("click", ".default-panel-btn", function () {
+ let commandID = $(this).attr("data-command");
+ if (commandID) {
+ CommandManager.execute(commandID);
+ }
+ });
+
+ // Auto-hide when any other panel is shown.
+ // hide() is a no-op if the panel is already closed, so no guard needed.
+ PanelView.on(PanelView.EVENT_PANEL_SHOWN, function (event, panelID) {
+ if (panelID !== WorkspaceManager.DEFAULT_PANEL_ID) {
+ _panel.hide();
+ }
+ if (panelID === WorkspaceManager.DEFAULT_PANEL_ID) {
+ _updateButtonVisibility();
+ }
+ });
+
+ // Initial visibility update and set up live observers
+ _updateButtonVisibility();
+ _observeStateChanges();
+ }
+
+ AppInit.appReady(_init);
+});
diff --git a/src/view/PanelView.js b/src/view/PanelView.js
index d3c6e266fa..a857972fc1 100644
--- a/src/view/PanelView.js
+++ b/src/view/PanelView.js
@@ -26,8 +26,9 @@
define(function (require, exports, module) {
const EventDispatcher = require("utils/EventDispatcher"),
- Resizer = require("utils/Resizer");
-
+ Resizer = require("utils/Resizer"),
+ Strings = require("strings");
+
/**
* Event when panel is hidden
* @type {string}
@@ -49,15 +50,133 @@ define(function (require, exports, module) {
*/
const PANEL_TYPE_BOTTOM_PANEL = 'bottomPanel';
+ // --- Module-level tab state ---
+
+ /** @type {Object.
} Maps panel ID to Panel instance */
+ let _panelMap = {};
+
+ /** @type {jQueryObject} The single container wrapping all bottom panels */
+ let _$container;
+
+ /** @type {jQueryObject} The tab bar inside the container */
+ let _$tabBar;
+
+ /** @type {jQueryObject} Scrollable area holding the tab elements */
+ let _$tabsOverflow;
+
+ /** @type {string[]} Ordered list of currently open (tabbed) panel IDs */
+ let _openIds = [];
+
+ /** @type {string|null} The panel ID of the currently visible (active) tab */
+ let _activeId = null;
+
+ // --- Tab helper functions ---
+
+ /**
+ * Resolve the display title for a bottom panel tab.
+ * Uses the explicit title if provided, then checks for a .toolbar .title
+ * DOM element in the panel, and finally derives a name from the panel id.
+ * @param {string} id The panel registration ID
+ * @param {jQueryObject} $panel The panel's jQuery element
+ * @param {string=} title Explicit title passed to createBottomPanel
+ * @return {string}
+ * @private
+ */
+ function _getPanelTitle(id, $panel, title) {
+ if (title) {
+ return title;
+ }
+ let $titleEl = $panel.find(".toolbar .title");
+ if ($titleEl.length && $.trim($titleEl.text())) {
+ return $.trim($titleEl.text());
+ }
+ let label = id.replace(new RegExp("[-_.]", "g"), " ").split(" ")[0];
+ return label.charAt(0).toUpperCase() + label.slice(1);
+ }
+
+ /**
+ * Full rebuild of the tab bar DOM from _openIds.
+ * Call this when tabs are added, removed, or renamed.
+ * @private
+ */
+ function _updateBottomPanelTabBar() {
+ if (!_$tabsOverflow) {
+ return;
+ }
+ _$tabsOverflow.empty();
+
+ _openIds.forEach(function (panelId) {
+ let panel = _panelMap[panelId];
+ if (!panel) {
+ return;
+ }
+ let title = panel._tabTitle || _getPanelTitle(panelId, panel.$panel);
+ let isActive = (panelId === _activeId);
+ let $tab = $('')
+ .toggleClass('active', isActive)
+ .attr('data-panel-id', panelId);
+ $tab.append($('').text(title));
+ $tab.append($('×').attr('title', Strings.CLOSE));
+ _$tabsOverflow.append($tab);
+ });
+ }
+
+ /**
+ * Swap the .active class on the tab bar without rebuilding the DOM.
+ * @private
+ */
+ function _updateActiveTabHighlight() {
+ if (!_$tabBar) {
+ return;
+ }
+ _$tabBar.find(".bottom-panel-tab").each(function () {
+ let $tab = $(this);
+ if ($tab.data("panel-id") === _activeId) {
+ $tab.addClass("active");
+ } else {
+ $tab.removeClass("active");
+ }
+ });
+ }
+
+ /**
+ * Switch the active tab to the given panel. Does not show/hide the container.
+ * @param {string} panelId
+ * @private
+ */
+ function _switchToTab(panelId) {
+ if (_activeId === panelId) {
+ return;
+ }
+ // Remove active class from current
+ if (_activeId) {
+ let prevPanel = _panelMap[_activeId];
+ if (prevPanel) {
+ prevPanel.$panel.removeClass("active-bottom-panel");
+ }
+ }
+ // Set new active
+ _activeId = panelId;
+ let newPanel = _panelMap[panelId];
+ if (newPanel) {
+ newPanel.$panel.addClass("active-bottom-panel");
+ }
+ _updateActiveTabHighlight();
+ }
+
/**
* Represents a panel below the editor area (a child of ".content").
* @constructor
* @param {!jQueryObject} $panel The entire panel, including any chrome, already in the DOM.
+ * @param {string} id Unique panel identifier.
+ * @param {string=} title Optional display title for the tab bar.
*/
- function Panel($panel, id) {
+ function Panel($panel, id, title) {
this.$panel = $panel;
this.panelID = id;
+ this._tabTitle = _getPanelTitle(id, $panel, title);
+ _panelMap[id] = this;
}
/**
@@ -71,7 +190,7 @@ define(function (require, exports, module) {
* @return {boolean} true if visible, false if not
*/
Panel.prototype.isVisible = function () {
- return this.$panel.is(":visible");
+ return (_activeId === this.panelID) && _$container && _$container.is(":visible");
};
/**
@@ -104,20 +223,75 @@ define(function (require, exports, module) {
* Shows the panel
*/
Panel.prototype.show = function () {
- if(!this.isVisible() && this.canBeShown()){
- Resizer.show(this.$panel[0]);
- exports.trigger(EVENT_PANEL_SHOWN, this.panelID);
+ if (!this.canBeShown()) {
+ return;
}
+ let panelId = this.panelID;
+ let isOpen = _openIds.indexOf(panelId) !== -1;
+ let isActive = (_activeId === panelId);
+
+ if (isOpen && isActive) {
+ // Already open and active — just ensure container is visible
+ if (!_$container.is(":visible")) {
+ Resizer.show(_$container[0]);
+ exports.trigger(EVENT_PANEL_SHOWN, panelId);
+ }
+ return;
+ }
+ if (isOpen && !isActive) {
+ // Open but not active - switch tab and ensure container is visible
+ _switchToTab(panelId);
+ if (!_$container.is(":visible")) {
+ Resizer.show(_$container[0]);
+ }
+ exports.trigger(EVENT_PANEL_SHOWN, panelId);
+ return;
+ }
+ // Not open: add to open set
+ _openIds.push(panelId);
+
+ // Show container if it was hidden
+ if (!_$container.is(":visible")) {
+ Resizer.show(_$container[0]);
+ }
+
+ _switchToTab(panelId);
+ _updateBottomPanelTabBar();
+ exports.trigger(EVENT_PANEL_SHOWN, panelId);
};
/**
* Hides the panel
*/
Panel.prototype.hide = function () {
- if(this.isVisible()){
- Resizer.hide(this.$panel[0]);
- exports.trigger(EVENT_PANEL_HIDDEN, this.panelID);
+ let panelId = this.panelID;
+ let idx = _openIds.indexOf(panelId);
+ if (idx === -1) {
+ // Not open - no-op
+ return;
+ }
+
+ // Remove from open set
+ _openIds.splice(idx, 1);
+ this.$panel.removeClass("active-bottom-panel");
+
+ let wasActive = (_activeId === panelId);
+
+ // Tab was removed — rebuild tab bar, then activate next if needed
+ if (wasActive && _openIds.length > 0) {
+ let nextIdx = Math.min(idx, _openIds.length - 1);
+ let nextId = _openIds[nextIdx];
+ _activeId = null; // clear so _switchToTab runs
+ _switchToTab(nextId);
+ exports.trigger(EVENT_PANEL_SHOWN, nextId);
+ } else if (wasActive) {
+ // No more tabs - hide the container
+ _activeId = null;
+ Resizer.hide(_$container[0]);
}
+ _updateBottomPanelTabBar();
+
+ exports.trigger(EVENT_PANEL_HIDDEN, panelId);
};
/**
@@ -132,6 +306,18 @@ define(function (require, exports, module) {
}
};
+ /**
+ * Updates the display title shown in the tab bar for this panel.
+ * @param {string} newTitle The new title to display.
+ */
+ Panel.prototype.setTitle = function (newTitle) {
+ this._tabTitle = newTitle;
+ if (_$tabsOverflow) {
+ _$tabsOverflow.find('.bottom-panel-tab[data-panel-id="' + this.panelID + '"] .bottom-panel-tab-title')
+ .text(newTitle);
+ }
+ };
+
/**
* gets the Panel's type
* @return {string}
@@ -140,10 +326,103 @@ define(function (require, exports, module) {
return PANEL_TYPE_BOTTOM_PANEL;
};
+ /**
+ * Initializes the PanelView module with references to the bottom panel container DOM elements.
+ * Called by WorkspaceManager during htmlReady.
+ * @param {jQueryObject} $container The bottom panel container element.
+ * @param {jQueryObject} $tabBar The tab bar element inside the container.
+ * @param {jQueryObject} $tabsOverflow The scrollable area holding tab elements.
+ */
+ function init($container, $tabBar, $tabsOverflow) {
+ _$container = $container;
+ _$tabBar = $tabBar;
+ _$tabsOverflow = $tabsOverflow;
+
+ // Tab bar click handlers
+ _$tabBar.on("click", ".bottom-panel-tab-close-btn", function (e) {
+ e.stopPropagation();
+ let panelId = $(this).closest(".bottom-panel-tab").data("panel-id");
+ if (panelId) {
+ let panel = _panelMap[panelId];
+ if (panel) {
+ panel.hide();
+ }
+ }
+ });
+
+ _$tabBar.on("click", ".bottom-panel-tab", function (e) {
+ let panelId = $(this).data("panel-id");
+ if (panelId && panelId !== _activeId) {
+ let panel = _panelMap[panelId];
+ if (panel) {
+ panel.show();
+ }
+ }
+ });
+
+ // Hide-panel button collapses the container but keeps tabs intact
+ _$tabBar.on("click", ".bottom-panel-hide-btn", function (e) {
+ e.stopPropagation();
+ if (_$container.is(":visible")) {
+ Resizer.hide(_$container[0]);
+ }
+ });
+ }
+
+ /**
+ * Returns a copy of the currently open bottom panel IDs in tab order.
+ * @return {string[]}
+ */
+ function getOpenBottomPanelIDs() {
+ return _openIds.slice();
+ }
+
+ /**
+ * Hides every open bottom panel tab in a single batch
+ * @return {string[]} The IDs of panels that were open (useful for restoring later).
+ */
+ function hideAllOpenPanels() {
+ if (_openIds.length === 0) {
+ return [];
+ }
+ let closedIds = _openIds.slice();
+
+ // Remove visual active state from every panel
+ for (let i = 0; i < closedIds.length; i++) {
+ let panel = _panelMap[closedIds[i]];
+ if (panel) {
+ panel.$panel.removeClass("active-bottom-panel");
+ }
+ }
+
+ // Clear internal state BEFORE hiding the container so the
+ // panelCollapsed handler sees an empty _openIds and doesn't
+ // redundantly update the stacks.
+ _openIds = [];
+ _activeId = null;
+
+ if (_$container && _$container.is(":visible")) {
+ Resizer.hide(_$container[0]);
+ }
+
+ _updateBottomPanelTabBar();
+
+ // Fire one EVENT_PANEL_HIDDEN per panel for stack tracking.
+ // No intermediate EVENT_PANEL_SHOWN events are emitted.
+ for (let i = 0; i < closedIds.length; i++) {
+ exports.trigger(EVENT_PANEL_HIDDEN, closedIds[i]);
+ }
+
+ return closedIds;
+ }
+
EventDispatcher.makeEventDispatcher(exports);
// Public API
exports.Panel = Panel;
+ exports.init = init;
+ exports.getOpenBottomPanelIDs = getOpenBottomPanelIDs;
+ exports.hideAllOpenPanels = hideAllOpenPanels;
//events
exports.EVENT_PANEL_HIDDEN = EVENT_PANEL_HIDDEN;
exports.EVENT_PANEL_SHOWN = EVENT_PANEL_SHOWN;
diff --git a/src/view/WorkspaceManager.js b/src/view/WorkspaceManager.js
index c56bbb4d95..45412239c0 100644
--- a/src/view/WorkspaceManager.js
+++ b/src/view/WorkspaceManager.js
@@ -40,11 +40,20 @@ define(function (require, exports, module) {
EventDispatcher = require("utils/EventDispatcher"),
KeyBindingManager = require("command/KeyBindingManager"),
Resizer = require("utils/Resizer"),
+ AnimationUtils = require("utils/AnimationUtils"),
+ Strings = require("strings"),
PluginPanelView = require("view/PluginPanelView"),
PanelView = require("view/PanelView"),
EditorManager = require("editor/EditorManager"),
KeyEvent = require("utils/KeyEvent");
+ /**
+ * Panel ID for the default launcher panel shown when no other panels are open.
+ * @const
+ * @private
+ */
+ const DEFAULT_PANEL_ID = "workspace.defaultPanel";
+
/**
* Event triggered when the workspace layout updates.
@@ -120,9 +129,16 @@ define(function (require, exports, module) {
*/
var windowResizing = false;
- let lastHiddenBottomPanelStack = [],
- lastShownBottomPanelStack = [];
+ let lastShownBottomPanelStack = [];
+ /** @type {jQueryObject} The single container wrapping all bottom panels */
+ let $bottomPanelContainer;
+
+ /** @type {jQueryObject} Chevron toggle in the status bar */
+ let $statusBarPanelToggle;
+
+ /** @type {boolean} True while the status bar toggle button is handling a click */
+ let _statusBarToggleInProgress = false;
/**
* Calculates the available height for the full-size Editor (or the no-editor placeholder),
@@ -226,7 +242,6 @@ define(function (require, exports, module) {
});
}
-
/**
* Creates a new resizable panel beneath the editor area and above the status bar footer. Panel is initially invisible.
* The panel's size & visibility are automatically saved & restored as a view-state preference.
@@ -234,21 +249,16 @@ define(function (require, exports, module) {
* @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. Must have an id
* attribute, for use as a preferences key.
- * @param {number=} minSize Minimum height of panel in px.
+ * @param {number=} minSize @deprecated No longer used. Pass `undefined`.
+ * @param {string=} title Display title shown in the bottom panel tab bar.
* @return {!Panel}
*/
- function createBottomPanel(id, $panel, minSize) {
- $panel.insertBefore("#status-bar");
+ function createBottomPanel(id, $panel, minSize, title) {
+ $bottomPanelContainer.append($panel);
$panel.hide();
- updateResizeLimits(); // initialize panel's max size
-
- let bottomPanel = new PanelView.Panel($panel, id);
+ updateResizeLimits();
+ let bottomPanel = new PanelView.Panel($panel, id, title);
panelIDMap[id] = bottomPanel;
-
- Resizer.makeResizable($panel[0], Resizer.DIRECTION_VERTICAL, Resizer.POSITION_TOP, minSize,
- false, undefined, true);
- listenToResize($panel);
-
return bottomPanel;
}
@@ -324,6 +334,81 @@ define(function (require, exports, module) {
$mainPluginPanel = $("#main-plugin-panel");
$pluginIconsBar = $("#plugin-icons-bar");
+ // --- Create the bottom panel tabbed container ---
+ $bottomPanelContainer = $('');
+ let $bottomPanelTabBar = $('');
+ let $bottomPanelTabsOverflow = $('');
+ let $tabBarActions = $('');
+ $tabBarActions.append(
+ $('')
+ .attr('title', Strings.BOTTOM_PANEL_HIDE)
+ );
+ $bottomPanelTabBar.append($bottomPanelTabsOverflow);
+ $bottomPanelTabBar.append($tabBarActions);
+ $bottomPanelContainer.append($bottomPanelTabBar);
+ $bottomPanelContainer.insertBefore("#status-bar");
+ $bottomPanelContainer.hide();
+
+ // Initialize PanelView with container DOM references and tab bar click handlers
+ PanelView.init($bottomPanelContainer, $bottomPanelTabBar, $bottomPanelTabsOverflow);
+
+ // Create status bar chevron toggle for bottom panel
+ $statusBarPanelToggle = $('
')
+ .attr('title', Strings.BOTTOM_PANEL_SHOW);
+ $("#status-indicators").prepend($statusBarPanelToggle);
+
+ $statusBarPanelToggle.on("click", function () {
+ _statusBarToggleInProgress = true;
+ if ($bottomPanelContainer.is(":visible")) {
+ Resizer.hide($bottomPanelContainer[0]);
+ triggerUpdateLayout();
+ } else if (PanelView.getOpenBottomPanelIDs().length > 0) {
+ Resizer.show($bottomPanelContainer[0]);
+ triggerUpdateLayout();
+ } else {
+ _showDefaultPanel();
+ }
+ _statusBarToggleInProgress = false;
+ });
+
+ // Make the container resizable (not individual panels)
+ Resizer.makeResizable($bottomPanelContainer[0], Resizer.DIRECTION_VERTICAL, Resizer.POSITION_TOP,
+ 200, false, undefined, true);
+ listenToResize($bottomPanelContainer);
+
+ $bottomPanelContainer.on("panelCollapsed", function () {
+ $statusBarPanelToggle.find("i")
+ .removeClass("fa-chevron-down")
+ .addClass("fa-chevron-up");
+ $statusBarPanelToggle.attr("title", Strings.BOTTOM_PANEL_SHOW);
+ if (!_statusBarToggleInProgress) {
+ AnimationUtils.animateUsingClass($statusBarPanelToggle[0], "flash", 800);
+ }
+ // When the container collapses while tabs are still open, clear the
+ // shown stack so the Escape-key handler doesn't silently close tabs
+ // that the user can't even see.
+ let openIds = PanelView.getOpenBottomPanelIDs();
+ for (let i = 0; i < openIds.length; i++) {
+ lastShownBottomPanelStack = lastShownBottomPanelStack.filter(item => item !== openIds[i]);
+ }
+ });
+
+ $bottomPanelContainer.on("panelExpanded", function () {
+ $statusBarPanelToggle.find("i")
+ .removeClass("fa-chevron-up")
+ .addClass("fa-chevron-down");
+ $statusBarPanelToggle.attr("title", Strings.BOTTOM_PANEL_HIDE_TOGGLE);
+ if (!_statusBarToggleInProgress) {
+ AnimationUtils.animateUsingClass($statusBarPanelToggle[0], "flash", 800);
+ }
+ // When the container re-expands, add the open panels to the shown stack.
+ let openIds = PanelView.getOpenBottomPanelIDs();
+ for (let i = 0; i < openIds.length; i++) {
+ lastShownBottomPanelStack = lastShownBottomPanelStack.filter(item => item !== openIds[i]);
+ lastShownBottomPanelStack.push(openIds[i]);
+ }
+ });
+
// Sidebar is a special case: it isn't a Panel, and is not created dynamically. Need to explicitly
// listen for resize here.
listenToResize($("#sidebar"));
@@ -351,16 +436,15 @@ define(function (require, exports, module) {
EventDispatcher.makeEventDispatcher(exports);
PanelView.on(PanelView.EVENT_PANEL_SHOWN, (event, panelID)=>{
- lastHiddenBottomPanelStack = lastHiddenBottomPanelStack.filter(item => item !== panelID);
lastShownBottomPanelStack = lastShownBottomPanelStack.filter(item => item !== panelID);
lastShownBottomPanelStack.push(panelID);
exports.trigger(EVENT_WORKSPACE_PANEL_SHOWN, panelID);
+ triggerUpdateLayout();
});
PanelView.on(PanelView.EVENT_PANEL_HIDDEN, (event, panelID)=>{
- lastHiddenBottomPanelStack = lastHiddenBottomPanelStack.filter(item => item !== panelID);
- lastHiddenBottomPanelStack.push(panelID);
lastShownBottomPanelStack = lastShownBottomPanelStack.filter(item => item !== panelID);
exports.trigger(EVENT_WORKSPACE_PANEL_HIDDEN, panelID);
+ triggerUpdateLayout();
});
let currentlyShownPanel = null,
@@ -492,44 +576,28 @@ define(function (require, exports, module) {
return false;
}
- function _showLastHiddenPanelIfPossible() {
- while(lastHiddenBottomPanelStack.length > 0){
- let panelToShow = getPanelForID(lastHiddenBottomPanelStack.pop());
- if(panelToShow.canBeShown()){
- panelToShow.show();
- return true;
- }
+ /**
+ * Shows the default launcher panel when no other panels are open.
+ * @private
+ */
+ function _showDefaultPanel() {
+ let defaultPanel = panelIDMap[DEFAULT_PANEL_ID];
+ if (defaultPanel) {
+ defaultPanel.show();
}
- return false;
}
function _handleEscapeKey() {
- let allPanelsIDs = getAllPanelIDs();
- // first we see if there is any least recently shown panel
- if(lastShownBottomPanelStack.length > 0){
- let panelToHide = getPanelForID(lastShownBottomPanelStack.pop());
- panelToHide.hide();
+ // Collapse the entire bottom panel container, keeping all tabs intact
+ if ($bottomPanelContainer && $bottomPanelContainer.is(":visible")) {
+ Resizer.hide($bottomPanelContainer[0]);
+ triggerUpdateLayout();
return true;
}
- // if not, see if there is any open panels that are not yet tracked in the least recently used stacks.
- for(let panelID of allPanelsIDs){
- let panel = getPanelForID(panelID);
- if(panel.getPanelType() === PanelView.PANEL_TYPE_BOTTOM_PANEL && panel.isVisible()){
- panel.hide();
- lastHiddenBottomPanelStack.push(panelID);
- return true;
- }
- }
- // no panels hidden, we will toggle the last hidden panel with succeeding escape key presses
- return _showLastHiddenPanelIfPossible();
- }
-
- function _handleShiftEscapeKey() {
- // show hidden panels one by one
- return _showLastHiddenPanelIfPossible();
+ return false;
}
- // pressing escape when focused on editor will toggle the last opened bottom panel
+ // pressing escape when focused on editor will hide the bottom panel container
function _handleKeydown(event) {
if(event.keyCode !== KeyEvent.DOM_VK_ESCAPE || KeyBindingManager.isInOverlayMode()){
return;
@@ -553,9 +621,7 @@ define(function (require, exports, module) {
return;
}
- if (event.keyCode === KeyEvent.DOM_VK_ESCAPE && event.shiftKey) {
- _handleShiftEscapeKey();
- } else if (event.keyCode === KeyEvent.DOM_VK_ESCAPE) {
+ if (event.keyCode === KeyEvent.DOM_VK_ESCAPE) {
_handleEscapeKey();
}
@@ -572,12 +638,15 @@ define(function (require, exports, module) {
exports.recomputeLayout = recomputeLayout;
exports.getAllPanelIDs = getAllPanelIDs;
exports.getPanelForID = getPanelForID;
+ exports.getOpenBottomPanelIDs = PanelView.getOpenBottomPanelIDs;
+ exports.hideAllOpenBottomPanels = PanelView.hideAllOpenPanels;
exports.addEscapeKeyEventHandler = addEscapeKeyEventHandler;
exports.removeEscapeKeyEventHandler = removeEscapeKeyEventHandler;
exports._setMockDOM = _setMockDOM;
exports.EVENT_WORKSPACE_UPDATE_LAYOUT = EVENT_WORKSPACE_UPDATE_LAYOUT;
exports.EVENT_WORKSPACE_PANEL_SHOWN = EVENT_WORKSPACE_PANEL_SHOWN;
exports.EVENT_WORKSPACE_PANEL_HIDDEN = EVENT_WORKSPACE_PANEL_HIDDEN;
+ exports.DEFAULT_PANEL_ID = DEFAULT_PANEL_ID;
/**
* Constant representing the type of bottom panel