JBR-2893 Big Sur: Add support of opening project as tabs

GitOrigin-RevId: 60da7d5975158ebade2345a360fa521cbd830426
This commit is contained in:
Alexander Lobas
2020-12-14 09:21:00 +00:00
committed by intellij-monorepo-bot
parent ab86ebe099
commit eecda53cd9
6 changed files with 252 additions and 10 deletions
@@ -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;
}
}
@@ -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();
@@ -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() {
@@ -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<FSListener> 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;
@@ -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
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
@@ -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));
}