blob: dcb514fc1179166f894c882bd7df92eebd6ca11b [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.ui.mac;
import com.apple.eawt.*;
import com.intellij.Patches;
import com.intellij.ide.ui.UISettings;
import com.intellij.ide.ui.UISettingsListener;
import com.intellij.openapi.application.ex.ApplicationInfoEx;
import com.intellij.openapi.application.impl.ApplicationInfoImpl;
import com.intellij.openapi.application.impl.LaterInvocator;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.ActionCallback;
import com.intellij.openapi.util.BuildNumber;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.wm.impl.IdeFrameDecorator;
import com.intellij.openapi.wm.impl.IdeFrameImpl;
import com.intellij.ui.CustomProtocolHandler;
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.Function;
import com.sun.jna.Callback;
import com.sun.jna.Pointer;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.EventListener;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static com.intellij.ui.mac.foundation.Foundation.invoke;
/**
* User: spLeaner
*/
public class MacMainFrameDecorator extends IdeFrameDecorator implements UISettingsListener {
private static final Logger LOG = Logger.getInstance("#com.intellij.ui.mac.MacMainFrameDecorator");
private final static boolean ORACLE_BUG_ID_8003173 = SystemInfo.isJavaVersionAtLeast("1.7");
private final FullscreenQueue<Runnable> myFullscreenQueue = new FullscreenQueue<Runnable>();
private final EventDispatcher<FSListener> myDispatcher = EventDispatcher.create(FSListener.class);
private interface FSListener extends FullScreenListener, EventListener {}
private static class FSAdapter extends FullScreenAdapter implements FSListener {}
private static class FullscreenQueue <T extends Runnable> {
private boolean waitingForAppKit = false;
private LinkedList<Runnable> queueModel = new LinkedList<Runnable>();
synchronized void runOrEnqueue (final T runnable) {
if (waitingForAppKit) {
enqueue(runnable);
} else {
LaterInvocator.invokeLater(runnable);
waitingForAppKit = true;
}
}
synchronized private void enqueue (final T runnable) {
queueModel.add(runnable);
}
synchronized void runFromQueue () {
if (!queueModel.isEmpty()) {
queueModel.remove().run();
waitingForAppKit = true;
} else {
waitingForAppKit = false;
}
}
}
// Fullscreen listener delivers event too late,
// so we use method swizzling here
private final Callback windowWillEnterFullScreenCallBack = new Callback() {
public void callback(ID self,
ID nsNotification)
{
invoke(self, "oldWindowWillEnterFullScreen:", nsNotification);
enterFullscreen();
}
};
private void enterFullscreen() {
myInFullScreen = true;
myFrame.storeFullScreenStateIfNeeded(true);
myFullscreenQueue.runFromQueue();
}
private final Callback windowWillExitFullScreenCallBack = new Callback() {
public void callback(ID self,
ID nsNotification)
{
invoke(self, "oldWindowWillExitFullScreen:", nsNotification);
exitFullscreen();
}
};
private void exitFullscreen() {
myInFullScreen = false;
myFrame.storeFullScreenStateIfNeeded(false);
JRootPane rootPane = myFrame.getRootPane();
if (rootPane != null) rootPane.putClientProperty(FULL_SCREEN, null);
myFullscreenQueue.runFromQueue();
}
public static final String FULL_SCREEN = "Idea.Is.In.FullScreen.Mode.Now";
private static boolean HAS_FULLSCREEN_UTILITIES;
private static Method requestToggleFullScreenMethod;
static {
try {
Class.forName("com.apple.eawt.FullScreenUtilities");
requestToggleFullScreenMethod = Application.class.getMethod("requestToggleFullScreen", Window.class);
HAS_FULLSCREEN_UTILITIES = true;
} catch (Exception e) {
HAS_FULLSCREEN_UTILITIES = false;
}
}
public static final boolean FULL_SCREEN_AVAILABLE = SystemInfo.isJavaVersionAtLeast("1.6.0_29") && HAS_FULLSCREEN_UTILITIES;
private static boolean SHOWN = false;
private static Callback SET_VISIBLE_CALLBACK = new Callback() {
public void callback(ID caller, ID selector, ID value) {
SHOWN = value.intValue() == 1;
SwingUtilities.invokeLater(CURRENT_SETTER);
}
};
private static Callback IS_VISIBLE = new Callback() {
public boolean callback(ID caller) {
return SHOWN;
}
};
private static AtomicInteger UNIQUE_COUNTER = new AtomicInteger(0);
public static final Runnable TOOLBAR_SETTER = new Runnable() {
@Override
public void run() {
final UISettings settings = UISettings.getInstance();
settings.SHOW_MAIN_TOOLBAR = SHOWN;
settings.fireUISettingsChanged();
}
};
public static final Runnable NAVBAR_SETTER = new Runnable() {
@Override
public void run() {
final UISettings settings = UISettings.getInstance();
settings.SHOW_NAVIGATION_BAR = SHOWN;
settings.fireUISettingsChanged();
}
};
public static final Function<Object, Boolean> NAVBAR_GETTER = new Function<Object, Boolean>() {
@Override
public Boolean fun(Object o) {
return UISettings.getInstance().SHOW_NAVIGATION_BAR;
}
};
public static final Function<Object, Boolean> TOOLBAR_GETTER = new Function<Object, Boolean>() {
@Override
public Boolean fun(Object o) {
return UISettings.getInstance().SHOW_MAIN_TOOLBAR;
}
};
private static Runnable CURRENT_SETTER = null;
private static Function<Object, Boolean> CURRENT_GETTER = null;
private static CustomProtocolHandler ourProtocolHandler = null;
private boolean myInFullScreen;
public MacMainFrameDecorator(@NotNull final IdeFrameImpl frame, final boolean navBar) {
super(frame);
if (CURRENT_SETTER == null) {
CURRENT_SETTER = navBar ? NAVBAR_SETTER : TOOLBAR_SETTER;
CURRENT_GETTER = navBar ? NAVBAR_GETTER : TOOLBAR_GETTER;
SHOWN = CURRENT_GETTER.fun(null);
}
UISettings.getInstance().addUISettingsListener(this, this);
final ID pool = invoke("NSAutoreleasePool", "new");
//if (ORACLE_BUG_ID_8003173) {
// replaceNativeFullscreenListenerCallback();
//}
int v = UNIQUE_COUNTER.incrementAndGet();
if (Patches.APPLE_BUG_ID_10514018) {
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowDeiconified(WindowEvent e) {
if (e.getWindow() == frame && frame.getState() == Frame.ICONIFIED) {
frame.setState(Frame.NORMAL);
}
}
});
}
try {
if (SystemInfo.isMacOSLion) {
if (!FULL_SCREEN_AVAILABLE) return;
FullScreenUtilities.setWindowCanFullScreen(frame, true);
// Native fullscreen listener can be set only once
FullScreenUtilities.addFullScreenListenerTo(frame, new FullScreenListener() {
@Override
public void windowEnteringFullScreen(AppEvent.FullScreenEvent event) {
myDispatcher.getMulticaster().windowEnteringFullScreen(event);
}
@Override
public void windowEnteredFullScreen(AppEvent.FullScreenEvent event) {
myDispatcher.getMulticaster().windowEnteredFullScreen(event);
}
@Override
public void windowExitingFullScreen(AppEvent.FullScreenEvent event) {
myDispatcher.getMulticaster().windowExitingFullScreen(event);
}
@Override
public void windowExitedFullScreen(AppEvent.FullScreenEvent event) {
myDispatcher.getMulticaster().windowExitedFullScreen(event);
}
});
myDispatcher.addListener(new FSAdapter() {
@Override
public void windowEnteredFullScreen(AppEvent.FullScreenEvent event) {
// We can get the notification when the frame has been disposed
JRootPane rootPane = frame.getRootPane();
if (rootPane != null) rootPane.putClientProperty(FULL_SCREEN, Boolean.TRUE);
enterFullscreen();
myFrame.validate();
}
@Override
public void windowExitedFullScreen(AppEvent.FullScreenEvent event) {
// We can get the notification when the frame has been disposed
if (myFrame == null/* || ORACLE_BUG_ID_8003173*/) return;
exitFullscreen();
myFrame.validate();
}
});
}
else {
final ID window = MacUtil.findWindowForTitle(frame.getTitle());
if (window == null) return;
// toggle toolbar
String className = "IdeaToolbar" + v;
final ID ownToolbar = Foundation.allocateObjcClassPair(Foundation.getObjcClass("NSToolbar"), className);
Foundation.registerObjcClassPair(ownToolbar);
final ID toolbar = invoke(invoke(className, "alloc"), "initWithIdentifier:", Foundation.nsString(className));
Foundation.cfRetain(toolbar);
invoke(toolbar, "setVisible:", 0); // hide native toolbar by default
Foundation.addMethod(ownToolbar, Foundation.createSelector("setVisible:"), SET_VISIBLE_CALLBACK, "v*");
Foundation.addMethod(ownToolbar, Foundation.createSelector("isVisible"), IS_VISIBLE, "B*");
Foundation.executeOnMainThread(new Runnable() {
@Override
public void run() {
invoke(window, "setToolbar:", toolbar);
invoke(window, "setShowsToolbarButton:", 1);
}
}, true, true);
}
}
finally {
invoke(pool, "release");
}
if (ourProtocolHandler == null) {
// install uri handler
final ID mainBundle = invoke("NSBundle", "mainBundle");
final ID urlTypes = invoke(mainBundle, "objectForInfoDictionaryKey:", Foundation.nsString("CFBundleURLTypes"));
final ApplicationInfoEx info = ApplicationInfoImpl.getShadowInstance();
final BuildNumber build = info != null ? info.getBuild() : null;
if (urlTypes.equals(ID.NIL) && build != null && !build.isSnapshot()) {
LOG.warn("no url bundle present. \n" +
"To use platform protocol handler to open external links specify required protocols in the mac app layout section of the build file\n" +
"Example: args.urlSchemes = [\"your-protocol\"] will handle following links: your-protocol://open?file=file&line=line");
return;
}
ourProtocolHandler = new CustomProtocolHandler();
Application.getApplication().setOpenURIHandler(new OpenURIHandler() {
@Override
public void openURI(AppEvent.OpenURIEvent event) {
ourProtocolHandler.openLink(event.getURI());
}
});
}
}
private void replaceNativeFullscreenListenerCallback() {
ID awtWindow = Foundation.getObjcClass("AWTWindow");
Pointer windowWillEnterFullScreenMethod = Foundation.createSelector("windowWillEnterFullScreen:");
ID originalWindowWillEnterFullScreen = Foundation.class_replaceMethod(awtWindow, windowWillEnterFullScreenMethod,
windowWillEnterFullScreenCallBack, "v@::@");
Foundation.addMethodByID(awtWindow, Foundation.createSelector("oldWindowWillEnterFullScreen:"),
originalWindowWillEnterFullScreen, "v@::@");
Pointer windowWillExitFullScreenMethod = Foundation.createSelector("windowWillExitFullScreen:");
ID originalWindowWillExitFullScreen = Foundation.class_replaceMethod(awtWindow, windowWillExitFullScreenMethod,
windowWillExitFullScreenCallBack, "v@::@");
Foundation.addMethodByID(awtWindow, Foundation.createSelector("oldWindowWillExitFullScreen:"),
originalWindowWillExitFullScreen, "v@::@");
}
@Override
public void uiSettingsChanged(final UISettings source) {
if (CURRENT_GETTER != null) {
SHOWN = CURRENT_GETTER.fun(null);
}
}
@Override
public boolean isInFullScreen() {
return myInFullScreen;
}
@Override
public ActionCallback toggleFullScreen(final boolean state) {
if (!SystemInfo.isMacOSLion || myFrame == null || myInFullScreen == state) return ActionCallback.REJECTED;
final ActionCallback callback = new ActionCallback();
myDispatcher.addListener(new FSAdapter() {
@Override
public void windowExitedFullScreen(AppEvent.FullScreenEvent event) {
callback.setDone();
myDispatcher.removeListener(this);
}
@Override
public void windowEnteredFullScreen(AppEvent.FullScreenEvent event) {
callback.setDone();
myDispatcher.removeListener(this);
}
});
myFullscreenQueue.runOrEnqueue( new Runnable() {
@Override
public void run() {
try {
requestToggleFullScreenMethod.invoke(Application.getApplication(),myFrame);
}
catch (IllegalAccessException e) {
LOG.error(e);
}
catch (InvocationTargetException e) {
LOG.error(e);
}
}
});
return callback;
}
public void exitFullScreenAndDispose() {
LOG.assertTrue(isInFullScreen());
try {
requestToggleFullScreenMethod.invoke(Application.getApplication(), myFrame);
}
catch (IllegalAccessException e) {
LOG.error(e);
}
catch (InvocationTargetException e) {
LOG.error(e);
}
myDispatcher.addListener(new FSAdapter() {
@Override
public void windowExitedFullScreen(AppEvent.FullScreenEvent event) {
myFrame.disposeImpl();
myDispatcher.removeListener(this);
}
});
}
}