diff --git a/platform/platform-impl/src/com/intellij/jdkEx/JdkEx.java b/platform/platform-impl/src/com/intellij/jdkEx/JdkEx.java index 7f5f61226d19..61eb00d2fcc0 100644 --- a/platform/platform-impl/src/com/intellij/jdkEx/JdkEx.java +++ b/platform/platform-impl/src/com/intellij/jdkEx/JdkEx.java @@ -2,6 +2,7 @@ package com.intellij.jdkEx; import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.registry.Registry; import com.intellij.util.MethodInvocator; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -121,4 +122,46 @@ public final class JdkEx { } } } + + private static MethodInvocator mySetTabbingMode; + + @Nullable + private static MethodInvocator getTabbingModeInvocator() { + if (!SystemInfo.isJetBrainsJvm || !SystemInfo.isMacOSBigSur || !Registry.is("ide.mac.bigsur.window.with.tabs.enabled", true)) { + return null; + } + if (mySetTabbingMode == null) { + try { + mySetTabbingMode = new MethodInvocator(false, Class.forName("java.awt.Window"), "setTabbingMode"); + if (mySetTabbingMode.isAvailable()) { + return mySetTabbingMode; + } + } + catch (ClassNotFoundException ignore) { + } + return null; + } + return mySetTabbingMode.isAvailable() ? mySetTabbingMode : null; + } + + public static boolean isTabbingModeAvailable() { + return getTabbingModeInvocator() != null; + } + + public static boolean setTabbingMode(@NotNull Window window, @Nullable Runnable moveTabToNewWindowCallback) { + MethodInvocator invocator = getTabbingModeInvocator(); + if (invocator != null) { + invocator.invoke(window); + if (moveTabToNewWindowCallback != null) { + try { + new MethodInvocator(false, Class.forName("java.awt.Window"), "setMoveTabToNewWindowCallback", Runnable.class) + .invoke(window, moveTabToNewWindowCallback); + } + catch (ClassNotFoundException ignore) { + } + } + return true; + } + return false; + } } diff --git a/platform/platform-impl/src/com/intellij/openapi/wm/impl/IdeFrameDecorator.java b/platform/platform-impl/src/com/intellij/openapi/wm/impl/IdeFrameDecorator.java index bb4f04475284..cc983c43a9e0 100644 --- a/platform/platform-impl/src/com/intellij/openapi/wm/impl/IdeFrameDecorator.java +++ b/platform/platform-impl/src/com/intellij/openapi/wm/impl/IdeFrameDecorator.java @@ -67,6 +67,14 @@ public abstract class IdeFrameDecorator implements IdeFrameImpl.FrameDecorator { return null; } + @NotNull + public static JComponent wrapRootPaneNorthSide(@NotNull JRootPane rootPane, @NotNull JComponent northComponent) { + if (SystemInfo.isMac) { + return MacMainFrameDecorator._wrapRootPaneNorthSide(rootPane, northComponent); + } + return northComponent; + } + protected void notifyFrameComponents(boolean state) { myFrame.getRootPane().putClientProperty(FULL_SCREEN, state); JMenuBar menuBar = myFrame.getJMenuBar(); diff --git a/platform/platform-impl/src/com/intellij/openapi/wm/impl/IdeRootPane.java b/platform/platform-impl/src/com/intellij/openapi/wm/impl/IdeRootPane.java index eab9c4192c8f..613af67678cb 100644 --- a/platform/platform-impl/src/com/intellij/openapi/wm/impl/IdeRootPane.java +++ b/platform/platform-impl/src/com/intellij/openapi/wm/impl/IdeRootPane.java @@ -74,7 +74,7 @@ public class IdeRootPane extends JRootPane implements UISettingsListener { } } - myContentPane.add(myNorthPanel, BorderLayout.NORTH); + myContentPane.add(IdeFrameDecorator.wrapRootPaneNorthSide(this, myNorthPanel), BorderLayout.NORTH); // listen to mouse motion events for a11y myContentPane.addMouseMotionListener(new MouseMotionAdapter() { diff --git a/platform/platform-impl/src/com/intellij/ui/mac/MacMainFrameDecorator.java b/platform/platform-impl/src/com/intellij/ui/mac/MacMainFrameDecorator.java index 7c50354e2506..aceab755ca06 100644 --- a/platform/platform-impl/src/com/intellij/ui/mac/MacMainFrameDecorator.java +++ b/platform/platform-impl/src/com/intellij/ui/mac/MacMainFrameDecorator.java @@ -3,22 +3,34 @@ package com.intellij.ui.mac; import com.apple.eawt.*; import com.intellij.ide.ActiveWindowsWatcher; +import com.intellij.jdkEx.JdkEx; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.registry.Registry; +import com.intellij.openapi.wm.IdeFrame; +import com.intellij.openapi.wm.WindowManager; import com.intellij.openapi.wm.impl.IdeFrameDecorator; import com.intellij.openapi.wm.impl.IdeRootPane; +import com.intellij.openapi.wm.impl.ProjectFrameHelper; +import com.intellij.ui.components.panels.NonOpaquePanel; +import com.intellij.ui.mac.foundation.Foundation; +import com.intellij.ui.mac.foundation.ID; +import com.intellij.ui.mac.foundation.MacUtil; import com.intellij.util.EventDispatcher; +import com.intellij.util.ui.JBDimension; import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jetbrains.concurrency.AsyncPromise; import org.jetbrains.concurrency.Promise; import org.jetbrains.concurrency.Promises; import javax.swing.*; import java.awt.*; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; import java.lang.reflect.Method; import java.util.EventListener; import java.util.LinkedList; @@ -57,6 +69,8 @@ public final class MacMainFrameDecorator extends IdeFrameDecorator { myInFullScreen = true; storeFullScreenStateIfNeeded(); myFullScreenQueue.runFromQueue(); + + updateTabBar(myFrame, 0); } private void exitFullScreen() { @@ -66,6 +80,8 @@ public final class MacMainFrameDecorator extends IdeFrameDecorator { JRootPane rootPane = myFrame.getRootPane(); if (rootPane != null) rootPane.putClientProperty(FULL_SCREEN, null); myFullScreenQueue.runFromQueue(); + + updateTabBarOnExitFromFullScreen(); } private void storeFullScreenStateIfNeeded() { @@ -104,9 +120,49 @@ public final class MacMainFrameDecorator extends IdeFrameDecorator { private final EventDispatcher myDispatcher = EventDispatcher.create(FSListener.class); private boolean myInFullScreen; + private static final String WIN_TAB_FILLER = "WIN_TAB_FILLER_KEY"; + + private static int DEFAULT_WIN_TAB_HEIGHT() { + return Registry.intValue("ide.mac.bigsur.window.with.tabs.height", 28); + } + + @NotNull + public static JComponent _wrapRootPaneNorthSide(@NotNull JRootPane rootPane, @NotNull JComponent northComponent) { + if (!JdkEx.isTabbingModeAvailable()) { + return northComponent; + } + + NonOpaquePanel panel = new NonOpaquePanel(new BorderLayout()); + + NonOpaquePanel filler = new NonOpaquePanel(); + filler.setVisible(false); + + panel.add(filler, BorderLayout.NORTH); + panel.add(northComponent); + rootPane.putClientProperty(WIN_TAB_FILLER, filler); + return panel; + } + public MacMainFrameDecorator(@NotNull JFrame frame, @NotNull Disposable parentDisposable) { super(frame); + if (JdkEx.setTabbingMode(frame, () -> updateTabBars(null))) { + // update tab logic only after call [NSWindow makeKeyAndOrderFront] + frame.addPropertyChangeListener("focusableWindowState", new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + myFrame.removePropertyChangeListener("focusableWindowState", this); + updateTabBars(myFrame); + } + }); + Disposer.register(parentDisposable, new Disposable() { // don't convert to lambda + @Override + public void dispose() { + updateTabBars(null); + } + }); + } + if (leaveFullScreenMethod != null || toggleFullScreenMethod != null) { FullScreenUtilities.setWindowCanFullScreen(frame, true); @@ -140,6 +196,7 @@ public final class MacMainFrameDecorator extends IdeFrameDecorator { if (rootPane != null && rootPane.getBorder() != null && Registry.is("ide.mac.transparentTitleBarAppearance")) { rootPane.setBorder(null); } + updateTabBar(myFrame, 0); } @Override @@ -169,6 +226,115 @@ public final class MacMainFrameDecorator extends IdeFrameDecorator { } } + private static void updateTabBars(@Nullable JFrame newFrame) { + if (!JdkEx.isTabbingModeAvailable()) { + return; + } + + IdeFrame[] frames = WindowManager.getInstance().getAllProjectFrames(); + + if (frames.length < 2) { + if (frames.length == 1) { + updateTabBar(frames[0], 0); + } + return; + } + + ApplicationManager.getApplication().invokeLater(() -> { + Integer[] visibleAndHeights = new Integer[frames.length]; + boolean callInAppkit = false; + int newIndex = -1; + + for (int i = 0; i < frames.length; i++) { + ProjectFrameHelper helper = (ProjectFrameHelper)frames[i]; + if (newFrame == helper.getFrame()) { + //boolean visible = Foundation.invoke("NSWindow", "userTabbingPreference").intValue() == 1; // NSWindowUserTabbingPreferenceAlways = 1 + //visibleAndHeights[i] = visible ? DEFAULT_WIN_TAB_HEIGHT : 0; + newIndex = i; + } + else if (helper.isInFullScreen()) { + visibleAndHeights[i] = 0; + } + else { + callInAppkit = true; + } + } + + if (callInAppkit) { + // call only for shown window and only in Appkit + Foundation.executeOnMainThread(true, false, () -> { + for (int i = 0; i < frames.length; i++) { + if (visibleAndHeights[i] == null) { + ID window = MacUtil.getWindowFromJavaWindow(((ProjectFrameHelper)frames[i]).getFrame()); + visibleAndHeights[i] = (int)Foundation.invoke_fpret(window, "getTabBarVisibleAndHeight"); + if (visibleAndHeights[i] == -1) { + visibleAndHeights[i] = DEFAULT_WIN_TAB_HEIGHT(); + } + } + } + + ApplicationManager.getApplication().invokeLater(() -> { + for (int i = 0; i < frames.length; i++) { + updateTabBar(frames[i], visibleAndHeights[i]); + } + }); + }); + } + else { + if (newIndex != -1) { + visibleAndHeights[newIndex] = 0; + } + + for (int i = 0; i < frames.length; i++) { + updateTabBar(frames[i], visibleAndHeights[i]); + } + } + }); + } + + private void updateTabBarOnExitFromFullScreen() { + if (!JdkEx.isTabbingModeAvailable()) { + return; + } + + Foundation.executeOnMainThread(true, false, () -> { + ID window = MacUtil.getWindowFromJavaWindow(myFrame); + int visibleAndHeight = (int)Foundation.invoke_fpret(window, "getTabBarVisibleAndHeight"); + ApplicationManager.getApplication() + .invokeLater(() -> updateTabBar(myFrame, visibleAndHeight == -1 ? DEFAULT_WIN_TAB_HEIGHT() : visibleAndHeight)); + }); + } + + private static void updateTabBar(@NotNull Object frameObject, int height) { + JFrame frame = null; + if (frameObject instanceof JFrame) { + frame = (JFrame)frameObject; + } + else if (frameObject instanceof ProjectFrameHelper) { + frame = ((ProjectFrameHelper)frameObject).getFrame(); + } + if (frame == null) { + return; + } + + JComponent filler = (JComponent)frame.getRootPane().getClientProperty(WIN_TAB_FILLER); + if (filler == null) { + return; + } + boolean visible = height > 0; + boolean oldVisible = filler.isVisible(); + filler.setVisible(visible); + filler.setPreferredSize(new JBDimension(-1, height)); + + Container parent = filler.getParent(); + if (parent == null || oldVisible == visible) { + return; + } + parent.doLayout(); + parent.revalidate(); + parent.repaint(); + } + @Override public boolean isInFullScreen() { return myInFullScreen; diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index 42725403f8c4..5ee55b72fbd2 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -1916,4 +1916,11 @@ ide.new.stripes.ui=false ide.new.stripes.ui.description=One stripe layout ide.web.preview.enabled=false -ide.web.preview.enabled.description=Ability to preview html content directly in IDE \ No newline at end of file +ide.web.preview.enabled.description=Ability to preview html content directly in IDE + +ide.mac.bigsur.window.with.tabs.height=28 +ide.mac.bigsur.window.with.tabs.height.description=Default height for tabs row + +ide.mac.bigsur.window.with.tabs.enabled=true +ide.mac.bigsur.window.with.tabs.enabled.restartRequired=true +ide.mac.bigsur.window.with.tabs.enabled.description=Enable Mac OS Big Sur window tabs \ No newline at end of file diff --git a/platform/util/ui/src/com/intellij/ui/mac/foundation/MacUtil.java b/platform/util/ui/src/com/intellij/ui/mac/foundation/MacUtil.java index 679bf8441937..ae296b149f07 100644 --- a/platform/util/ui/src/com/intellij/ui/mac/foundation/MacUtil.java +++ b/platform/util/ui/src/com/intellij/ui/mac/foundation/MacUtil.java @@ -142,25 +142,43 @@ public final class MacUtil { Toolkit.getDefaultToolkit().addAWTEventListener(listener, AWTEvent.KEY_EVENT_MASK); } - public static ID findWindowFromJavaWindow(final Window w) { - if (SystemInfo.isJetBrainsJvm && Registry.is("skip.untitled.windows.for.mac.messages")) { + @NotNull + public static ID getWindowFromJavaWindow(@Nullable Window w) { + if (w == null) { + return ID.NIL; + } + if (SystemInfo.isJetBrainsJvm) { try { Class awtAccessor = Class.forName("sun.awt.AWTAccessor"); Object componentAccessor = awtAccessor.getMethod("getComponentAccessor").invoke(null); Method getPeer = componentAccessor.getClass().getMethod("getPeer", Component.class); getPeer.setAccessible(true); Object peer = getPeer.invoke(componentAccessor, w); - Class cWindowPeerClass = peer.getClass(); - Method getPlatformWindowMethod = cWindowPeerClass.getDeclaredMethod("getPlatformWindow"); - Object cPlatformWindow = getPlatformWindowMethod.invoke(peer); - Field ptr = cPlatformWindow.getClass().getSuperclass().getDeclaredField("ptr"); - ptr.setAccessible(true); - return new ID(ptr.getLong(cPlatformWindow)); + if (peer != null) { + Class cWindowPeerClass = peer.getClass(); + Method getPlatformWindowMethod = cWindowPeerClass.getDeclaredMethod("getPlatformWindow"); + Object cPlatformWindow = getPlatformWindowMethod.invoke(peer); + if (cPlatformWindow != null) { + Field ptr = cPlatformWindow.getClass().getSuperclass().getDeclaredField("ptr"); + ptr.setAccessible(true); + return new ID(ptr.getLong(cPlatformWindow)); + } + } } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | ClassNotFoundException | NoSuchFieldException e) { LOG.debug(e); } } + return ID.NIL; + } + + public static ID findWindowFromJavaWindow(final Window w) { + if (Registry.is("skip.untitled.windows.for.mac.messages")) { + ID window = getWindowFromJavaWindow(w); + if (!ID.NIL.equals(window)) { + return window; + } + } return findWindowForTitle(getWindowTitle(w)); }