/*
* Copyright 2000-2016 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.openapi.wm.impl;
import com.intellij.diagnostic.IdeMessagePanel;
import com.intellij.ide.AppLifecycleListener;
import com.intellij.ide.DataManager;
import com.intellij.ide.impl.ProjectUtil;
import com.intellij.ide.ui.UISettings;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.notification.impl.IdeNotificationArea;
import com.intellij.openapi.MnemonicHelper;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.openapi.actionSystem.ex.ActionManagerEx;
import com.intellij.openapi.actionSystem.impl.MouseGestureManager;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationInfo;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.ex.ApplicationInfoEx;
import com.intellij.openapi.application.ex.ApplicationManagerEx;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.wm.IdeFrame;
import com.intellij.openapi.wm.IdeRootPaneNorthExtension;
import com.intellij.openapi.wm.StatusBar;
import com.intellij.openapi.wm.WindowManager;
import com.intellij.openapi.wm.ex.IdeFrameEx;
import com.intellij.openapi.wm.ex.LayoutFocusTraversalPolicyExt;
import com.intellij.openapi.wm.ex.StatusBarEx;
import com.intellij.openapi.wm.impl.status.*;
import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeFrame;
import com.intellij.ui.*;
import com.intellij.ui.mac.MacMainFrameDecorator;
import com.intellij.util.Alarm;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.accessibility.AccessibleContextAccessor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.accessibility.AccessibleContext;
import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.lang.reflect.Field;
/**
* @author Anton Katilin
* @author Vladimir Kondratyev
*/
public class IdeFrameImpl extends JFrame implements IdeFrameEx, AccessibleContextAccessor, DataProvider {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.wm.impl.IdeFrameImpl");
public static final Key<Boolean> SHOULD_OPEN_IN_FULL_SCREEN = Key.create("should.open.in.full.screen");
private static final String FULL_SCREEN = "FullScreen";
private static boolean myUpdatingTitle;
private String myTitle;
private String myFileTitle;
private File myCurrentFile;
private Project myProject;
private IdeRootPane myRootPane;
private BalloonLayout myBalloonLayout;
private IdeFrameDecorator myFrameDecorator;
private PropertyChangeListener myWindowsBorderUpdater;
private boolean myRestoreFullScreen;
public IdeFrameImpl(ApplicationInfoEx applicationInfoEx,
ActionManagerEx actionManager,
DataManager dataManager,
Application application) {
super(applicationInfoEx.getFullApplicationName());
myRootPane = createRootPane(actionManager, dataManager, application);
setRootPane(myRootPane);
setBackground(UIUtil.getPanelBackground());
AppUIUtil.updateWindowIcon(this);
final Dimension size = ScreenUtil.getMainScreenBounds().getSize();
size.width = Math.min(1400, size.width - 20);
size.height= Math.min(1000, size.height - 40);
setSize(size);
setLocationRelativeTo(null);
LayoutFocusTraversalPolicyExt layoutFocusTraversalPolicy = new LayoutFocusTraversalPolicyExt();
setFocusTraversalPolicy(layoutFocusTraversalPolicy);
setupCloseAction();
MnemonicHelper.init(this);
myBalloonLayout = new BalloonLayoutImpl(myRootPane, new Insets(8, 8, 8, 8));
// to show window thumbnail under Macs
// http://lists.apple.com/archives/java-dev/2009/Dec/msg00240.html
if (SystemInfo.isMac) setIconImage(null);
MouseGestureManager.getInstance().add(this);
myFrameDecorator = IdeFrameDecorator.decorate(this);
addWindowStateListener(new WindowAdapter() {
@Override
public void windowStateChanged(WindowEvent e) {
updateBorder();
}
});
if (SystemInfo.isWindows) {
myWindowsBorderUpdater = __ -> updateBorder();
Toolkit.getDefaultToolkit().addPropertyChangeListener("win.xpstyle.themeActive", myWindowsBorderUpdater);
if (!SystemInfo.isJavaVersionAtLeast("1.8")) {
final Ref<Dimension> myDimensionRef = new Ref<>(new Dimension());
final Alarm alarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD);
final Runnable runnable = new Runnable() {
@Override
public void run() {
if (isDisplayable() && !getSize().equals(myDimensionRef.get())) {
Rectangle bounds = getBounds();
bounds.width--;
setBounds(bounds);
bounds.width++;
setBounds(bounds);
myDimensionRef.set(getSize());
}
alarm.addRequest(this, 50);
}
};
alarm.addRequest(runnable, 50);
}
}
// UIUtil.suppressFocusStealing();
}
private void updateBorder() {
int state = getExtendedState();
if (!WindowManager.getInstance().isFullScreenSupportedInCurrentOS() || !SystemInfo.isWindows || myRootPane == null) {
return;
}
myRootPane.setBorder(null);
boolean isNotClassic = Boolean.parseBoolean(String.valueOf(Toolkit.getDefaultToolkit().getDesktopProperty("win.xpstyle.themeActive")));
if (isNotClassic && (state & MAXIMIZED_BOTH) != 0) {
IdeFrame[] projectFrames = WindowManager.getInstance().getAllProjectFrames();
GraphicsDevice device = ScreenUtil.getScreenDevice(getBounds());
for (IdeFrame frame : projectFrames) {
if (frame == this) continue;
if (((IdeFrameImpl)frame).isInFullScreen() && ScreenUtil.getScreenDevice(((IdeFrameImpl)frame).getBounds()) == device) {
Insets insets = ScreenUtil.getScreenInsets(device.getDefaultConfiguration());
int mask = SideBorder.NONE;
if (insets.top != 0) mask |= SideBorder.TOP;
if (insets.left != 0) mask |= SideBorder.LEFT;
if (insets.bottom != 0) mask |= SideBorder.BOTTOM;
if (insets.right != 0) mask |= SideBorder.RIGHT;
myRootPane.setBorder(new SideBorder(JBColor.BLACK, mask, 3));
break;
}
}
}
}
protected IdeRootPane createRootPane(ActionManagerEx actionManager,
DataManager dataManager,
Application application) {
return new IdeRootPane(actionManager, dataManager, application, this);
}
@NotNull
@Override
public Insets getInsets() {
if (SystemInfo.isMac && isInFullScreen()) {
return new Insets(0, 0, 0, 0);
}
return super.getInsets();
}
@Override
public JComponent getComponent() {
return getRootPane();
}
@Nullable
public static Window getActiveFrame() {
for (Frame frame : getFrames()) {
if (frame.isActive()) return frame;
}
return null;
}
@Override
public void show() {
super.show();
SwingUtilities.invokeLater(() -> setFocusableWindowState(true));
}
/**
* This is overridden to get rid of strange Alloy LaF customization of frames. For unknown reason it sets the maxBounds rectangle
* and it does it plain wrong. Setting bounds to {@code null} means default value should be taken from the underlying OS.
*/
@Override
public synchronized void setMaximizedBounds(Rectangle bounds) {
super.setMaximizedBounds(null);
}
private void setupCloseAction() {
setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
addWindowListener(
new WindowAdapter() {
@Override
public void windowClosing(@NotNull final WindowEvent e) {
if (isTemporaryDisposed())
return;
final Project[] openProjects = ProjectManager.getInstance().getOpenProjects();
if (openProjects.length > 1 || openProjects.length == 1 && SystemInfo.isMacSystemMenu) {
if (myProject != null && myProject.isOpen()) {
ProjectUtil.closeAndDispose(myProject);
}
ApplicationManager.getApplication().getMessageBus().syncPublisher(AppLifecycleListener.TOPIC).projectFrameClosed();
WelcomeFrame.showIfNoProjectOpened();
}
else {
ApplicationManagerEx.getApplicationEx().exit();
}
}
}
);
}
@Override
public StatusBar getStatusBar() {
return myRootPane == null ? null : myRootPane.getStatusBar();
}
@Override
public void setTitle(final String title) {
if (myUpdatingTitle) {
super.setTitle(title);
} else {
myTitle = title;
}
updateTitle();
}
@Override
public void setFrameTitle(final String text) {
super.setTitle(text);
}
public void setFileTitle(final String fileTitle) {
setFileTitle(fileTitle, null);
}
@Override
public void setFileTitle(@Nullable final String fileTitle, @Nullable File file) {
myFileTitle = fileTitle;
myCurrentFile = file;
updateTitle();
}
@Override
public IdeRootPaneNorthExtension getNorthExtension(String key) {
return myRootPane.findByName(key);
}
private void updateTitle() {
updateTitle(this, myTitle, myFileTitle, myCurrentFile);
}
public static void updateTitle(JFrame frame, final String title, final String fileTitle, final File currentFile) {
if (myUpdatingTitle) return;
try {
myUpdatingTitle = true;
frame.getRootPane().putClientProperty("Window.documentFile", currentFile);
final String applicationName = ((ApplicationInfoEx)ApplicationInfo.getInstance()).getFullApplicationName();
final Builder builder = new Builder();
if (SystemInfo.isMac) {
boolean addAppName = StringUtil.isEmpty(title) ||
ProjectManager.getInstance().getOpenProjects().length == 0 ||
((ApplicationInfoEx)ApplicationInfo.getInstance()).isEAP() && !applicationName.endsWith("SNAPSHOT");
builder.append(fileTitle).append(title).append(addAppName ? applicationName : null);
} else {
builder.append(title).append(fileTitle).append(applicationName);
}
frame.setTitle(builder.sb.toString());
}
finally {
myUpdatingTitle = false;
}
}
public void updateView() {
((IdeRootPane)getRootPane()).updateToolbar();
((IdeRootPane)getRootPane()).updateMainMenuActions();
((IdeRootPane)getRootPane()).updateNorthComponents();
}
@Override
public AccessibleContext getCurrentAccessibleContext() {
return accessibleContext;
}
private static final class Builder {
public StringBuilder sb = new StringBuilder();
public Builder append(@Nullable final String s) {
if (s == null || s.isEmpty()) return this;
if (sb.length() > 0) sb.append(" - ");
sb.append(s);
return this;
}
}
@Override
public Object getData(final String dataId) {
if (CommonDataKeys.PROJECT.is(dataId)) {
if (myProject != null) {
return myProject.isInitialized() ? myProject : null;
}
}
if (IdeFrame.KEY.getName().equals(dataId)) {
return this;
}
return null;
}
public void setProject(final Project project) {
if (WindowManager.getInstance().isFullScreenSupportedInCurrentOS() && myProject != project && project != null) {
myRestoreFullScreen = myProject == null && shouldRestoreFullScreen(project);
if (myProject != null) {
storeFullScreenStateIfNeeded(false); // disable for old project
}
}
myProject = project;
if (project != null) {
ProjectFrameBounds.getInstance(project); // make sure the service is initialized and its state will be saved
if (myRootPane != null) {
myRootPane.installNorthComponents(project);
project.getMessageBus().connect().subscribe(StatusBar.Info.TOPIC, myRootPane.getStatusBar());
}
installDefaultProjectStatusBarWidgets(myProject);
}
else {
if (myRootPane != null) { //already disposed
myRootPane.deinstallNorthComponents();
}
}
if (project == null) {
FocusTrackback.release(this);
}
if (isVisible() && myRestoreFullScreen) {
toggleFullScreen(true);
myRestoreFullScreen = false;
}
}
@SuppressWarnings("SSBasedInspection")
@Override
public void setVisible(boolean b) {
super.setVisible(b);
if (b && myRestoreFullScreen) {
SwingUtilities.invokeLater(() -> {
toggleFullScreen(true);
if (SystemInfo.isMacOSLion) {
setBounds(ScreenUtil.getScreenRectangle(getLocationOnScreen()));
}
myRestoreFullScreen = false;
});
}
}
private void installDefaultProjectStatusBarWidgets(@NotNull final Project project) {
final StatusBar statusBar = getStatusBar();
final PositionPanel positionPanel = new PositionPanel(project);
statusBar.addWidget(positionPanel, "before " + IdeMessagePanel.FATAL_ERROR);
final IdeNotificationArea notificationArea = new IdeNotificationArea();
statusBar.addWidget(notificationArea, "before " + IdeMessagePanel.FATAL_ERROR);
final EncodingPanel encodingPanel = new EncodingPanel(project);
statusBar.addWidget(encodingPanel, "after Position");
final LineSeparatorPanel lineSeparatorPanel = new LineSeparatorPanel(project);
statusBar.addWidget(lineSeparatorPanel, "before " + encodingPanel.ID());
final ToggleReadOnlyAttributePanel readOnlyAttributePanel = new ToggleReadOnlyAttributePanel(project);
final InsertOverwritePanel insertOverwritePanel = new InsertOverwritePanel(project);
statusBar.addWidget(insertOverwritePanel, "after Encoding");
statusBar.addWidget(readOnlyAttributePanel, "after InsertOverwrite");
Disposer.register(project, () -> {
statusBar.removeWidget(encodingPanel.ID());
statusBar.removeWidget(lineSeparatorPanel.ID());
statusBar.removeWidget(positionPanel.ID());
statusBar.removeWidget(notificationArea.ID());
statusBar.removeWidget(readOnlyAttributePanel.ID());
statusBar.removeWidget(insertOverwritePanel.ID());
((StatusBarEx)statusBar).removeCustomIndicationComponents();
});
}
@Override
public Project getProject() {
return myProject;
}
@Override
public void dispose() {
if (SystemInfo.isMac && isInFullScreen()) {
((MacMainFrameDecorator)myFrameDecorator).toggleFullScreenNow();
}
if (isTemporaryDisposed()) {
super.dispose();
return;
}
MouseGestureManager.getInstance().remove(this);
if (myBalloonLayout != null) {
((BalloonLayoutImpl)myBalloonLayout).dispose();
myBalloonLayout = null;
}
// clear both our and swing hard refs
if (myRootPane != null) {
if (ApplicationManager.getApplication().isUnitTestMode()) {
myRootPane.removeNotify();
}
myRootPane = null;
setRootPane(new JRootPane());
}
if (myFrameDecorator != null) {
Disposer.dispose(myFrameDecorator);
myFrameDecorator = null;
}
if (myWindowsBorderUpdater != null) {
Toolkit.getDefaultToolkit().removePropertyChangeListener("win.xpstyle.themeActive", myWindowsBorderUpdater);
myWindowsBorderUpdater = null;
}
FocusTrackback.release(this);
super.dispose();
}
private boolean isTemporaryDisposed() {
return myRootPane != null && myRootPane.getClientProperty(ScreenUtil.DISPOSE_TEMPORARY) != null;
}
void storeFullScreenStateIfNeeded() {
if (myFrameDecorator != null) {
storeFullScreenStateIfNeeded(myFrameDecorator.isInFullScreen());
}
}
public void storeFullScreenStateIfNeeded(boolean state) {
if (!WindowManager.getInstance().isFullScreenSupportedInCurrentOS()) return;
if (myProject != null) {
PropertiesComponent.getInstance(myProject).setValue(FULL_SCREEN, state);
doLayout();
}
}
private static boolean shouldRestoreFullScreen(@Nullable Project project) {
return WindowManager.getInstance().isFullScreenSupportedInCurrentOS() &&
project != null &&
(SHOULD_OPEN_IN_FULL_SCREEN.get(project) == Boolean.TRUE || PropertiesComponent.getInstance(project).getBoolean(FULL_SCREEN));
}
@Override
public void paint(@NotNull Graphics g) {
UISettings.setupAntialiasing(g);
//noinspection Since15
super.paint(g);
}
@Override
public Color getBackground() {
return super.getBackground();
}
@Override
public void doLayout() {
super.doLayout();
}
@Override
public Rectangle suggestChildFrameBounds() {
//todo [kirillk] a dummy implementation
final Rectangle b = getBounds();
b.x += 100;
b.width -= 200;
b.y += 100;
b.height -= 200;
return b;
}
@Override
public final BalloonLayout getBalloonLayout() {
return myBalloonLayout;
}
@Override
public boolean isInFullScreen() {
return myFrameDecorator != null && myFrameDecorator.isInFullScreen();
}
@NotNull
@Override
public ActionCallback toggleFullScreen(boolean state) {
if (temporaryFixForIdea156004(state)) return ActionCallback.DONE;
if (myFrameDecorator != null) {
return myFrameDecorator.toggleFullScreen(state);
}
IdeFrame[] frames = WindowManager.getInstance().getAllProjectFrames();
for (IdeFrame frame : frames) {
((IdeFrameImpl)frame).updateBorder();
}
return ActionCallback.DONE;
}
private boolean temporaryFixForIdea156004(final boolean state) {
if (SystemInfo.isMac) {
try {
Field modalBlockerField = Window.class.getDeclaredField("modalBlocker");
modalBlockerField.setAccessible(true);
final Window modalBlocker = (Window)modalBlockerField.get(this);
if (modalBlocker != null) {
ApplicationManager.getApplication().invokeLater(() -> toggleFullScreen(state), ModalityState.NON_MODAL);
return true;
}
}
catch (NoSuchFieldException | IllegalAccessException e) {
LOG.error(e);
}
}
return false;
}
@Override
public AccessibleContext getAccessibleContext() {
if (accessibleContext == null) {
accessibleContext = new AccessibleIdeFrameImpl();
}
return accessibleContext;
}
protected class AccessibleIdeFrameImpl extends AccessibleJFrame {
@Override
public String getAccessibleName() {
final StringBuilder builder = new StringBuilder();
if (myProject != null) {
builder.append(myProject.getName());
builder.append(" - ");
}
final String applicationName = ((ApplicationInfoEx)ApplicationInfo.getInstance()).getFullApplicationName();
builder.append(applicationName);
return builder.toString();
}
}
}