diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index 21e14e4eb5..fa50fdfeff 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -347,19 +347,24 @@ define(function (require, exports, module) { _$dirtydot.css("visibility", "hidden"); } - // Set _$titleWrapper to a fixed width just large enough to accommodate _$title. This seems equivalent to what - // the browser would do automatically, but the CSS trick we use for layout requires _$titleWrapper to have a - // fixed width set on it (see the "#titlebar" CSS rule for details). - _$titleWrapper.css("width", ""); - var newWidth = _$title.width(); - _$titleWrapper.css("width", newWidth); - - // Changing the width of the title may cause the toolbar layout to change height, which needs to resize the - // editor beneath it (toolbar changing height due to window resize is already caught by EditorManager). - var newToolbarHeight = _$titleContainerToolbar.height(); - if (_lastToolbarHeight !== newToolbarHeight) { - _lastToolbarHeight = newToolbarHeight; - WorkspaceManager.recomputeLayout(); + if (!Phoenix.isNativeApp) { + // In the native app the title-wrapper is display:none (the OS window titlebar + // shows the file name), so none of this layout work applies there. + + // Set _$titleWrapper to a fixed width just large enough to accommodate _$title. This seems equivalent + // to what the browser would do automatically, but the CSS trick we use for layout requires + // _$titleWrapper to have a fixed width set on it (see the "#titlebar" CSS rule for details). + _$titleWrapper.css("width", ""); + const newWidth = _$title.width(); + _$titleWrapper.css("width", newWidth); + + // Changing the width of the title may cause the toolbar layout to change height, which needs to resize + // the editor beneath it (toolbar changing height due to window resize is already caught by EditorManager). + const newToolbarHeight = _$titleContainerToolbar.height(); + if (_lastToolbarHeight !== newToolbarHeight) { + _lastToolbarHeight = newToolbarHeight; + WorkspaceManager.recomputeLayout(); + } } diff --git a/src/extensionsIntegrated/Terminal/main.js b/src/extensionsIntegrated/Terminal/main.js index 29e1649733..40cdfd9cef 100644 --- a/src/extensionsIntegrated/Terminal/main.js +++ b/src/extensionsIntegrated/Terminal/main.js @@ -885,8 +885,15 @@ define(function (require, exports, module) { id: "terminal-toolbar-button", href: "#", title: Strings.CMD_VIEW_TERMINAL - }) - .insertBefore("#app-drawer-button"); + }); + // The profile button (added externally by phoenix-pro) must stay at the + // very bottom of the toolbar, and it may or may not exist yet at appReady. + const $profileBtn = $("#user-profile-button"); + if ($profileBtn.length) { + $btn.insertBefore($profileBtn); + } else { + $btn.appendTo($("#main-toolbar .bottom-buttons")); + } $btn.on("click", function () { if (WorkspaceManager.isInDesignMode()) { diff --git a/src/index.html b/src/index.html index 2783f0cdbb..6c20b09f9a 100644 --- a/src/index.html +++ b/src/index.html @@ -1045,7 +1045,11 @@
- + +
diff --git a/src/styles/brackets_patterns_override.less b/src/styles/brackets_patterns_override.less index fdec07a22b..bc24e0eb12 100644 --- a/src/styles/brackets_patterns_override.less +++ b/src/styles/brackets_patterns_override.less @@ -190,8 +190,8 @@ a:focus { } body.tauri & { - .title, .dirty-dot { - // In tauri, the window title bar shows the file name fully. so this isn't required. + display: flow-root; + .title-wrapper { display: none; } } diff --git a/src/utils/Resizer.js b/src/utils/Resizer.js index fb22bce770..a9b0d133c4 100644 --- a/src/utils/Resizer.js +++ b/src/utils/Resizer.js @@ -597,7 +597,23 @@ define(function (require, exports, module) { $resizeShield.off("mousedown"); $resizeShield.remove(); animationRequest = null; - toggle($element); + + // The shield covers the whole window, so only treat this press as + // the second click of a double click if it landed on/near the + // resizer; stray clicks elsewhere just dismiss the shield. + const buffer = 8; + const resizerOffset = $resizer.offset(); + let nearResizer; + if (direction === DIRECTION_HORIZONTAL) { + nearResizer = e.pageX >= resizerOffset.left - buffer && + e.pageX <= resizerOffset.left + $resizer.outerWidth() + buffer; + } else { + nearResizer = e.pageY >= resizerOffset.top - buffer && + e.pageY <= resizerOffset.top + $resizer.outerHeight() + buffer; + } + if (nearResizer) { + toggle($element); + } }); } @@ -616,18 +632,25 @@ define(function (require, exports, module) { isResizing = false; - if (resizeStarted) { - $element.trigger(EVENT_PANEL_RESIZE_END, [elementSize]); - } - - // We wait 300ms to remove the resizer container to capture a mousedown - // on the container that would account for double click - window.setTimeout(function () { + function removeShield() { $(window.document).off("mousemove", onMouseMove); $resizeShield.off("mousedown"); $resizeShield.remove(); animationRequest = null; - }, 300); + } + + if (resizeStarted) { + $element.trigger(EVENT_PANEL_RESIZE_END, [elementSize]); + // A double click never includes a drag between its two presses, + // so after a real resize the shield isn't needed to catch a + // second click — remove it right away so it doesn't swallow + // clicks elsewhere in the window. + removeShield(); + } else { + // We wait 300ms to remove the resizer container to capture a mousedown + // on the container that would account for double click + window.setTimeout(removeShield, 300); + } } } diff --git a/test/spec/CentralControlBar-integ-test.js b/test/spec/CentralControlBar-integ-test.js index 3fe377a13d..d1647dbcd0 100644 --- a/test/spec/CentralControlBar-integ-test.js +++ b/test/spec/CentralControlBar-integ-test.js @@ -1035,6 +1035,9 @@ define(function (require, exports, module) { } }); + // app-drawer-button is commented out in index.html for now, + // uncomment these two tests when it comes back + /* it("should exit design mode and open the tools bottom panel when #app-drawer-button is clicked in design mode", async function () { await enterDesignMode(); expect(WorkspaceManager.isInDesignMode()).toBe(true); @@ -1056,6 +1059,7 @@ define(function (require, exports, module) { "tools bottom panel to become visible", 3000); expect(WorkspaceManager.isInDesignMode()).toBe(false); }); + */ it("should exit design mode before mounting Find in Files bar", async function () { await enterDesignMode(); diff --git a/test/spec/DragTestUtils.js b/test/spec/DragTestUtils.js index c3df0a446e..45b219b3b1 100644 --- a/test/spec/DragTestUtils.js +++ b/test/spec/DragTestUtils.js @@ -104,11 +104,13 @@ define(function (require, exports, module) { _fireMouse(doc, "mouseup", endX, endY, testWindow, 0); await _awaitFrames(testWindow, 2); - // Resizer leaves a full-viewport `.resizing-container` shield in the DOM - // for 300ms after mouseup (so a trailing mousedown still registers as a - // double-click). If the next test fires its mousedown before that shield - // is removed, it lands on the shield rather than the handle and the drag - // silently no-ops. Waiting the shield out makes consecutive drags reliable. + // After a real drag Resizer removes its full-viewport `.resizing-container` + // shield immediately on mouseup, but a press+release with no effective + // movement leaves it in the DOM for 300ms (so a trailing mousedown still + // registers as a double-click). If a drag degenerates to no movement and + // the next test fires its mousedown before the shield is removed, it lands + // on the shield rather than the handle and the drag silently no-ops. + // Waiting the window out keeps consecutive drags reliable either way. await new Promise(function (resolve) { testWindow.setTimeout(resolve, 320); }); } diff --git a/test/spec/MainViewManager-integ-test.js b/test/spec/MainViewManager-integ-test.js index 8fee8c4ea1..f428808eb1 100644 --- a/test/spec/MainViewManager-integ-test.js +++ b/test/spec/MainViewManager-integ-test.js @@ -148,6 +148,35 @@ define(function (require, exports, module) { expect(isDisplayed).toBeFalse(); }); + + it("should set a fixed title wrapper width in browser windows", async function () { + if(Phoenix.isNativeApp) { + return; + } + const openPromise = MainViewManager._open(MainViewManager.FIRST_PANE, + FileSystem.getFileForPath(testPath + "/test.js")); + await awaitsForDone(openPromise, "MainViewManager.doOpen"); + + const wrapper = testWindow.$('.title-wrapper')[0]; + await awaitsFor(function () { + return parseFloat(wrapper.style.width) > 0; + }, "title wrapper to get a fixed width", 2000); + }); + + it("should not set a title wrapper width in tauri windows", async function () { + if(!Phoenix.isNativeApp) { + return; + } + const openPromise = MainViewManager._open(MainViewManager.FIRST_PANE, + FileSystem.getFileForPath(testPath + "/test.js")); + await awaitsForDone(openPromise, "MainViewManager.doOpen"); + + // In native apps, the file name is shown in the OS window title bar + // the in-app wrapper for file name stays hidden and _updateTitle skips its layout work... + const $wrapper = testWindow.$('.title-wrapper'); + expect($wrapper.css("display")).toBe("none"); + expect($wrapper[0].style.width).toBe(""); + }); }); describe("opening and closing files", function () { @@ -1153,6 +1182,9 @@ define(function (require, exports, module) { WorkspaceManager.destroyBottomPanel("focusTestPanel"); }); + // app-drawer-button is commented out in index.html for now, + // uncomment this test when it comes back + /* it("should app-drawer-button toggle the default panel", async function () { panel1.hide(); panel2.hide(); @@ -1170,6 +1202,7 @@ define(function (require, exports, module) { _$("#app-drawer-button").click(); expect(defaultPanel.isVisible()).toBeFalse(); }); + */ it("should escape collapse bottom panel regardless of canBeShown", async function () { panel1.show(); @@ -1416,6 +1449,9 @@ define(function (require, exports, module) { }); }); + // app-drawer-button is commented out in index.html for now, + // uncomment this describe when it comes back + /* describe("Quick Access panel (app drawer button)", function () { const DEFAULT_PANEL_ID = "workspace.defaultPanel"; @@ -1485,5 +1521,6 @@ define(function (require, exports, module) { await CommandManager.execute(Commands.VIEW_TOGGLE_PROBLEMS); }); }); + */ }); }); diff --git a/test/spec/SidebarTabs-integ-test.js b/test/spec/SidebarTabs-integ-test.js index 3a13268fb3..b32da624d6 100644 --- a/test/spec/SidebarTabs-integ-test.js +++ b/test/spec/SidebarTabs-integ-test.js @@ -18,7 +18,7 @@ * */ -/*global describe, it, expect, beforeAll, afterAll, beforeEach, awaitsFor, awaitsForDone, jsPromise */ +/*global describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, awaitsFor, awaitsForDone, jsPromise */ define(function (require, exports, module) { @@ -586,5 +586,111 @@ define(function (require, exports, module) { expect(prefs.size).toBe(380); }); }); + + describe("Sidebar resizer double-click and drag shield", function () { + + function fireMouse(target, type, x, y) { + const ev = new testWindow.MouseEvent(type, { + bubbles: true, + cancelable: true, + view: testWindow, + clientX: x, + clientY: y, + button: 0, + buttons: type === "mouseup" ? 0 : 1 + }); + target.dispatchEvent(ev); + } + + function awaitFrames(n) { + return new Promise(function (resolve) { + let remaining = n; + function tick() { + remaining -= 1; + if (remaining <= 0) { + resolve(); + return; + } + testWindow.requestAnimationFrame(tick); + } + testWindow.requestAnimationFrame(tick); + }); + } + + function resizerCenter() { + const rect = _$("#sidebar > .horz-resizer")[0].getBoundingClientRect(); + return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }; + } + + beforeEach(async function () { + SidebarView.show(); + _$(".resizing-container").remove(); + SidebarView.resize(300); + await awaitsFor(function () { + return _$("#sidebar")[0].offsetWidth === 300; + }, "sidebar to settle at baseline 300px", 2000); + }); + + afterEach(function () { + _$(".resizing-container").remove(); + SidebarView.show(); + }); + + it("should collapse the sidebar on resizer double click", function () { + const center = resizerCenter(); + const $resizer = _$("#sidebar > .horz-resizer"); + + fireMouse($resizer[0], "mousedown", center.x, center.y); + fireMouse(testWindow.document, "mouseup", center.x, center.y); + const $shield = _$(".resizing-container"); + expect($shield.length).toBe(1); + + fireMouse($shield[0], "mousedown", center.x, center.y); + expect(SidebarView.isVisible()).toBe(false); + }); + + it("should not collapse the sidebar when the second press lands away from the resizer", function () { + const center = resizerCenter(); + const $resizer = _$("#sidebar > .horz-resizer"); + + fireMouse($resizer[0], "mousedown", center.x, center.y); + fireMouse(testWindow.document, "mouseup", center.x, center.y); + const $shield = _$(".resizing-container"); + expect($shield.length).toBe(1); + + fireMouse($shield[0], "mousedown", center.x + 200, center.y); + expect(SidebarView.isVisible()).toBe(true); + expect(_$(".resizing-container").length).toBe(0); + }); + + it("should remove the shield right after a drag so a quick editor click cannot collapse", async function () { + const center = resizerCenter(); + const $resizer = _$("#sidebar > .horz-resizer"); + + fireMouse($resizer[0], "mousedown", center.x, center.y); + await awaitFrames(1); + for (let i = 1; i <= 5; i++) { + fireMouse(testWindow.document, "mousemove", center.x + i * 10, center.y); + await awaitFrames(1); + } + fireMouse(testWindow.document, "mouseup", center.x + 50, center.y); + await awaitFrames(1); + + const widthAfterDrag = _$("#sidebar")[0].offsetWidth; + expect(widthAfterDrag).toBeGreaterThan(300); + + expect(_$(".resizing-container").length).toBe(0); + + const edRect = _$("#editor-holder")[0].getBoundingClientRect(); + const clickX = edRect.left + edRect.width / 2; + const clickY = edRect.top + edRect.height / 2; + const target = testWindow.document.elementFromPoint(clickX, clickY) || _$("#editor-holder")[0]; + fireMouse(target, "mousedown", clickX, clickY); + fireMouse(target, "mouseup", clickX, clickY); + + expect(SidebarView.isVisible()).toBe(true); + expect(_$("#sidebar")[0].offsetWidth).toBe(widthAfterDrag); + }); + }); }); });