diff --git a/src/extensions/default/Git/src/Panel.js b/src/extensions/default/Git/src/Panel.js index 3a90cac8e..a8c4d7c9b 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, Strings.GIT_PANEL_TITLE); + gitPanel = WorkspaceManager.createBottomPanel("main-git.panel", $panelHtml, 100, Strings.GIT_PANEL_TITLE, {iconClass: "fa-brands fa-git-alt"}); $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 973c994cd..d533e3770 100644 --- a/src/extensionsIntegrated/CustomSnippets/main.js +++ b/src/extensionsIntegrated/CustomSnippets/main.js @@ -59,7 +59,7 @@ define(function (require, exports, module) { */ function _createPanel() { customSnippetsPanel = WorkspaceManager.createBottomPanel(PANEL_ID, $snippetsPanel, PANEL_MIN_SIZE, - Strings.CUSTOM_SNIPPETS_PANEL_TITLE); + Strings.CUSTOM_SNIPPETS_PANEL_TITLE, {iconClass: "fa-solid fa-code"}); UIHelper.init(customSnippetsPanel); customSnippetsPanel.show(); diff --git a/src/extensionsIntegrated/DisplayShortcuts/main.js b/src/extensionsIntegrated/DisplayShortcuts/main.js index 0b8f49678..ceb6e1ca5 100644 --- a/src/extensionsIntegrated/DisplayShortcuts/main.js +++ b/src/extensionsIntegrated/DisplayShortcuts/main.js @@ -479,7 +479,7 @@ 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, - Strings.KEYBOARD_SHORTCUT_PANEL_TITLE); + Strings.KEYBOARD_SHORTCUT_PANEL_TITLE, {iconClass: "fa-solid fa-keyboard"}); panel.hide(); $shortcutsPanel = $("#shortcuts-panel"); diff --git a/src/extensionsIntegrated/Terminal/TerminalInstance.js b/src/extensionsIntegrated/Terminal/TerminalInstance.js index 90d21e26d..e0afb59b9 100644 --- a/src/extensionsIntegrated/Terminal/TerminalInstance.js +++ b/src/extensionsIntegrated/Terminal/TerminalInstance.js @@ -39,14 +39,6 @@ define(function (require, exports, module) { let _nextId = 0; - // Shortcuts that should be passed to the editor, not the terminal - const EDITOR_SHORTCUTS = [ - {ctrlKey: true, shiftKey: true, key: "p"}, // Command Palette - {ctrlKey: true, key: "p"}, // Quick Open - {ctrlKey: true, key: "b"}, // Toggle sidebar - {ctrlKey: true, key: "Tab"}, // Next tab - {ctrlKey: true, shiftKey: true, key: "Tab"} // Previous tab - ]; /** * Read terminal theme colors from CSS variables @@ -267,16 +259,6 @@ define(function (require, exports, module) { return false; } - for (const shortcut of EDITOR_SHORTCUTS) { - const ctrlMatch = shortcut.ctrlKey ? ctrlOrMeta : !ctrlOrMeta; - const shiftMatch = shortcut.shiftKey ? event.shiftKey : !event.shiftKey; - const keyMatch = event.key.toLowerCase() === shortcut.key.toLowerCase(); - - if (ctrlMatch && shiftMatch && keyMatch) { - return false; // Don't let xterm handle it - } - } - return true; // Let xterm handle it }; diff --git a/src/extensionsIntegrated/Terminal/main.js b/src/extensionsIntegrated/Terminal/main.js index fda71c4aa..b55323403 100644 --- a/src/extensionsIntegrated/Terminal/main.js +++ b/src/extensionsIntegrated/Terminal/main.js @@ -115,7 +115,7 @@ define(function (require, exports, module) { }; $panel = $(Mustache.render(panelHTML, templateVars)); - panel = WorkspaceManager.createBottomPanel(PANEL_ID, $panel, PANEL_MIN_SIZE); + panel = WorkspaceManager.createBottomPanel(PANEL_ID, $panel, PANEL_MIN_SIZE, undefined, {iconClass: "fa-solid fa-terminal"}); // Override focus() so Shift+Escape can transfer focus to the terminal panel.focus = function () { @@ -147,37 +147,7 @@ define(function (require, exports, module) { // Dropdown chevron button toggles shell selector $panel.find(".terminal-flyout-dropdown-btn").on("click", _onDropdownButtonClick); - // When the terminal is focused, prevent Phoenix keybindings from - // stealing keys that should go to the shell (e.g. Ctrl+L for clear). - // The EDITOR_SHORTCUTS list in TerminalInstance.js already defines which - // Ctrl combos should pass through to Phoenix; everything else should - // reach xterm/the PTY. - KeyBindingManager.addGlobalKeydownHook(function (event) { - if (event.type !== "keydown") { - return false; - } - // Only intercept when a terminal textarea is focused - const el = document.activeElement; - if (!el || !$contentArea[0].contains(el)) { - return false; - } - // Let the terminal handle Ctrl/Cmd key combos that aren't - // reserved for the editor (those are handled by TerminalInstance's - // _customKeyHandler which returns false for them). - const ctrlOrMeta = event.ctrlKey || event.metaKey; - const key = event.key.toLowerCase(); - if (ctrlOrMeta && !event.shiftKey && key === "l") { - _showClearBufferHintToast(); - return true; // Block Phoenix, let xterm handle Ctrl+L - } - // Ctrl+K (Cmd+K on mac): clear terminal scrollback - if (ctrlOrMeta && !event.shiftKey && key === "k") { - event.preventDefault(); - _clearActiveTerminal(); - return true; - } - return false; - }); + _setupPhoenixShortcuts(); // Refresh process info when the tab bar gains focus or mouse enters $panel.find(".terminal-tab-bar").on("mouseenter", _refreshAllProcesses); @@ -251,6 +221,22 @@ define(function (require, exports, module) { * Show/hide the shell dropdown */ function _showShellDropdown() { + // Move dropdown out of the flyout and append to the terminal body + // so it isn't clipped by the tab bar's overflow: hidden. + // Position it above the actions row, aligned to the right. + const $body = $panel.find(".terminal-body"); + const $actions = $panel.find(".terminal-flyout-actions"); + const actionsRect = $actions[0].getBoundingClientRect(); + const bodyRect = $body[0].getBoundingClientRect(); + + $shellDropdown.appendTo($body); + $shellDropdown.css({ + position: "absolute", + bottom: (bodyRect.bottom - actionsRect.top) + "px", + right: "0", + left: "auto", + top: "auto" + }); $shellDropdown.removeClass("forced-hidden"); // Close on outside click setTimeout(function () { @@ -642,6 +628,69 @@ define(function (require, exports, module) { _refreshAllProcesses(); } + /** + * Set up keyboard shortcut routing so that when the terminal is focused, + * all keys go to the terminal except shortcuts bound to specific Phoenix + * commands (e.g. toggle terminal, keyboard nav overlay). + */ + function _setupPhoenixShortcuts() { + // Commands whose shortcuts should pass through to Phoenix + // even when the terminal is focused. + const PASSTHROUGH_COMMANDS = [ + Commands.VIEW_TERMINAL, + Commands.CMD_KEYBOARD_NAV_UI_OVERLAY + ]; + + // Build a set of shortcut strings, rebuilt when bindings change. + let passthroughShortcuts = new Set(); + function rebuild() { + passthroughShortcuts = new Set(); + for (const cmdId of PASSTHROUGH_COMMANDS) { + for (const binding of KeyBindingManager.getKeyBindings(cmdId)) { + if (binding.key) { + passthroughShortcuts.add(binding.key); + } + } + } + } + rebuild(); + KeyBindingManager.on(KeyBindingManager.EVENT_KEY_BINDING_ADDED, rebuild); + KeyBindingManager.on(KeyBindingManager.EVENT_KEY_BINDING_REMOVED, rebuild); + + KeyBindingManager.addGlobalKeydownHook(function (event, shortcut) { + if (event.type !== "keydown") { + return false; + } + const el = document.activeElement; + if (!el || !$contentArea[0].contains(el)) { + return false; + } + + const ctrlOrMeta = event.ctrlKey || event.metaKey; + const key = event.key.toLowerCase(); + + // Ctrl+K (Cmd+K on mac): clear terminal scrollback + if (ctrlOrMeta && !event.shiftKey && key === "k") { + event.preventDefault(); + _clearActiveTerminal(); + return true; + } + + // Show clear buffer hint on Ctrl+L + if (ctrlOrMeta && !event.shiftKey && key === "l") { + _showClearBufferHintToast(); + } + + // Let Phoenix handle shortcuts bound to passthrough commands + if (shortcut && passthroughShortcuts.has(shortcut)) { + return false; + } + + // Block Phoenix from handling everything else — let xterm get it + return true; + }); + } + /** * Update all terminal themes (after editor theme change) */ diff --git a/src/language/CodeInspection.js b/src/language/CodeInspection.js index 027cfd1ad..afad847b0 100644 --- a/src/language/CodeInspection.js +++ b/src/language/CodeInspection.js @@ -1240,7 +1240,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, Strings.CMD_VIEW_TOGGLE_PROBLEMS); + problemsPanel = WorkspaceManager.createBottomPanel("errors", $(panelHtml), 100, Strings.CMD_VIEW_TOGGLE_PROBLEMS, {iconClass: "fa-solid fa-triangle-exclamation"}); $problemsPanel = $("#problems-panel"); $fixAllBtn = $problemsPanel.find(".problems-fix-all-btn"); $fixAllBtn.click(()=>{ diff --git a/src/search/SearchResultsView.js b/src/search/SearchResultsView.js index e79a86116..0c8a8c0d1 100644 --- a/src/search/SearchResultsView.js +++ b/src/search/SearchResultsView.js @@ -82,7 +82,7 @@ define(function (require, exports, module) { const self = this; let panelHtml = Mustache.render(searchPanelTemplate, {panelID: panelID}); - this._panel = WorkspaceManager.createBottomPanel(panelName, $(panelHtml), 100, title); + this._panel = WorkspaceManager.createBottomPanel(panelName, $(panelHtml), 100, title, {iconClass: "fa-solid fa-magnifying-glass"}); 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 index b65d181ae..afe16fabf 100644 --- a/src/styles/Extn-BottomPanelTabs.less +++ b/src/styles/Extn-BottomPanelTabs.less @@ -144,12 +144,68 @@ } } +/* Tab icon: hidden by default, shown when tabs are collapsed */ +.bottom-panel-tab-icon { + display: none; + font-size: 1rem; + width: 1rem; + text-align: center; + pointer-events: none; +} + +/* Override any FA class specificity (e.g. fa-brands sets font-size: 1.2em) */ +i.panel-titlebar-icon.panel-titlebar-icon { + font-size: 1rem; +} + +img.panel-titlebar-icon { + width: 1rem; + height: 1rem; + vertical-align: middle; +} + +.default-panel-btn i.panel-titlebar-icon { + font-size: 20px; +} + +.default-panel-btn img.panel-titlebar-icon { + width: 20px; + height: 20px; +} + + .bottom-panel-tab-title { display: inline-flex; align-items: center; pointer-events: none; } +/* Drag-and-drop tab reordering */ +.bottom-panel-tab-dragging { + opacity: 0.5; +} + +/* Collapsed tab bar: show icons, hide titles for tabs that have icons */ +.bottom-panel-tabs-collapsed { + .bottom-panel-tab-icon { + display: inline-flex; + } + .bottom-panel-tab-icon ~ .bottom-panel-tab-title { + display: none; + } + /* Increase spacing in collapsed mode */ + .bottom-panel-tab { + padding: 0 0.6rem 0 0.8rem; + } + .bottom-panel-tab-close-btn { + margin-left: 0.8rem; + } + /* Only show close button on the active tab to prevent accidental clicks */ + .bottom-panel-tab:not(.active) .bottom-panel-tab-close-btn { + visibility: hidden; + } +} + .bottom-panel-tab-close-btn { margin-left: 0.55rem; border-radius: 3px; diff --git a/src/styles/Extn-Terminal.less b/src/styles/Extn-Terminal.less index 3d2a1033b..8743e14ca 100644 --- a/src/styles/Extn-Terminal.less +++ b/src/styles/Extn-Terminal.less @@ -314,11 +314,8 @@ display: inline; } -/* ─── Shell dropdown (pops above the actions row) ─── */ +/* ─── Shell dropdown (positioned on $panel to avoid flyout overflow clip) ─── */ .terminal-shell-dropdown { - position: absolute; - bottom: 100%; - right: 0; min-width: 180px; background: var(--terminal-tab-bg); border: 1px solid var(--terminal-border); diff --git a/src/view/DefaultPanelView.js b/src/view/DefaultPanelView.js index 5dff454db..8bbdfd52b 100644 --- a/src/view/DefaultPanelView.js +++ b/src/view/DefaultPanelView.js @@ -43,17 +43,12 @@ define(function (require, exports, module) { 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", + icon: "fa-brands fa-git-alt", label: Strings.GIT_PANEL_TITLE || "Git", - commandID: Commands.CMD_GIT_TOGGLE_PANEL + commandID: Commands.CMD_GIT_TOGGLE_PANEL, + nativeOnly: true }, { id: "snippets", @@ -104,7 +99,7 @@ define(function (require, exports, module) { .attr("data-command", btn.commandID) .attr("data-btn-id", btn.id) .attr("title", btn.label); - let $icon = $('').addClass(btn.icon); + let $icon = $('').addClass(btn.icon); let $label = $('').text(btn.label); $button.append($icon).append($label); $buttonsRow.append($button); @@ -130,15 +125,16 @@ define(function (require, exports, module) { /** * Show or hide buttons based on current state. - * The Problems button is always shown since the panel now displays - * meaningful content regardless of error state. + * On desktop, Git is always shown. On browser, it depends on availability. * @private */ function _updateButtonVisibility() { if (!_$panel) { return; } - _$panel.find('.default-panel-btn[data-btn-id="git"]').toggle(_isGitAvailable()); + if (!Phoenix.isNativeApp) { + _$panel.find('.default-panel-btn[data-btn-id="git"]').toggle(_isGitAvailable()); + } } /** @@ -165,7 +161,8 @@ define(function (require, exports, module) { WorkspaceManager.DEFAULT_PANEL_ID, _$panel, undefined, - Strings.BOTTOM_PANEL_DEFAULT_TITLE + Strings.BOTTOM_PANEL_DEFAULT_TITLE, + {iconSvg: "styles/images/app-drawer.svg"} ); // Button click handler: execute the command to open the target panel. @@ -177,21 +174,6 @@ define(function (require, exports, module) { } }); - const iconHTML = ''; - - /** - * Inject the app-drawer icon into the Quick Access tab title. - * Called each time the panel is shown because the tab DOM is rebuilt. - */ - function _addTabIcon() { - const $tabTitle = $('#bottom-panel-tab-bar .bottom-panel-tab[data-panel-id="' - + WorkspaceManager.DEFAULT_PANEL_ID + '"] .bottom-panel-tab-title'); - if ($tabTitle.length && !$tabTitle.find(".app-drawer-tab-icon").length) { - $tabTitle.prepend(iconHTML); - } - } - // The app-drawer button is defined in index.html; set its title here. const $drawerBtn = $("#app-drawer-button") .attr("title", Strings.BOTTOM_PANEL_DEFAULT_TITLE); @@ -210,7 +192,6 @@ define(function (require, exports, module) { _panel.hide(); } else { _updateButtonVisibility(); - _addTabIcon(); } $drawerBtn.toggleClass("selected-button", panelID === WorkspaceManager.DEFAULT_PANEL_ID); }); diff --git a/src/view/PanelView.js b/src/view/PanelView.js index 5f07df975..3b3134454 100644 --- a/src/view/PanelView.js +++ b/src/view/PanelView.js @@ -139,10 +139,42 @@ define(function (require, exports, module) { * Call this when tabs are added, removed, or renamed. * @private */ + /** + * Build a tab element for a panel. + * @param {Panel} panel + * @param {boolean} isActive + * @return {jQueryObject} + * @private + */ + function _buildTab(panel, isActive) { + let title = panel._tabTitle || _getPanelTitle(panel.panelID, panel.$panel); + let $tab = $('
') + .toggleClass('active', isActive) + .attr('data-panel-id', panel.panelID); + const opts = panel._options; + if (opts.iconClass) { + $tab.append($('') + .addClass(opts.iconClass)); + } else if (opts.iconSvg) { + $tab.append($('') + .attr("src", opts.iconSvg)); + } else { + // Fallback generic icon for panels without a custom icon + $tab.append($('')); + } + $tab.append($('').text(title)); + $tab.append($('×').attr('title', Strings.CLOSE)); + return $tab; + } + function _updateBottomPanelTabBar() { if (!_$tabsOverflow) { return; } + // Detach the add button before emptying to preserve its event handlers + if (_$addBtn) { + _$addBtn.detach(); + } _$tabsOverflow.empty(); _openIds.forEach(function (panelId) { @@ -150,14 +182,7 @@ define(function (require, exports, module) { 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); + _$tabsOverflow.append(_buildTab(panel, panelId === _activeId)); }); // Re-append the "+" button at the end (after all tabs) @@ -165,6 +190,7 @@ define(function (require, exports, module) { _$tabsOverflow.append(_$addBtn); _updateAddButtonVisibility(); } + _checkTabOverflow(); } /** @@ -199,13 +225,7 @@ define(function (require, exports, module) { 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)); + let $tab = _buildTab(panel, panelId === _activeId); // Insert before the "+" button so it stays at the end if (_$addBtn && _$addBtn.parent().length) { @@ -214,6 +234,7 @@ define(function (require, exports, module) { _$tabsOverflow.append($tab); } _updateAddButtonVisibility(); + _checkTabOverflow(); } /** @@ -228,6 +249,128 @@ define(function (require, exports, module) { } _$tabsOverflow.find('.bottom-panel-tab[data-panel-id="' + panelId + '"]').remove(); _updateAddButtonVisibility(); + _checkTabOverflow(); + } + + /** + * Set up drag-and-drop tab reordering on the bottom panel tab bar. + * Uses a vertical line indicator matching the file tab bar UX. + * @private + */ + function _initDragAndDrop() { + let draggedTab = null; + let $indicator = $('
'); + $("body").append($indicator); + + function getDropPosition(targetTab, mouseX) { + const rect = targetTab.getBoundingClientRect(); + return mouseX < rect.left + rect.width / 2; + } + + function updateIndicator(targetTab, insertBefore) { + if (!targetTab) { + $indicator.hide(); + return; + } + const rect = targetTab.getBoundingClientRect(); + $indicator.css({ + position: "fixed", + left: (insertBefore ? rect.left : rect.right) + "px", + top: rect.top + "px", + height: rect.height + "px", + width: "2px", + zIndex: 10001 + }).show(); + } + + function cleanup() { + if (draggedTab) { + $(draggedTab).removeClass("bottom-panel-tab-dragging"); + } + draggedTab = null; + $indicator.hide(); + _$tabBar.find(".bottom-panel-tab").removeClass("drag-target"); + } + + _$tabBar.on("dragstart", ".bottom-panel-tab", function (e) { + draggedTab = this; + e.originalEvent.dataTransfer.effectAllowed = "move"; + e.originalEvent.dataTransfer.setData("text/plain", "panel-tab"); + $(this).addClass("bottom-panel-tab-dragging"); + }); + + _$tabBar.on("dragend", ".bottom-panel-tab", function () { + setTimeout(cleanup, 50); + }); + + _$tabBar.on("dragover", ".bottom-panel-tab", function (e) { + if (!draggedTab || this === draggedTab) { + return; + } + e.preventDefault(); + e.originalEvent.dataTransfer.dropEffect = "move"; + _$tabBar.find(".bottom-panel-tab").removeClass("drag-target"); + $(this).addClass("drag-target"); + updateIndicator(this, getDropPosition(this, e.originalEvent.clientX)); + }); + + _$tabBar.on("dragleave", ".bottom-panel-tab", function (e) { + const related = e.originalEvent.relatedTarget; + if (!$(this).is(related) && !$(this).has(related).length) { + $(this).removeClass("drag-target"); + } + }); + + _$tabBar.on("drop", ".bottom-panel-tab", function (e) { + e.preventDefault(); + e.stopPropagation(); + if (!draggedTab || this === draggedTab) { + cleanup(); + return; + } + let draggedId = $(draggedTab).data("panel-id"); + let targetId = $(this).data("panel-id"); + let fromIdx = _openIds.indexOf(draggedId); + let toIdx = _openIds.indexOf(targetId); + if (fromIdx === -1 || toIdx === -1) { + cleanup(); + return; + } + const insertBefore = getDropPosition(this, e.originalEvent.clientX); + _openIds.splice(fromIdx, 1); + let newIdx = _openIds.indexOf(targetId); + if (!insertBefore) { + newIdx++; + } + _openIds.splice(newIdx, 0, draggedId); + cleanup(); + _updateBottomPanelTabBar(); + _updateActiveTabHighlight(); + }); + } + + /** + * Check if the tab bar is overflowing and collapse tabs to icons if so. + * Only collapses tabs that have an icon available. + * @private + */ + function _checkTabOverflow() { + if (!_$tabBar) { + return; + } + // Remove collapsed state first to measure true width + _$tabBar.removeClass("bottom-panel-tabs-collapsed"); + const isOverflowing = _$tabsOverflow[0].scrollWidth > _$tabsOverflow[0].clientWidth; + _$tabBar.toggleClass("bottom-panel-tabs-collapsed", isOverflowing); + // Show tooltip on hover only in collapsed mode (title text is hidden) + _$tabBar.find(".bottom-panel-tab").each(function () { + const $tab = $(this); + if (isOverflowing) { + $tab.attr("title", $tab.find(".bottom-panel-tab-title").text()); + } else { + $tab.removeAttr("title"); + } + }); } /** @@ -281,10 +424,19 @@ define(function (require, exports, module) { * @param {string} id Unique panel identifier. * @param {string=} title Optional display title for the tab bar. */ - function Panel($panel, id, title) { + /** + * @param {jQueryObject} $panel + * @param {string} id + * @param {string=} title + * @param {Object=} options + * @param {string=} options.iconClass FontAwesome class string (e.g. "fa-solid fa-terminal"). + * @param {string=} options.iconSvg Path to an SVG icon (e.g. "styles/images/icon.svg"). + */ + function Panel($panel, id, title, options) { this.$panel = $panel; this.panelID = id; this._tabTitle = _getPanelTitle(id, $panel, title); + this._options = options || {}; _panelMap[id] = this; } @@ -542,6 +694,8 @@ define(function (require, exports, module) { } }); + _initDragAndDrop(); + // "+" button opens the default/quick-access panel _$addBtn.on("click", function (e) { e.stopPropagation(); @@ -573,6 +727,10 @@ define(function (require, exports, module) { _toggleMaximize(); }); + // Re-check tab overflow when the tab bar resizes (e.g. window resize) + const tabBarResizeObserver = new ResizeObserver(_checkTabOverflow); + tabBarResizeObserver.observe(_$tabsOverflow[0]); + // Restore maximize state from preferences (survives reload). _isMaximized = PreferencesManager.getViewState(PREF_BOTTOM_PANEL_MAXIMIZED) === true; diff --git a/src/view/WorkspaceManager.js b/src/view/WorkspaceManager.js index e7babe09a..02394d80e 100644 --- a/src/view/WorkspaceManager.js +++ b/src/view/WorkspaceManager.js @@ -265,13 +265,16 @@ define(function (require, exports, module) { * attribute, for use as a preferences key. * @param {number=} minSize @deprecated No longer used. Pass `undefined`. * @param {string=} title Display title shown in the bottom panel tab bar. + * @param {Object=} options Optional settings: + * - {string} iconClass FontAwesome class string (e.g. "fa-solid fa-terminal"). + * - {string} iconSvg Path to an SVG icon (e.g. "styles/images/icon.svg"). * @return {!Panel} */ - function createBottomPanel(id, $panel, minSize, title) { + function createBottomPanel(id, $panel, minSize, title, options) { $bottomPanelContainer.append($panel); $panel.hide(); updateResizeLimits(); - let bottomPanel = new PanelView.Panel($panel, id, title); + let bottomPanel = new PanelView.Panel($panel, id, title, options); panelIDMap[id] = bottomPanel; return bottomPanel; }