From 2c0c7ec040cb3aca5308a0137372d276763116c4 Mon Sep 17 00:00:00 2001 From: Anton Tarasov Date: Thu, 5 Sep 2019 12:24:49 +0300 Subject: [PATCH] IDEA-222026 implement heavyweight facade heuristic for jcef overlapping problem GitOrigin-RevId: 2094959a1c28859390df7c7e9b7a0923968471a8 --- .../src/com/intellij/jdkEx/JdkEx.java | 15 ++ .../src/com/intellij/ui/BalloonImpl.java | 6 +- .../com/intellij/ui/jcef/HwFacadeHelper.java | 215 ++++++++++++++++++ .../com/intellij/ui/jcef/HwFacadeJPanel.java | 46 ++++ .../ui/jcef/HwFacadeNonOpaquePanel.java | 47 ++++ 5 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 platform/platform-impl/src/com/intellij/ui/jcef/HwFacadeHelper.java create mode 100644 platform/platform-impl/src/com/intellij/ui/jcef/HwFacadeJPanel.java create mode 100644 platform/platform-impl/src/com/intellij/ui/jcef/HwFacadeNonOpaquePanel.java diff --git a/platform/platform-impl/src/com/intellij/jdkEx/JdkEx.java b/platform/platform-impl/src/com/intellij/jdkEx/JdkEx.java index 8d9d9bff999e..0f585bfb4d54 100644 --- a/platform/platform-impl/src/com/intellij/jdkEx/JdkEx.java +++ b/platform/platform-impl/src/com/intellij/jdkEx/JdkEx.java @@ -106,4 +106,19 @@ public class JdkEx { return null; } } + + public static void setIgnoreMouseEvents(@NotNull Window window, boolean ignoreMouseEvents) { + if (SystemInfo.isJetBrainsJvm && SystemInfo.isMac) { + window.setEnabled(false); + try { + MethodInvocator invocator = + new MethodInvocator(false, Class.forName("java.awt.Window"), "setIgnoreMouseEvents", boolean.class); + if (invocator.isAvailable()) { + invocator.invoke(window, ignoreMouseEvents); + } + } + catch (ClassNotFoundException ignore) { + } + } + } } diff --git a/platform/platform-impl/src/com/intellij/ui/BalloonImpl.java b/platform/platform-impl/src/com/intellij/ui/BalloonImpl.java index 30808572d48d..160399a925fa 100644 --- a/platform/platform-impl/src/com/intellij/ui/BalloonImpl.java +++ b/platform/platform-impl/src/com/intellij/ui/BalloonImpl.java @@ -30,6 +30,8 @@ import com.intellij.openapi.wm.WeakFocusStackManager; import com.intellij.ui.awt.RelativePoint; import com.intellij.ui.components.panels.NonOpaquePanel; import com.intellij.ui.components.panels.Wrapper; +import com.intellij.ui.jcef.HwFacadeJPanel; +import com.intellij.ui.jcef.HwFacadeNonOpaquePanel; import com.intellij.ui.scale.JBUIScale; import com.intellij.util.Alarm; import com.intellij.util.Consumer; @@ -1634,7 +1636,7 @@ public final class BalloonImpl implements Balloon, IdeTooltip.Ui, ScreenAreaCons void layout(@NotNull Rectangle bounds); } - public class ActionButton extends NonOpaquePanel implements IdeGlassPane.TopComponent { + public class ActionButton extends HwFacadeNonOpaquePanel implements IdeGlassPane.TopComponent { private final Icon myIcon; private final Icon myHoverIcon; private final Consumer myListener; @@ -1697,7 +1699,7 @@ public final class BalloonImpl implements Balloon, IdeTooltip.Ui, ScreenAreaCons } } - private class MyComponent extends JPanel implements ComponentWithMnemonics { + private class MyComponent extends HwFacadeJPanel implements ComponentWithMnemonics { private BufferedImage myImage; private float myAlpha; diff --git a/platform/platform-impl/src/com/intellij/ui/jcef/HwFacadeHelper.java b/platform/platform-impl/src/com/intellij/ui/jcef/HwFacadeHelper.java new file mode 100644 index 000000000000..1ec156eb98ea --- /dev/null +++ b/platform/platform-impl/src/com/intellij/ui/jcef/HwFacadeHelper.java @@ -0,0 +1,215 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.ui.jcef; + +import com.intellij.jdkEx.JdkEx; +import com.intellij.util.FieldAccessor; +import org.cef.CefApp; +import org.cef.CefClient; +import org.cef.browser.CefBrowser; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import sun.awt.AWTAccessor; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.image.VolatileImage; +import java.util.*; +import java.util.List; +import java.util.function.Consumer; + +/** + * Provides a heavyweight window "facade" for a lightweight component. + * Used to workaround the problem of overlapping a heavyweight JCEF browser component. + * The "facade" means that a hw window follows the lifecycle of the target lw component + * (add/show/hide/dispose), follows its bounds and intercepts its painting. + * The hw window itself is non-focusable and mouse-transparent, so the target lw + * component handles mouse events as usual. + * + * @author tav + */ +public class HwFacadeHelper { + @SuppressWarnings("UseJBColor") + public static final Color TRANSPARENT_COLOR = new Color(1, 1, 1, 0); + + private final JComponent myTarget; + private Window myHwFacade; + private ComponentAdapter myOwnerListener; + private ComponentAdapter myTargetListener; + private VolatileImage myBackBuffer; + + // [tav] todo: export visible browser bounds from jcef instead + private static class JCEFAccessor { + private static FieldAccessor> clientsField; + private static FieldAccessor> browsersField; + private static CefApp ourCefApp; + + @Nullable + public static CefApp getCefApp() { + if (ourCefApp == null && CefApp.getState() != CefApp.CefAppState.NONE) { + ourCefApp = CefApp.getInstance(); + clientsField = new FieldAccessor<>(CefApp.class, "clients_"); + browsersField = new FieldAccessor<>(CefClient.class, "browser_"); + } + return ourCefApp; + } + + @NotNull + public static List getBrowsers() { + List list = new LinkedList<>(); + if (getCefApp() != null && clientsField.isAvailable() && browsersField.isAvailable()) { + Set clients = clientsField.get(ourCefApp); + if (clients != null) { + for (CefClient client : clients) { + HashMap browsers = browsersField.get(client); + if (browsers != null) { + list.addAll(browsers.values()); + } + } + } + } + return list; + } + } + + public HwFacadeHelper(JComponent target) { + myTarget = target; + } + + private boolean isActive() { + return myHwFacade != null; + } + + private static boolean isCefAppActive() { + return JCEFAccessor.getCefApp() != null; + } + + private void onShowing() { + if (!isCefAppActive()) return; + + assert myHwFacade == null; + assert myTarget.isVisible(); + + myTarget.addComponentListener(myTargetListener = new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + if (isActive()) { + myHwFacade.setSize(myTarget.getSize()); + } + else { + activateIfNeeded(); + } + } + @Override + public void componentMoved(ComponentEvent e) { + if (isActive()) { + if (myHwFacade.isVisible()) myHwFacade.setLocation(myTarget.getLocationOnScreen()); + } + else { + activateIfNeeded(); + } + } + }); + + activateIfNeeded(); + } + + private void activateIfNeeded() { + if (!isCefAppActive()) return; + + Rectangle targetBounds = new Rectangle(myTarget.getLocationOnScreen(), myTarget.getSize()); + boolean overlaps = false; + for (CefBrowser browser : JCEFAccessor.getBrowsers()) { + Component comp = browser.getUIComponent(); + if (comp != null && comp.isVisible() && comp.isShowing() && + new Rectangle(comp.getLocationOnScreen(), comp.getSize()).intersects(targetBounds)) + { + overlaps = true; + break; + } + } + if (overlaps) { + Window owner = SwingUtilities.getWindowAncestor(myTarget); + owner.addComponentListener(myOwnerListener = new ComponentAdapter() { + @Override + public void componentMoved(ComponentEvent e) { + if (myTarget.isVisible()) { + myHwFacade.setLocation(myTarget.getLocationOnScreen()); + } + } + }); + myHwFacade = new Window(owner) { + @Override + public void paint(Graphics g) { + super.paint(g); + if (myBackBuffer != null) { + g.drawImage(myBackBuffer, 0, 0, null); + } + } + }; + JdkEx.setIgnoreMouseEvents(myHwFacade, true); + myHwFacade.setBounds(targetBounds); + myHwFacade.setFocusableWindowState(false); + myHwFacade.setBackground(TRANSPARENT_COLOR); + myHwFacade.setVisible(true); + } + } + + public void addNotify() { + if (myTarget.isVisible()) { + onShowing(); + } + } + + public void show() { + if (!isCefAppActive()) return; + + if (AWTAccessor.getComponentAccessor().getPeer(myTarget) != null) { + if (isActive()) { + myHwFacade.setVisible(true); + } + else { + onShowing(); + } + } + } + + public void removeNotify() { + if (isActive()) { + myHwFacade.dispose(); + myHwFacade = null; + myBackBuffer = null; + myTarget.removeComponentListener(myTargetListener); + Window owner = SwingUtilities.getWindowAncestor(myTarget); + if (owner != null) owner.removeComponentListener(myOwnerListener); + } + } + + public void hide() { + if (isActive()) { + myHwFacade.setVisible(false); + } + } + + public void paint(Graphics g, Consumer targetPaint) { + if (isActive()) { + Dimension size = myTarget.getSize(); + if (myBackBuffer == null || myBackBuffer.getWidth() != size.width || myBackBuffer.getHeight() != size.height) { + myBackBuffer = GraphicsEnvironment. + getLocalGraphicsEnvironment(). + getDefaultScreenDevice(). + getDefaultConfiguration(). + createCompatibleVolatileImage(size.width, size.height, Transparency.TRANSLUCENT); + } + Graphics2D bbGraphics = (Graphics2D)myBackBuffer.getGraphics(); + bbGraphics.setBackground(TRANSPARENT_COLOR); + bbGraphics.clearRect(0, 0, size.width, size.height); + targetPaint.accept(bbGraphics); + myHwFacade.repaint(); + } + else { + targetPaint.accept(g); + } + } +} diff --git a/platform/platform-impl/src/com/intellij/ui/jcef/HwFacadeJPanel.java b/platform/platform-impl/src/com/intellij/ui/jcef/HwFacadeJPanel.java new file mode 100644 index 000000000000..1a5b25c204b8 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/ui/jcef/HwFacadeJPanel.java @@ -0,0 +1,46 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.ui.jcef; + +import javax.swing.*; +import java.awt.*; + +/** + * A heavyweight facade for {@link JPanel}. + * + * @see HwFacadeHelper + * @author tav + */ +public class HwFacadeJPanel extends JPanel { + private final HwFacadeHelper myHwFacadeHelper = new HwFacadeHelper(this); + + @Override + public void addNotify() { + super.addNotify(); + myHwFacadeHelper.addNotify(); + } + + @Override + public void removeNotify() { + super.removeNotify(); + myHwFacadeHelper.removeNotify(); + } + + @SuppressWarnings("deprecation") + @Override + public void show() { + super.show(); + myHwFacadeHelper.show(); + } + + @SuppressWarnings("deprecation") + @Override + public void hide() { + super.hide(); + myHwFacadeHelper.hide(); + } + + @Override + public void paint(Graphics g) { + myHwFacadeHelper.paint(g, (gg) -> super.paint(gg)); + } +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/ui/jcef/HwFacadeNonOpaquePanel.java b/platform/platform-impl/src/com/intellij/ui/jcef/HwFacadeNonOpaquePanel.java new file mode 100644 index 000000000000..f548a4694a96 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/ui/jcef/HwFacadeNonOpaquePanel.java @@ -0,0 +1,47 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.ui.jcef; + +import com.intellij.ui.components.panels.NonOpaquePanel; + +import java.awt.*; + +/** + * A heavyweight facade for {@link NonOpaquePanel}. + * + * @see HwFacadeHelper + * @author tav + */ +public class HwFacadeNonOpaquePanel extends NonOpaquePanel { + private final HwFacadeHelper myHwFacadeHelper = new HwFacadeHelper(this); + + @Override + public void addNotify() { + super.addNotify(); + myHwFacadeHelper.addNotify(); + } + + @Override + public void removeNotify() { + super.removeNotify(); + myHwFacadeHelper.removeNotify(); + } + + @SuppressWarnings("deprecation") + @Override + public void show() { + super.show(); + myHwFacadeHelper.show(); + } + + @SuppressWarnings("deprecation") + @Override + public void hide() { + super.hide(); + myHwFacadeHelper.hide(); + } + + @Override + public void paint(Graphics g) { + myHwFacadeHelper.paint(g, (gg) -> super.paint(gg)); + } +}