[settings] IJPL-245458 IJPL-245379 Non-modal settings: fix window disappearing when pinned from macOS full-screen

When the Settings window (unpinned/window mode) was in macOS native full-screen
and the user pressed Pin, the new FloatDialog was created while macOS was still
running the Space-exit animation, making it invisible until a Cmd+Tab round-trip.

Fix: switchWindowMode now detects native full-screen via MacFullScreenSupport and
defers doSwitchWindowMode until windowExitedFullScreen fires. MacFullScreenSupport
gains exitFullScreen(window, onExited) that triggers requestToggleFullScreen and
routes the completion callback through the primary FullScreenListener (the only one
that JBR reliably delivers events to)


(cherry picked from commit dbb0b15c6817d66a07560897457038d02378addc)

IJ-CR-217470

GitOrigin-RevId: f5112ce355632d272129f107a68061f08acf6520
This commit is contained in:
Aleksandra Olemskaia
2026-08-07 13:34:32 +00:00
committed by intellij-monorepo-bot
parent afe2923d9c
commit 6c2d42e871
2 changed files with 56 additions and 4 deletions
@@ -33,6 +33,7 @@ import com.intellij.ui.ComponentUtil
import com.intellij.ui.FullScreenSupport
import com.intellij.ui.ScreenUtil
import com.intellij.ui.ToolbarService
import com.intellij.ui.mac.MacFullScreenSupport
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.launchOnShow
@@ -119,6 +120,8 @@ abstract class NonModalWindowWrapper(
private lateinit var minWindowSize: Dimension
private var windowListener: WindowAdapter? = null
private var windowDisposable: Disposable? = null
/** Non-null only on macOS while [activeWindow] is a [WindowFrame]. Tracks native full-screen state. */
private var fullScreenSupport: FullScreenSupport? = null
protected var isFloat: Boolean
get() = PropertiesComponent.getInstance().getBoolean(floatModeKey, true)
@@ -303,10 +306,12 @@ abstract class NonModalWindowWrapper(
frame.accessibleContext.accessibleName = getAccessibleWindowName()
val wd = Disposer.newDisposable(frameDisposable)
windowDisposable = wd
val fss = FullScreenSupport.NEW.apply("com.intellij.ui.mac.MacFullScreenSupport")
fullScreenSupport = fss
ToolbarService.getInstance().setTransparentTitleBar(
window = frame,
rootPane = frame.rootPane,
handlerProvider = { FullScreenSupport.NEW.apply("com.intellij.ui.mac.MacFullScreenSupport") },
handlerProvider = { fss },
onDispose = { runnable -> Disposer.register(wd) { runnable.run() } },
)
}
@@ -381,24 +386,47 @@ abstract class NonModalWindowWrapper(
/**
* Switches between Float ([JDialog]) and Window ([JFrame]) mode.
* The [content] component is reparented and window bounds are transferred.
*
* On macOS, if the [WindowFrame] is in native full-screen when the user pins the window
* (toFloat=true, i.e. switching FROM WindowFrame TO FloatDialog), we first exit full-screen
* via [MacFullScreenSupport.exitFullScreen] and defer the actual switch until the OS animation
* completes. This ensures the new [FloatDialog] is created after macOS has restored the
* pre-full-screen window bounds. On non-macOS, [fullScreenSupport] is always null.
*/
private fun switchWindowMode(toFloat: Boolean) {
val bounds = activeWindow.bounds
val macFss = if (toFloat) fullScreenSupport as? MacFullScreenSupport else null
if (macFss != null && macFss.isFullScreen) {
val ideFrame = getIdeJFrame()
macFss.exitFullScreen(activeWindow) {
if (!isDisposed) doSwitchWindowMode(toFloat, ideFrame)
}
return
}
doSwitchWindowMode(toFloat)
}
private fun doSwitchWindowMode(toFloat: Boolean, ideFrameToActivate: JFrame? = null) {
val oldBounds = activeWindow.bounds
val wasVisible = activeWindow.isVisible
val savedDefaultButton = (activeWindow as RootPaneContainer).rootPane.defaultButton
windowListener?.let { activeWindow.removeWindowListener(it) }
windowListener = null
content.parent?.remove(content)
fullScreenSupport = null
disposeWindow(activeWindow)
windowDisposable?.let { Disposer.dispose(it) }
windowDisposable = null
activeWindow = createAwtWindow(toFloat, content, minWindowSize, bounds.size)
activeWindow = createAwtWindow(toFloat, content, minWindowSize, oldBounds.size)
fitWindowToScreen(activeWindow)
installWindowListeners()
savedDefaultButton?.let { (activeWindow as RootPaneContainer).rootPane.defaultButton = it }
activeWindow.bounds = bounds
activeWindow.bounds = oldBounds
dimensionKey?.let { WindowStateService.getInstance(project).getState(it, activeWindow) }
if (wasVisible) {
// When switching back from full-screen, first bring the IDE frame to front so macOS
// navigates to the IDE Space, then show the dialog on top of it.
ideFrameToActivate?.toFront()
activeWindow.isVisible = true
activeWindow.toFront()
activeWindow.requestFocus()
@@ -1,14 +1,17 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ui.mac;
import com.apple.eawt.Application;
import com.apple.eawt.FullScreenListener;
import com.apple.eawt.FullScreenUtilities;
import com.apple.eawt.event.FullScreenEvent;
import com.intellij.ui.FullScreenSupport;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.RootPaneContainer;
import javax.swing.SwingUtilities;
import java.awt.Window;
/**
@@ -22,6 +25,7 @@ import java.awt.Window;
public final class MacFullScreenSupport implements FullScreenSupport {
private FullScreenListener myListener;
private boolean myIsFullScreen;
private @Nullable Runnable myOnExitedCallback;
@Override
public boolean isFullScreen() {
@@ -49,11 +53,31 @@ public final class MacFullScreenSupport implements FullScreenSupport {
@Override
public void windowExitedFullScreen(FullScreenEvent event) {
myIsFullScreen = false;
Runnable callback = myOnExitedCallback;
myOnExitedCallback = null;
if (callback != null) {
SwingUtilities.invokeLater(callback);
}
}
};
FullScreenUtilities.addFullScreenListenerTo(window, myListener);
}
/**
* Programmatically exits macOS native full-screen and invokes {@code onExited} once the OS
* animation completes (after {@code windowExitedFullScreen}). If the window is not in
* full-screen, {@code onExited} is called immediately on the calling thread.
* Not part of {@link FullScreenSupport} — this is a macOS-specific detail.
*/
public void exitFullScreen(@NotNull Window window, @NotNull Runnable onExited) {
if (!myIsFullScreen) {
onExited.run();
return;
}
myOnExitedCallback = onExited;
Application.getApplication().requestToggleFullScreen(window);
}
@Override
public void removeListener(@NotNull Window window) {
if (window instanceof RootPaneContainer container && container.getRootPane() != null) {