From 857e6cbf4c15443db4a8712cfca47a3bf51cc2a9 Mon Sep 17 00:00:00 2001 From: Anton Tarasov Date: Thu, 7 Mar 2019 16:31:05 +0300 Subject: [PATCH] Added ScaleContext.overrideScale (and IconUtil.overrideScale) - added the case to IconScaleTest - added CopyableIcon.deepCopy (and IconUtil.deepCopy), implemented in LayeredIcon, RowIcon --- .../src/com/intellij/ui/LayeredIcon.java | 11 +++ .../core-api/src/com/intellij/ui/RowIcon.java | 10 +++ .../src/com/intellij/util/IconUtil.java | 32 ++++++++- .../com/intellij/util/ui/IconScaleTest.java | 67 ++++++++++++------- .../intellij/openapi/util/CopyableIcon.java | 8 +++ .../com/intellij/openapi/util/IconLoader.java | 8 +-- .../src/com/intellij/util/ui/JBUIScale.java | 40 +++++++++++ 7 files changed, 148 insertions(+), 28 deletions(-) diff --git a/platform/core-api/src/com/intellij/ui/LayeredIcon.java b/platform/core-api/src/com/intellij/ui/LayeredIcon.java index 98b5fbc527d1..ecfd13781e22 100644 --- a/platform/core-api/src/com/intellij/ui/LayeredIcon.java +++ b/platform/core-api/src/com/intellij/ui/LayeredIcon.java @@ -19,6 +19,7 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.IconLoader; import com.intellij.openapi.util.IconLoader.DarkIconProvider; import com.intellij.util.ArrayUtil; +import com.intellij.util.IconUtil; import com.intellij.util.ui.JBCachingScalableIcon; import org.intellij.lang.annotations.MagicConstant; import org.jetbrains.annotations.NotNull; @@ -82,6 +83,16 @@ public class LayeredIcon extends JBCachingScalableIcon implements D return new LayeredIcon(this); } + @NotNull + @Override + public LayeredIcon deepCopy() { + LayeredIcon icon = new LayeredIcon(this); + for (int i = 0; i < icon.myIcons.length; i++) { + icon.myIcons[i] = IconUtil.copy(icon.myIcons[i], null); + } + return icon; + } + @NotNull private Icon[] myScaledIcons() { if (myScaledIcons != null) { diff --git a/platform/core-api/src/com/intellij/ui/RowIcon.java b/platform/core-api/src/com/intellij/ui/RowIcon.java index 14751e397d8c..00385a261b9c 100644 --- a/platform/core-api/src/com/intellij/ui/RowIcon.java +++ b/platform/core-api/src/com/intellij/ui/RowIcon.java @@ -65,6 +65,16 @@ public class RowIcon extends JBCachingScalableIcon implements DarkIconP return new RowIcon(this); } + @NotNull + @Override + public RowIcon deepCopy() { + RowIcon icon = new RowIcon(this); + for (int i = 0; i < icon.myIcons.length; i++) { + icon.myIcons[i] = IconUtil.copy(icon.myIcons[i], null); + } + return icon; + } + @NotNull private Icon[] myScaledIcons() { if (myScaledIcons != null) { diff --git a/platform/core-api/src/com/intellij/util/IconUtil.java b/platform/core-api/src/com/intellij/util/IconUtil.java index b578f7ba5301..1324e9967b02 100644 --- a/platform/core-api/src/com/intellij/util/IconUtil.java +++ b/platform/core-api/src/com/intellij/util/IconUtil.java @@ -458,7 +458,17 @@ public class IconUtil { */ @Contract("null, _->null; !null, _->!null") public static Icon copy(@Nullable Icon icon, @Nullable Component ancestor) { - return IconLoader.copy(icon, ancestor); + return IconLoader.copy(icon, ancestor, false); + } + + /** + * Returns a deep copy of the provided {@code icon}. + * + * @see CopyableIcon + */ + @Contract("null, _->null; !null, _->!null") + public static Icon deepCopy(@Nullable Icon icon, @Nullable Component ancestor) { + return IconLoader.copy(icon, ancestor, true); } /** @@ -521,6 +531,26 @@ public class IconUtil { return scale(icon, ancestor, scale); } + /** + * Overrides the provided scale in the icon's scale context and in the composited icon's scale contexts (when applicable). + * + * @see JBUIScale.UserScaleContext#overrideScale(JBUIScale.Scale) + */ + @NotNull + public static Icon overrideScale(@NotNull Icon icon, JBUIScale.Scale scale) { + if (icon instanceof CompositeIcon) { + CompositeIcon compositeIcon = (CompositeIcon)icon; + for (int i = 0; i < compositeIcon.getIconCount(); i++) { + Icon subIcon = compositeIcon.getIcon(i); + if (subIcon != null) overrideScale(subIcon, scale); + } + } + if (icon instanceof ScaleContextAware) { + ((ScaleContextAware)icon).getScaleContext().overrideScale(scale); + } + return icon; + } + @NotNull public static Icon colorize(@NotNull Icon source, @NotNull Color color) { return colorize(source, color, false); diff --git a/platform/platform-tests/testSrc/com/intellij/util/ui/IconScaleTest.java b/platform/platform-tests/testSrc/com/intellij/util/ui/IconScaleTest.java index 2bb188102fdf..7d554ee0b6b3 100644 --- a/platform/platform-tests/testSrc/com/intellij/util/ui/IconScaleTest.java +++ b/platform/platform-tests/testSrc/com/intellij/util/ui/IconScaleTest.java @@ -6,14 +6,9 @@ import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.SystemInfo; import com.intellij.testFramework.PlatformTestUtil; import com.intellij.testFramework.fixtures.BareTestFixtureTestCase; -import com.intellij.ui.DeferredIconImpl; -import com.intellij.ui.LayeredIcon; -import com.intellij.ui.RestoreScaleRule; -import com.intellij.ui.RowIcon; +import com.intellij.ui.*; import com.intellij.util.IconUtil; -import com.intellij.util.ui.JBUIScale.UserScaleContext; -import com.intellij.util.ui.JBUIScale.ScaleContext; -import com.intellij.util.ui.JBUIScale.ScaleContextAware; +import com.intellij.util.ui.JBUIScale.*; import com.intellij.util.ui.paint.ImageComparator; import org.junit.BeforeClass; import org.junit.ClassRule; @@ -28,8 +23,7 @@ import java.net.MalformedURLException; import static com.intellij.util.ui.JBUIScale.DerivedScaleType.DEV_SCALE; import static com.intellij.util.ui.JBUIScale.DerivedScaleType.EFF_USR_SCALE; -import static com.intellij.util.ui.JBUIScale.ScaleType.SYS_SCALE; -import static com.intellij.util.ui.JBUIScale.ScaleType.USR_SCALE; +import static com.intellij.util.ui.JBUIScale.ScaleType.*; import static com.intellij.util.ui.TestScaleHelper.*; /** @@ -40,6 +34,7 @@ import static com.intellij.util.ui.TestScaleHelper.*; public class IconScaleTest extends BareTestFixtureTestCase { private static final int ICON_BASE_SIZE = 16; private static final float ICON_OBJ_SCALE = 1.75f; + private static final float ICON_OVER_USR_SCALE = 1.0f; @ClassRule public static final ExternalResource manageState = new RestoreScaleRule(); @@ -97,33 +92,51 @@ public class IconScaleTest extends BareTestFixtureTestCase { test(new RowIcon(new CachedImageIcon(new File(getIconPath()).toURI().toURL())), UserScaleContext.create(ctx)); } - private static void test(Icon icon, UserScaleContext bctx) { - ((ScaleContextAware)icon).updateScaleContext(bctx); + private static void test(Icon icon, UserScaleContext iconCtx) { + ((ScaleContextAware)icon).updateScaleContext(iconCtx); - ScaleContext ctx = ScaleContext.create(bctx); + ScaleContext ctx = ScaleContext.create(iconCtx); + /* + * (A) normal conditions + */ + + //noinspection UnnecessaryLocalVariable + Icon iconA = icon; double usrSize2D = ctx.apply(ICON_BASE_SIZE, EFF_USR_SCALE); int usrSize = (int)Math.round(usrSize2D); int devSize = (int)Math.round(ctx.apply(usrSize2D, DEV_SCALE)); - assertEquals("unexpected icon user width " + bctx, usrSize, icon.getIconWidth()); - assertEquals("unexpected icon user height " + bctx, usrSize, icon.getIconHeight()); - assertEquals("unexpected icon real width " + bctx, devSize, ImageUtil.getRealWidth(IconUtil.toImage(icon, ctx))); - assertEquals("unexpected icon real height " + bctx, devSize, ImageUtil.getRealHeight(IconUtil.toImage(icon, ctx))); + assertIcon(iconA, iconCtx, usrSize, devSize); - Icon scaledIcon = IconUtil.scale(icon, null, ICON_OBJ_SCALE); + /* + * (B) override scale + */ + if (!(icon instanceof RetrievableIcon)) { // RetrievableIcon may return a copy of its wrapped icon and we may fail to override scale in the origin. - assertNotSame("scaled instance of the icon " + bctx, icon, scaledIcon); - assertEquals("ScaleContext of the original icon changed " + bctx, bctx, ((ScaleContextAware)icon).getScaleContext()); + Icon iconB = IconUtil.overrideScale(IconUtil.deepCopy(icon, null), USR_SCALE.of(ICON_OVER_USR_SCALE)); + usrSize2D = ICON_BASE_SIZE * ICON_OVER_USR_SCALE * ctx.getScale(OBJ_SCALE); + usrSize = (int)Math.round(usrSize2D); + devSize = (int)Math.round(ctx.apply(usrSize2D, DEV_SCALE)); + + assertIcon(iconB, iconCtx, usrSize, devSize); + } + + /* + * (C) scale icon + */ + Icon iconC = IconUtil.scale(icon, null, ICON_OBJ_SCALE); + + assertNotSame("scaled instance of the icon " + iconCtx, icon, iconC); + assertEquals("ScaleContext of the original icon changed " + iconCtx, iconCtx, ((ScaleContextAware)icon).getScaleContext()); + + usrSize2D = ctx.apply(ICON_BASE_SIZE, EFF_USR_SCALE); double scaledUsrSize2D = usrSize2D * ICON_OBJ_SCALE; int scaledUsrSize = (int)Math.round(scaledUsrSize2D); int scaledDevSize = (int)Math.round(ctx.apply(scaledUsrSize2D, DEV_SCALE)); - assertEquals("unexpected scaled icon user width " + bctx, scaledUsrSize, scaledIcon.getIconWidth()); - assertEquals("unexpected scaled icon user height " + bctx, scaledUsrSize, scaledIcon.getIconHeight()); - assertEquals("unexpected scaled icon real width " + bctx, scaledDevSize, ImageUtil.getRealWidth(IconUtil.toImage(scaledIcon, ctx))); - assertEquals("unexpected scaled icon real height " + bctx, scaledDevSize, ImageUtil.getRealHeight(IconUtil.toImage(scaledIcon, ctx))); + assertIcon(iconC, iconCtx, scaledUsrSize, scaledDevSize); // Additionally check that the original image hasn't changed after scaling Pair pair = createImageAndGraphics(ctx.getScale(DEV_SCALE), icon.getIconWidth(), icon.getIconHeight()); @@ -138,6 +151,14 @@ public class IconScaleTest extends BareTestFixtureTestCase { new ImageComparator.AASmootherComparator(0.1, 0.1, new Color(0, 0, 0, 0)), goldImage, iconImage, null); } + static void assertIcon(Icon icon, UserScaleContext iconCtx, int usrSize, int devSize) { + ScaleContext ctx = ScaleContext.create(iconCtx); + assertEquals("unexpected icon user width (ctx: " + iconCtx + ")", usrSize, icon.getIconWidth()); + assertEquals("unexpected icon user height (ctx: " + iconCtx + ")", usrSize, icon.getIconHeight()); + assertEquals("unexpected icon real width (ctx: " + iconCtx + ")", devSize, ImageUtil.getRealWidth(IconUtil.toImage(icon, ctx))); + assertEquals("unexpected icon real height (ctx: " + iconCtx + ")", devSize, ImageUtil.getRealHeight(IconUtil.toImage(icon, ctx))); + } + private static String getIconPath() { return PlatformTestUtil.getPlatformTestDataPath() + "ui/abstractClass.svg"; } diff --git a/platform/util/src/com/intellij/openapi/util/CopyableIcon.java b/platform/util/src/com/intellij/openapi/util/CopyableIcon.java index 14e135e60bb4..b7271a331a41 100644 --- a/platform/util/src/com/intellij/openapi/util/CopyableIcon.java +++ b/platform/util/src/com/intellij/openapi/util/CopyableIcon.java @@ -17,4 +17,12 @@ public interface CopyableIcon extends Icon { */ @NotNull Icon copy(); + + /** + * Returns a copy of this icon (see {@link #copy()}) trying to deep-copy composited icons when applicable. + */ + @NotNull + default Icon deepCopy() { + return copy(); + } } diff --git a/platform/util/src/com/intellij/openapi/util/IconLoader.java b/platform/util/src/com/intellij/openapi/util/IconLoader.java index 87d46f562c67..008b7b86cce9 100644 --- a/platform/util/src/com/intellij/openapi/util/IconLoader.java +++ b/platform/util/src/com/intellij/openapi/util/IconLoader.java @@ -324,11 +324,11 @@ public final class IconLoader { } } - @Contract("null, _->null; !null, _->!null") - public static Icon copy(@Nullable Icon icon, @Nullable Component ancestor) { + @Contract("null, _, _->null; !null, _, _->!null") + public static Icon copy(@Nullable Icon icon, @Nullable Component ancestor, boolean deepCopy) { if (icon == null) return null; if (icon instanceof CopyableIcon) { - return ((CopyableIcon)icon).copy(); + return deepCopy ? ((CopyableIcon)icon).deepCopy() : ((CopyableIcon)icon).copy(); } BufferedImage image = UIUtil.createImage(ancestor, icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D g = image.createGraphics(); @@ -941,7 +941,7 @@ public final class IconLoader { @NotNull @Override public Icon copy() { - return IconLoader.copy(getOrComputeIcon(), null); + return IconLoader.copy(getOrComputeIcon(), null, false); } } diff --git a/platform/util/src/com/intellij/util/ui/JBUIScale.java b/platform/util/src/com/intellij/util/ui/JBUIScale.java index a8689c510fcf..f4f70dd3b0cd 100644 --- a/platform/util/src/com/intellij/util/ui/JBUIScale.java +++ b/platform/util/src/com/intellij/util/ui/JBUIScale.java @@ -11,6 +11,7 @@ import java.awt.image.ImageObserver; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.EnumMap; +import java.util.EnumSet; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; @@ -236,6 +237,7 @@ public class JBUIScale { } } + // [tav] todo: [User]ScaleContext is thread-unsafe, should it be thread-safe? /** * Represents a snapshot of the user space scale factors: {@link ScaleType#USR_SCALE} and {@link ScaleType#OBJ_SCALE}). * The context can be associated with a UI object (see {@link ScaleContextAware}) to define its HiDPI behaviour. @@ -250,6 +252,7 @@ public class JBUIScale { protected double pixScale = usrScale.value; private List listeners; + private EnumSet overriddenScales; protected UserScaleContext() { } @@ -294,14 +297,49 @@ public class JBUIScale { return usrScale.value * objScale.value; } + /** + * Permanently overrides the provided scale (the scale won't be changed on subsequent {@link #update()}). + * Can be used to make a UI object user scale independent: + *

+ * + * ((ScaleContextAware)uiObject).getScaleContext().overrideScale(USR_SCALE.of(1.0)); + * + * + * @param scale the new scale to override + * @return whether the new scale updated the current value + * @see ScaleType#of(double) + */ + public boolean overrideScale(@NotNull Scale scale) { + if (overriddenScales != null) { + overriddenScales.remove(scale.type); // previous override should not prevent this override + } + boolean updated = setScale(scale); + + if (overriddenScales == null) { + overriddenScales = EnumSet.of(scale.type); + } + else { + overriddenScales.add(scale.type); + } + return updated; + } + + protected boolean isScaleOverridden(@NotNull Scale scale) { + return overriddenScales != null && overriddenScales.contains(scale.type); + } + /** * Sets the new scale (system scale is ignored). Use {@link ScaleType#of(double)} to provide the new scale. + * Note, the new scale value can be change on subsequent {@link #update()}. Use {@link #overrideScale(Scale)} + * to set a scale permanently. * * @param scale the new scale to set * @return whether the new scale updated the current value * @see ScaleType#of(double) */ public boolean setScale(@NotNull Scale scale) { + if (isScaleOverridden(scale)) return false; + boolean updated = false; switch (scale.type) { case USR_SCALE: @@ -618,6 +656,8 @@ public class JBUIScale { */ @Override public boolean setScale(@NotNull Scale scale) { + if (isScaleOverridden(scale)) return false; + if (scale.type == SYS_SCALE) { boolean updated = !sysScale.equals(scale); sysScale = scale;