diff --git a/src-mdviewer/src/bridge.js b/src-mdviewer/src/bridge.js index a998a76ea0..f1c80d56ee 100644 --- a/src-mdviewer/src/bridge.js +++ b/src-mdviewer/src/bridge.js @@ -20,6 +20,35 @@ let _baseURL = ""; let _cursorPosBeforeEdit = null; // cursor position before current edit batch let _cursorPosDirty = false; // true after content changes, reset when emitted let _pendingReloadScroll = null; // { filePath, scrollSourceLine } for scroll restore after reload +// Key strings ("Ctrl-Shift-O", "Ctrl-Shift-F", …) for shortcuts Phoenix +// handles by opening a UI that needs focus — we must not auto-refocus our +// own editor after forwarding those. Populated from the parent via the +// MDVIEWR_SKIP_REFOCUS_KEYS message; starts empty so bugs in the parent +// can't break the default refocus behavior. +let _skipRefocusKeys = new Set(); +const _isMacForKey = /Mac|iPhone|iPad/.test(navigator.platform); + +/** + * Mirror Phoenix's KeyBindingManager _buildKeyDescriptor to produce the + * same canonical key string from a KeyboardEvent so we can look it up in + * _skipRefocusKeys. Non-mac: "Ctrl-Shift-O"; mac: "Shift-Cmd-O". + */ +function _eventToKeyString(e) { + const parts = []; + if (_isMacForKey && e.ctrlKey) { parts.push("Ctrl"); } + if (e.altKey) { parts.push("Alt"); } + if (e.shiftKey) { parts.push("Shift"); } + if (!_isMacForKey && e.ctrlKey) { parts.unshift("Ctrl"); } + if (_isMacForKey && e.metaKey) { parts.push("Cmd"); } + let key = e.key || ""; + if (key.length === 1) { key = key.toUpperCase(); } + parts.push(key); + return parts.join("-"); +} + +function _isSkipRefocusShortcut(e) { + return _skipRefocusKeys.has(_eventToKeyString(e)); +} /** * Check if a URL is absolute (not relative to the document). @@ -317,6 +346,14 @@ export function initBridge() { window.getSelection().removeAllRanges(); document.body.click(); break; + case "MDVIEWR_SKIP_REFOCUS_KEYS": + // Phoenix tells us which forwarded shortcuts should NOT + // trigger our auto-refocus (e.g. Quick Open, Find in Files + // — shortcuts that open parent-side UI which needs focus). + if (Array.isArray(data.keys)) { + _skipRefocusKeys = new Set(data.keys); + } + break; } }); @@ -416,13 +453,18 @@ export function initBridge() { altKey: e.altKey }); // Refocus md editor after Phoenix handles the shortcut - // (some commands like Save focus the CM editor) - setTimeout(() => { - const content = document.getElementById("viewer-content"); - if (content && getState().editMode) { - content.focus({ preventScroll: true }); - } - }, 100); + // (e.g. Save focuses the CM editor). Phoenix sends us the + // list of shortcuts for which focus should stay with the + // parent (Quick Open, Find in Files …) via + // MDVIEWR_SKIP_REFOCUS_KEYS. Don't refocus for those. + if (!_isSkipRefocusShortcut(e)) { + setTimeout(() => { + const content = document.getElementById("viewer-content"); + if (content && getState().editMode) { + content.focus({ preventScroll: true }); + } + }, 100); + } } } }, true); diff --git a/src/base-config/keyboard.json b/src/base-config/keyboard.json index 488573c9a4..5f9f685721 100644 --- a/src/base-config/keyboard.json +++ b/src/base-config/keyboard.json @@ -23,6 +23,9 @@ "file.liveFilePreview": [ "Ctrl-Alt-L" ], + "view.toggleDesignMode": [ + "Ctrl-F11" + ], "file.reloadLivePreview": [ { "key": "Ctrl-Shift-R" diff --git a/src/brackets.js b/src/brackets.js index 3a3e08c739..4c53bb9c83 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -127,6 +127,7 @@ define(function (require, exports, module) { require("file/FileUtils"); require("project/SidebarView"); require("view/SidebarTabs"); + require("view/CentralControlBar"); require("utils/Resizer"); require("LiveDevelopment/main"); require("utils/NodeConnection"); diff --git a/src/command/Commands.js b/src/command/Commands.js index 30403d1cbe..caa7c4bd14 100644 --- a/src/command/Commands.js +++ b/src/command/Commands.js @@ -257,6 +257,9 @@ define(function (require, exports, module) { /** Toggles sidebar visibility */ exports.VIEW_HIDE_SIDEBAR = "view.toggleSidebar"; // SidebarView.js toggle() + /** Toggles the design (full live-preview) mode — collapses/expands the editor */ + exports.VIEW_TOGGLE_DESIGN_MODE = "view.toggleDesignMode"; // view/CentralControlBar.js _setEditorCollapsed() + /** Toggles tabbar visibility */ exports.TOGGLE_TABBAR = "view.toggleTabbar"; // extensionsIntegrated/TabBar/main.js diff --git a/src/command/Menus.js b/src/command/Menus.js index 18fb38ea21..c9a21f8a7c 100644 --- a/src/command/Menus.js +++ b/src/command/Menus.js @@ -1778,35 +1778,6 @@ define(function (require, exports, module) { const $hamburgerToggle = $hamburger.find(".hamburger-toggle"); let _activeSubmenuId = null; - // Sidebar collapse/expand toggle button before the File menu - const $sidebarToggle = $(``); - $menubar.prepend($sidebarToggle); - const $sidebarIcon = $sidebarToggle.find("a"); - - function _updateSidebarToggleIcon() { - const isVisible = $("#sidebar").is(":visible"); - if (isVisible) { - $sidebarIcon.html(''); - $sidebarIcon.attr("title", Strings.CMD_HIDE_SIDEBAR); - } else { - $sidebarIcon.html(''); - $sidebarIcon.attr("title", Strings.CMD_SHOW_SIDEBAR); - } - } - - $sidebarIcon.on("click", function (e) { - e.preventDefault(); - e.stopPropagation(); - CommandManager.execute(Commands.VIEW_HIDE_SIDEBAR); - }); - - $("#sidebar").on("panelCollapsed panelExpanded", _updateSidebarToggleIcon); - _updateSidebarToggleIcon(); - function _resetMenuItemStyles($menuItem) { const menu = menuMap[$menuItem.attr("id")]; if (menu) { diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index 2c35042527..7292015592 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -1953,6 +1953,9 @@ define(function (require, exports, module) { function handleShowInTree() { let activeFile = MainViewManager.getCurrentlyViewedFile(MainViewManager.ACTIVE_PANE); if(activeFile){ + if (!$("#sidebar").is(":visible")) { + CommandManager.execute(Commands.VIEW_HIDE_SIDEBAR); + } SidebarTabs.setActiveTab(SidebarTabs.SIDEBAR_TAB_FILES); ProjectManager.showInTree(activeFile); } diff --git a/src/extensions/default/Git/src/Main.js b/src/extensions/default/Git/src/Main.js index 89f94df33a..49a9723912 100644 --- a/src/extensions/default/Git/src/Main.js +++ b/src/extensions/default/Git/src/Main.js @@ -49,7 +49,20 @@ define(function (require, exports) { Branch.init(); CloseNotModified.init(); // Attach events - $icon.on("click", Panel.toggle); + $icon.on("click", function () { + // Design mode collapses the editor and stretches live preview, + // which leaves no room for the git bottom panel. Exit design mode + // first so the panel has somewhere to render. + // TODO: make git panel float/overlay live preview so users can + // peek at git status without leaving design mode. + const WorkspaceManager = brackets.getModule("view/WorkspaceManager"); + const CommandManager = brackets.getModule("command/CommandManager"); + const Commands = brackets.getModule("command/Commands"); + if (WorkspaceManager.isInDesignMode()) { + CommandManager.execute(Commands.VIEW_TOGGLE_DESIGN_MODE); + } + Panel.toggle(); + }); } function _addRemoveItemInGitignore(selectedEntry, method) { diff --git a/src/extensionsIntegrated/NavigationAndHistory/NavigationProvider.js b/src/extensionsIntegrated/NavigationAndHistory/NavigationProvider.js index 304dfe6795..ba12210795 100644 --- a/src/extensionsIntegrated/NavigationAndHistory/NavigationProvider.js +++ b/src/extensionsIntegrated/NavigationAndHistory/NavigationProvider.js @@ -745,12 +745,11 @@ define(function (require, exports, module) { } function _setupNavigationButtons() { - let $mainNavBarRight = $("#mainNavBarRight"); let $mainNavBarLeft = $("#mainNavBarLeft"); - $showInTree = $mainNavBarRight.find("#showInfileTree"); - $navback = $mainNavBarRight.find("#navBackButton"); - $navForward = $mainNavBarRight.find("#navForwardButton"); - $searchNav = $mainNavBarRight.find("#searchNav"); + $showInTree = $("#showInfileTree"); + $navback = $("#navBackButton"); + $navForward = $("#navForwardButton"); + $searchNav = $("#searchNav"); $newProject = $mainNavBarLeft.find("#newProject"); updateTooltips(); diff --git a/src/extensionsIntegrated/NoDistractions/main.js b/src/extensionsIntegrated/NoDistractions/main.js index 55f8d27a94..4337b689dc 100644 --- a/src/extensionsIntegrated/NoDistractions/main.js +++ b/src/extensionsIntegrated/NoDistractions/main.js @@ -168,14 +168,27 @@ define(function (require, exports, module) { KeyBindingManager.addBinding(CMD_TOGGLE_PANELS, [ {key: togglePanelsKey_EN}, {key: togglePanelsKeyMac_EN, platform: "mac"} ]); PreferencesManager.on("change", PREFS_PURE_CODE, function () { + const inDesignMode = WorkspaceManager.isInDesignMode && + WorkspaceManager.isInDesignMode(); if (PreferencesManager.get(PREFS_PURE_CODE)) { - ViewUtils.hideMainToolBar(); - CommandManager.execute(Commands.HIDE_SIDEBAR); - _hidePanelsIfRequired(); + if (inDesignMode) { + // In design mode the live preview already fills the editor + // area and the main toolbar is the visible surface; just + // collapse the sidebar so LP can stretch to the full width. + CommandManager.execute(Commands.HIDE_SIDEBAR); + } else { + ViewUtils.hideMainToolBar(); + CommandManager.execute(Commands.HIDE_SIDEBAR); + _hidePanelsIfRequired(); + } } else { - ViewUtils.showMainToolBar(); - CommandManager.execute(Commands.SHOW_SIDEBAR); - _showPanelsIfRequired(); + if (inDesignMode) { + CommandManager.execute(Commands.SHOW_SIDEBAR); + } else { + ViewUtils.showMainToolBar(); + CommandManager.execute(Commands.SHOW_SIDEBAR); + _showPanelsIfRequired(); + } } _updateCheckedState(); }); diff --git a/src/extensionsIntegrated/Phoenix-live-preview/MarkdownSync.js b/src/extensionsIntegrated/Phoenix-live-preview/MarkdownSync.js index 4bfdeee5f7..e899bc281d 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/MarkdownSync.js +++ b/src/extensionsIntegrated/Phoenix-live-preview/MarkdownSync.js @@ -25,8 +25,23 @@ define(function (require, exports, module) { const ThemeManager = require("view/ThemeManager"), NativeApp = require("utils/NativeApp"), EditorManager = require("editor/EditorManager"), + AppInit = require("utils/AppInit"), + CommandManager = require("command/CommandManager"), + Commands = require("command/Commands"), + KeyBindingManager = require("command/KeyBindingManager"), utils = require("./utils"); + // Commands whose shortcuts, when forwarded from the md viewer iframe, + // open a parent-side UI that needs to keep keyboard focus. The iframe's + // 100ms auto-refocus must skip these shortcuts — otherwise it yanks + // focus out of the picker immediately after it opens. We send the raw + // key strings (resolved at runtime from KeyBindingManager) so the + // iframe stays theme/rebind-agnostic. + const SKIP_REFOCUS_COMMANDS = [ + Commands.NAVIGATE_QUICK_OPEN, + Commands.CMD_FIND_IN_FILES + ]; + let _active = false; let _doc = null; let _$iframe = null; @@ -348,16 +363,52 @@ define(function (require, exports, module) { // --- iframe ready --- + /** + * Collect the current `key` strings bound to each SKIP_REFOCUS_COMMANDS + * command and send them to the iframe so its keyboard-shortcut forward + * path can skip its own 100ms auto-refocus for these shortcuts. + */ + function _sendSkipRefocusShortcuts() { + const iframeWindow = _getIframeWindow(); + if (!iframeWindow) { return; } + const keys = []; + SKIP_REFOCUS_COMMANDS.forEach(function (cmdID) { + const bindings = KeyBindingManager.getKeyBindings(cmdID) || []; + bindings.forEach(function (b) { + if (b && b.key) { keys.push(b.key); } + }); + }); + iframeWindow.postMessage({ + type: "MDVIEWR_SKIP_REFOCUS_KEYS", + keys: keys + }, "*"); + } + function _onIframeReady() { _iframeReady = true; _sendContent(); _sendTheme(); _sendLocale(); + _sendSkipRefocusShortcuts(); if (_onIframeReadyCallback) { _onIframeReadyCallback(); } } + // Re-send shortcuts if the user rebinds Quick Open / Find in Files. + // Deferred to appReady so the commands have been registered before we + // try to hook their key-binding-change events. + AppInit.appReady(function () { + SKIP_REFOCUS_COMMANDS.forEach(function (cmdID) { + const cmd = CommandManager.get(cmdID); + if (cmd && cmd.on) { + cmd.on(KeyBindingManager.EVENT_KEY_BINDING_ADDED, function () { + if (_iframeReady) { _sendSkipRefocusShortcuts(); } + }); + } + }); + }); + // --- Phoenix → iframe --- /** diff --git a/src/extensionsIntegrated/Phoenix-live-preview/main.js b/src/extensionsIntegrated/Phoenix-live-preview/main.js index 3129acb45e..b08e322798 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/main.js +++ b/src/extensionsIntegrated/Phoenix-live-preview/main.js @@ -1451,6 +1451,14 @@ define(function (require, exports, module) { Menus.AFTER, Commands.FILE_LIVE_FILE_PREVIEW); fileMenu.addMenuItem(Commands.FILE_LIVE_FILE_PREVIEW_SETTINGS, "", Menus.AFTER, Commands.CMD_RELOAD_LIVE_PREVIEW); + // Design-mode toggle is registered by view/CentralControlBar at module load + // so the command exists by now. Insert it last with `AFTER Live Preview` so + // it lands directly below the Live Preview item (above Reload / Settings + // which were inserted earlier into the same AFTER slot). + if (CommandManager.get(Commands.VIEW_TOGGLE_DESIGN_MODE)) { + fileMenu.addMenuItem(Commands.VIEW_TOGGLE_DESIGN_MODE, "", + Menus.AFTER, Commands.FILE_LIVE_FILE_PREVIEW); + } fileMenu.addMenuDivider(Menus.BEFORE, Commands.FILE_LIVE_FILE_PREVIEW); _registerHandlers(); diff --git a/src/extensionsIntegrated/Phoenix-live-preview/utils.js b/src/extensionsIntegrated/Phoenix-live-preview/utils.js index 0697bac805..db7c4c45a1 100644 --- a/src/extensionsIntegrated/Phoenix-live-preview/utils.js +++ b/src/extensionsIntegrated/Phoenix-live-preview/utils.js @@ -39,60 +39,10 @@ define(function (require, exports, module) { const LIVE_PREVIEW_IFRAME_ID = "panel-live-preview-frame"; const MDVIEWR_IFRAME_ID = "panel-md-preview-frame"; const EditorManager = require("editor/EditorManager"); - function getExtension(filePath) { - filePath = filePath || ''; - let pathSplit = filePath.split('.'); - return pathSplit && pathSplit.length>1 ? pathSplit[pathSplit.length-1] : ''; - } - - function isPreviewableFile(filePath) { - // only svg images should appear in the live preview as it needs text editor. - // All other image types should appear in the image previewer - return isSVG(filePath) || isMarkdownFile(filePath) || isHTMLFile(filePath) || isPDF(filePath); - } - - function isPDF(filePath) { - let extension = getExtension(filePath); - return extension === "pdf"; - } - - function isSVG(filePath) { - let extension = getExtension(filePath); - return extension === "svg"; - } - - function isImage(filePath) { - let extension = getExtension(filePath); - return ["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp", "ico", "avif"] - .includes(extension.toLowerCase()); - } - - function isMarkdownFile(filePath) { - let extension = getExtension(filePath); - return ['md', 'markdown', 'mdx'].includes(extension.toLowerCase()); - } - - function isHTMLFile(filePath) { - let extension = getExtension(filePath); - return ['html', 'htm', 'xhtml'].includes(extension.toLowerCase()); - } - - function isServerRenderedFile(filePath) { - let extension = getExtension(filePath); - return [ - "shtml", - "asp", - "aspx", - "php", - "jsp", - "jspx", - "cfm", - "cfc", // ColdFusion Component - "rb", // Ruby file, used in Ruby on Rails for views with ERB - "erb", // Embedded Ruby, used in Ruby on Rails views - "py" // Python file, used in web frameworks like Django or Flask for views - ].includes(extension.toLowerCase()); - } + // File-type classification helpers live in a shared utility module so + // non-live-preview code (e.g. Quick Open in design mode) can use them + // without depending on this extension. + const FileTypeUtils = require("utils/FileTypeUtils"); function focusActiveEditorIfFocusInLivePreview() { const editor = EditorManager.getActiveEditor(); @@ -105,15 +55,14 @@ define(function (require, exports, module) { } } - exports.getExtension = getExtension; - exports.isPreviewableFile = isPreviewableFile; - exports.isImage = isImage; - exports.isPDF = isPDF; - exports.isSVG = isSVG; - exports.isHTMLFile = isHTMLFile; - exports.isServerRenderedFile = isServerRenderedFile; - exports.isMarkdownFile = isMarkdownFile; + // Re-export the shared helpers so existing callers keep working. + exports.getExtension = FileTypeUtils.getExtension; + exports.isPreviewableFile = FileTypeUtils.isPreviewableFile; + exports.isImage = FileTypeUtils.isImage; + exports.isPDF = FileTypeUtils.isPDF; + exports.isSVG = FileTypeUtils.isSVG; + exports.isHTMLFile = FileTypeUtils.isHTMLFile; + exports.isServerRenderedFile = FileTypeUtils.isServerRenderedFile; + exports.isMarkdownFile = FileTypeUtils.isMarkdownFile; exports.focusActiveEditorIfFocusInLivePreview = focusActiveEditorIfFocusInLivePreview; }); - - diff --git a/src/index.html b/src/index.html index ac7e396d5b..547b96dfdc 100644 --- a/src/index.html +++ b/src/index.html @@ -905,7 +905,7 @@
- + +
+ +
+ +
+
+ + + +
+
+
+ + + +
+
+
+
+ + +
+
+
+