Added ScaleContext.overrideScale (and IconUtil.overrideScale)

- added the case to IconScaleTest
- added CopyableIcon.deepCopy (and IconUtil.deepCopy), implemented in LayeredIcon, RowIcon
This commit is contained in:
Anton Tarasov
2019-03-07 16:52:52 +03:00
parent 3e118e4af4
commit 857e6cbf4c
7 changed files with 148 additions and 28 deletions
@@ -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<LayeredIcon> 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) {
@@ -65,6 +65,16 @@ public class RowIcon extends JBCachingScalableIcon<RowIcon> 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) {
@@ -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);
@@ -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<BufferedImage, Graphics2D> 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";
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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<UpdateListener> listeners;
private EnumSet<ScaleType> 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:
* <p>
* <code>
* ((ScaleContextAware)uiObject).getScaleContext().overrideScale(USR_SCALE.of(1.0));
* </code>
*
* @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;