diff --git a/src/extensions/default/DebugCommands/testBuilder.js b/src/extensions/default/DebugCommands/testBuilder.js index 38e58ddf0e..bfc9b434af 100644 --- a/src/extensions/default/DebugCommands/testBuilder.js +++ b/src/extensions/default/DebugCommands/testBuilder.js @@ -38,7 +38,7 @@ define(function (require, exports, module) { function toggleTestBuilder() { if(!$panel){ $panel = $(panelHTML); - builderPanel = WorkspaceManager.createBottomPanel("phcode-test-builder-panel", $panel, 100); + builderPanel = WorkspaceManager.createBottomPanel("phcode-test-builder-panel", $panel, 100, "Test Builder"); builderPanel.hide(); _setupPanel().then(()=>{ builderPanel.setVisible(!builderPanel.isVisible()); @@ -177,7 +177,7 @@ define(function (require, exports, module) { return; } $panel = $(panelHTML); - builderPanel = WorkspaceManager.createBottomPanel("phcode-test-builder-panel", $panel, 100); + builderPanel = WorkspaceManager.createBottomPanel("phcode-test-builder-panel", $panel, 100, "Test Builder"); builderPanel.hide(); _setupPanel(); }); diff --git a/src/extensions/default/Git/src/Panel.js b/src/extensions/default/Git/src/Panel.js index 02a34cfcdc..cc4aac6dcb 100644 --- a/src/extensions/default/Git/src/Panel.js +++ b/src/extensions/default/Git/src/Panel.js @@ -1240,7 +1240,7 @@ define(function (require, exports) { var $panelHtml = $(panelHtml); $panelHtml.find(".git-available, .git-not-available").hide(); - gitPanel = WorkspaceManager.createBottomPanel("main-git.panel", $panelHtml, 100); + gitPanel = WorkspaceManager.createBottomPanel("main-git.panel", $panelHtml, 100, Strings.GIT_PANEL_TITLE); $gitPanel = gitPanel.$panel; const resizeObserver = new ResizeObserver(_panelResized); resizeObserver.observe($gitPanel[0]); diff --git a/src/extensionsIntegrated/CustomSnippets/main.js b/src/extensionsIntegrated/CustomSnippets/main.js index 5e2b1b3d0f..6ca00fbfa2 100644 --- a/src/extensionsIntegrated/CustomSnippets/main.js +++ b/src/extensionsIntegrated/CustomSnippets/main.js @@ -58,7 +58,8 @@ define(function (require, exports, module) { * @private */ function _createPanel() { - customSnippetsPanel = WorkspaceManager.createBottomPanel(PANEL_ID, $snippetsPanel, PANEL_MIN_SIZE); + customSnippetsPanel = WorkspaceManager.createBottomPanel(PANEL_ID, $snippetsPanel, PANEL_MIN_SIZE, + Strings.CUSTOM_SNIPPETS_PANEL_TITLE); customSnippetsPanel.show(); // also register the handlers diff --git a/src/extensionsIntegrated/DisplayShortcuts/main.js b/src/extensionsIntegrated/DisplayShortcuts/main.js index 857ddf79ce..96b41cd769 100644 --- a/src/extensionsIntegrated/DisplayShortcuts/main.js +++ b/src/extensionsIntegrated/DisplayShortcuts/main.js @@ -478,7 +478,8 @@ define(function (require, exports, module) { // AppInit.htmlReady() has already executed before extensions are loaded // so, for now, we need to call this ourself - panel = WorkspaceManager.createBottomPanel(TOGGLE_SHORTCUTS_ID, $(s), 300); + panel = WorkspaceManager.createBottomPanel(TOGGLE_SHORTCUTS_ID, $(s), 300, + Strings.KEYBOARD_SHORTCUT_PANEL_TITLE); panel.hide(); $shortcutsPanel = $("#shortcuts-panel"); diff --git a/src/extensionsIntegrated/NoDistractions/main.js b/src/extensionsIntegrated/NoDistractions/main.js index e0da1645eb..17894b8c54 100644 --- a/src/extensionsIntegrated/NoDistractions/main.js +++ b/src/extensionsIntegrated/NoDistractions/main.js @@ -89,13 +89,20 @@ define(function (require, exports, module) { function _hidePanelsIfRequired() { var panelIDs = WorkspaceManager.getAllPanelIDs(); _previouslyOpenPanelIDs = []; - panelIDs.forEach(function (panelID) { - var panel = WorkspaceManager.getPanelForID(panelID); - if (panel && panel.isVisible()) { - panel.hide(); - _previouslyOpenPanelIDs.push(panelID); + // Loop until no visible panels remain. In a tabbed system, hiding the + // active tab may reveal the next tab, so we must iterate. + let hiddenSomething = true; + while (hiddenSomething) { + hiddenSomething = false; + for (let i = 0; i < panelIDs.length; i++) { + let panel = WorkspaceManager.getPanelForID(panelIDs[i]); + if (panel && panel.isVisible()) { + panel.hide(); + _previouslyOpenPanelIDs.push(panelIDs[i]); + hiddenSomething = true; + } } - }); + } } /** diff --git a/src/features/FindReferencesManager.js b/src/features/FindReferencesManager.js index 0c71269278..2cb213c871 100644 --- a/src/features/FindReferencesManager.js +++ b/src/features/FindReferencesManager.js @@ -194,7 +194,8 @@ define(function (require, exports, module) { searchModel, "reference-in-files-results", "reference-in-files.results", - "reference" + "reference", + Strings.REFERENCES_PANEL_TITLE ); if(_resultsView) { _resultsView diff --git a/src/language/CodeInspection.js b/src/language/CodeInspection.js index 8cbb15d89f..01a8352bda 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(()=>{ diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index cde5aeabfb..d1fd4bd702 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -1249,6 +1249,11 @@ 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", "CMD_FIND_DOCUMENT_SYMBOLS": "Find Document Symbols", "CMD_FIND_PROJECT_SYMBOLS": "Find Project Symbols", @@ -1396,6 +1401,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", diff --git a/src/search/FindInFilesUI.js b/src/search/FindInFilesUI.js index 6a02cd7e19..0aea171f61 100644 --- a/src/search/FindInFilesUI.js +++ b/src/search/FindInFilesUI.js @@ -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..5557cf96d9 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"); diff --git a/src/styles/Extn-BottomPanelTabs.less b/src/styles/Extn-BottomPanelTabs.less new file mode 100644 index 0000000000..327f2d62e3 --- /dev/null +++ b/src/styles/Extn-BottomPanelTabs.less @@ -0,0 +1,227 @@ +/* + * 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; + } + } +} + +#bottom-panel-tab-bar { + display: flex; + align-items: center; + height: 2rem; + min-height: 2rem; + background-color: #f5f5f5; + border-bottom: none; + overflow: hidden; + user-select: none; + + .dark & { + background-color: #222222; + } +} + +.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; + } + } +} diff --git a/src/styles/brackets.less b/src/styles/brackets.less index a0f0c46faa..500a374a68 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; diff --git a/src/view/WorkspaceManager.js b/src/view/WorkspaceManager.js index c56bbb4d95..8f2c594dcf 100644 --- a/src/view/WorkspaceManager.js +++ b/src/view/WorkspaceManager.js @@ -40,6 +40,8 @@ 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"), @@ -123,6 +125,28 @@ define(function (require, exports, module) { let lastHiddenBottomPanelStack = [], lastShownBottomPanelStack = []; + // --- Bottom panel tabbed container state --- + + /** @type {jQueryObject} The single container wrapping all bottom panels */ + let $bottomPanelContainer; + + /** @type {jQueryObject} The tab bar inside the container */ + let $bottomPanelTabBar; + + /** @type {jQueryObject} Scrollable area holding the tab elements */ + let $bottomPanelTabsOverflow; + + /** @type {jQueryObject} Chevron toggle in the status bar */ + let $statusBarPanelToggle; + + /** @type {string[]} Ordered list of currently open (tabbed) panel IDs */ + let _openBottomPanelIds = []; + + /** @type {string|null} The panel ID of the currently visible (active) tab */ + let _activeBottomPanelId = null; + + /** @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,6 +250,106 @@ define(function (require, exports, module) { }); } + // --- Bottom panel tab helpers --- + + /** + * 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 _openBottomPanelIds. + * Call this when tabs are added, removed, or renamed. + * @private + */ + function _updateBottomPanelTabBar() { + if (!$bottomPanelTabsOverflow) { + return; + } + $bottomPanelTabsOverflow.empty(); + + _openBottomPanelIds.forEach(function (panelId) { + let panel = panelIDMap[panelId]; + if (!panel) { + return; + } + let title = panel._tabTitle || _getPanelTitle(panelId, panel.$panel); + let isActive = (panelId === _activeBottomPanelId); + let $tab = $('
' + + '' + $("").text(title).html() + '' + + '×' + + '
'); + $bottomPanelTabsOverflow.append($tab); + }); + } + + /** + * Swap the .active class on the tab bar without rebuilding the DOM. + * @private + */ + function _updateActiveTabHighlight() { + if (!$bottomPanelTabBar) { + return; + } + $bottomPanelTabBar.find(".bottom-panel-tab").each(function () { + let $tab = $(this); + if ($tab.data("panel-id") === _activeBottomPanelId) { + $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 (_activeBottomPanelId === panelId) { + return; + } + // Remove active class from current + if (_activeBottomPanelId) { + let prevPanel = panelIDMap[_activeBottomPanelId]; + if (prevPanel) { + prevPanel.$panel.removeClass("active-bottom-panel"); + } + } + // Set new active + _activeBottomPanelId = panelId; + let newPanel = panelIDMap[panelId]; + if (newPanel) { + newPanel.$panel.addClass("active-bottom-panel"); + } + _updateActiveTabHighlight(); + } + + /** + * Returns a copy of the currently open bottom panel IDs in tab order. + * @return {string[]} + */ + function getOpenBottomPanelIDs() { + return _openBottomPanelIds.slice(); + } /** * Creates a new resizable panel beneath the editor area and above the status bar footer. Panel is initially invisible. @@ -234,20 +358,108 @@ 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) { + // Insert panel into the tabbed container instead of before #status-bar + $bottomPanelContainer.append($panel); $panel.hide(); - updateResizeLimits(); // initialize panel's max size + updateResizeLimits(); let bottomPanel = new PanelView.Panel($panel, id); panelIDMap[id] = bottomPanel; - Resizer.makeResizable($panel[0], Resizer.DIRECTION_VERTICAL, Resizer.POSITION_TOP, minSize, - false, undefined, true); - listenToResize($panel); + // Cache the tab title at creation time + bottomPanel._tabTitle = _getPanelTitle(id, $panel, title); + + // Do NOT call Resizer.makeResizable on individual panels. + // The container handles resizing. + + // --- Override show/hide/isVisible on the Panel instance --- + + bottomPanel.show = function () { + if (!this.canBeShown()) { + return; + } + let panelId = this.panelID; + let isOpen = _openBottomPanelIds.indexOf(panelId) !== -1; + let isActive = (_activeBottomPanelId === panelId); + + if (isOpen && isActive) { + // Already open and active — just ensure container is visible + if (!$bottomPanelContainer.is(":visible")) { + Resizer.show($bottomPanelContainer[0]); + triggerUpdateLayout(); + } + return; + } + if (isOpen && !isActive) { + // Open but not active - switch tab and ensure container is visible + _switchToTab(panelId); + if (!$bottomPanelContainer.is(":visible")) { + Resizer.show($bottomPanelContainer[0]); + } + PanelView.trigger(PanelView.EVENT_PANEL_SHOWN, panelId); + triggerUpdateLayout(); + return; + } + // Not open: add to open set + _openBottomPanelIds.push(panelId); + + // Show container if it was hidden + if (!$bottomPanelContainer.is(":visible")) { + Resizer.show($bottomPanelContainer[0]); + } + + _switchToTab(panelId); + _updateBottomPanelTabBar(); + PanelView.trigger(PanelView.EVENT_PANEL_SHOWN, panelId); + triggerUpdateLayout(); + }; + + bottomPanel.hide = function () { + let panelId = this.panelID; + let idx = _openBottomPanelIds.indexOf(panelId); + if (idx === -1) { + // Not open - no-op + return; + } + + // Remove from open set + _openBottomPanelIds.splice(idx, 1); + this.$panel.removeClass("active-bottom-panel"); + + let wasActive = (_activeBottomPanelId === panelId); + + // Tab was removed — rebuild tab bar, then activate next if needed + if (wasActive && _openBottomPanelIds.length > 0) { + let nextIdx = Math.min(idx, _openBottomPanelIds.length - 1); + let nextId = _openBottomPanelIds[nextIdx]; + _activeBottomPanelId = null; // clear so _switchToTab runs + _switchToTab(nextId); + PanelView.trigger(PanelView.EVENT_PANEL_SHOWN, nextId); + } else if (wasActive) { + // No more tabs - hide the container + _activeBottomPanelId = null; + Resizer.hide($bottomPanelContainer[0]); + } + _updateBottomPanelTabBar(); + + PanelView.trigger(PanelView.EVENT_PANEL_HIDDEN, panelId); + triggerUpdateLayout(); + }; + + bottomPanel.isVisible = function () { + return (_activeBottomPanelId === this.panelID) && + $bottomPanelContainer.is(":visible"); + }; + + bottomPanel.setTitle = function (newTitle) { + this._tabTitle = newTitle; + _updateBottomPanelTabBar(); + }; return bottomPanel; } @@ -324,6 +536,97 @@ define(function (require, exports, module) { $mainPluginPanel = $("#main-plugin-panel"); $pluginIconsBar = $("#plugin-icons-bar"); + // --- Create the bottom panel tabbed container --- + $bottomPanelContainer = $('
'); + $bottomPanelTabBar = $('
'); + $bottomPanelTabsOverflow = $('
'); + let $tabBarActions = $('
'); + $tabBarActions.append( + '' + ); + $bottomPanelTabBar.append($bottomPanelTabsOverflow); + $bottomPanelTabBar.append($tabBarActions); + $bottomPanelContainer.append($bottomPanelTabBar); + $bottomPanelContainer.insertBefore("#status-bar"); + $bottomPanelContainer.hide(); + + // Create status bar chevron toggle for bottom panel + $statusBarPanelToggle = $( + '
' + + '' + + '
' + ); + $("#status-indicators").prepend($statusBarPanelToggle); + + $statusBarPanelToggle.on("click", function () { + _statusBarToggleInProgress = true; + if ($bottomPanelContainer.is(":visible")) { + Resizer.hide($bottomPanelContainer[0]); + triggerUpdateLayout(); + } else if (_openBottomPanelIds.length > 0) { + Resizer.show($bottomPanelContainer[0]); + triggerUpdateLayout(); + } else { + _showLastHiddenPanelIfPossible(); + } + _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); + } + }); + + $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); + } + }); + + // Tab bar click handlers + $bottomPanelTabBar.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 = panelIDMap[panelId]; + if (panel) { + panel.hide(); + } + } + }); + + $bottomPanelTabBar.on("click", ".bottom-panel-tab", function (e) { + let panelId = $(this).data("panel-id"); + if (panelId && panelId !== _activeBottomPanelId) { + _switchToTab(panelId); + PanelView.trigger(PanelView.EVENT_PANEL_SHOWN, panelId); + triggerUpdateLayout(); + } + }); + + // Hide-panel button collapses the container but keeps tabs intact + $bottomPanelTabBar.on("click", ".bottom-panel-hide-btn", function (e) { + e.stopPropagation(); + if ($bottomPanelContainer.is(":visible")) { + Resizer.hide($bottomPanelContainer[0]); + triggerUpdateLayout(); + } + }); + // Sidebar is a special case: it isn't a Panel, and is not created dynamically. Need to explicitly // listen for resize here. listenToResize($("#sidebar")); @@ -572,6 +875,7 @@ define(function (require, exports, module) { exports.recomputeLayout = recomputeLayout; exports.getAllPanelIDs = getAllPanelIDs; exports.getPanelForID = getPanelForID; + exports.getOpenBottomPanelIDs = getOpenBottomPanelIDs; exports.addEscapeKeyEventHandler = addEscapeKeyEventHandler; exports.removeEscapeKeyEventHandler = removeEscapeKeyEventHandler; exports._setMockDOM = _setMockDOM;