From cd2e4747affa43c1707367b1358cc7468f017474 Mon Sep 17 00:00:00 2001 From: weisj <31143295+weisJ@users.noreply.github.com> Date: Sat, 6 Mar 2021 01:50:58 +0100 Subject: [PATCH] LafInstallation: Add theme transition animation when installing the laf using the LafManager. This can be disabled by setting the "darklaf.animatedLafChange" system property to false. --- .../github/weisj/darklaf/LafInstaller.java | 88 +++++++++ .../com/github/weisj/darklaf/LafManager.java | 41 ++--- .../github/weisj/darklaf/LafTransition.java | 166 +++++++++++++++++ .../src/test/java/test/AbstractImageTest.java | 7 +- .../github/weisj/darklaf/util/ImageUtil.java | 87 +++++++-- .../darklaf/util/graphics/ImagePainter.java | 168 ++++++++++++++++++ .../darklaf/util/graphics/ScaledImage.java | 78 ++++++++ .../weisj/darklaf/util/value/Shared.java | 38 ++++ .../darklaf/util/value/SharedNonNull.java | 36 ++++ 9 files changed, 660 insertions(+), 49 deletions(-) create mode 100644 core/src/main/java/com/github/weisj/darklaf/LafInstaller.java create mode 100644 core/src/main/java/com/github/weisj/darklaf/LafTransition.java create mode 100644 utils/src/main/java/com/github/weisj/darklaf/util/graphics/ImagePainter.java create mode 100644 utils/src/main/java/com/github/weisj/darklaf/util/graphics/ScaledImage.java create mode 100644 utils/src/main/java/com/github/weisj/darklaf/util/value/Shared.java create mode 100644 utils/src/main/java/com/github/weisj/darklaf/util/value/SharedNonNull.java diff --git a/core/src/main/java/com/github/weisj/darklaf/LafInstaller.java b/core/src/main/java/com/github/weisj/darklaf/LafInstaller.java new file mode 100644 index 00000000..765f5182 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/LafInstaller.java @@ -0,0 +1,88 @@ +/* + * MIT License + * + * Copyright (c) 2021 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.github.weisj.darklaf; + +import java.awt.Window; +import java.util.logging.Logger; + +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; + +import com.github.weisj.darklaf.theme.Theme; +import com.github.weisj.darklaf.theme.event.ThemeChangeEvent; +import com.github.weisj.darklaf.theme.event.ThemeChangeListener; +import com.github.weisj.darklaf.theme.event.ThemeEventSupport; +import com.github.weisj.darklaf.util.LogUtil; + +final class LafInstaller { + + private static final Logger LOGGER = LogUtil.getLogger(LafManager.class); + private static final ThemeEventSupport eventSupport = + new ThemeEventSupport<>(); + + void install(final Theme theme) { + try { + LOGGER.fine(() -> "Installing theme " + theme); + LafTransition transition = LafTransition.showSnapshot(); + UIManager.setLookAndFeel(new DarkLaf(false)); + updateLaf(); + SwingUtilities.invokeLater(transition::runTransition); + notifyThemeInstalled(theme); + } catch (final UnsupportedLookAndFeelException e) { + e.printStackTrace(); + } + } + + void updateLaf() { + for (final Window w : Window.getWindows()) { + updateLafRecursively(w); + } + } + + private void updateLafRecursively(final Window window) { + for (final Window childWindow : window.getOwnedWindows()) { + updateLafRecursively(childWindow); + } + SwingUtilities.updateComponentTreeUI(window); + } + + void notifyThemeInstalled(final Theme newTheme) { + eventSupport.dispatchEvent(new ThemeChangeEvent(null, newTheme), ThemeChangeListener::themeInstalled); + } + + void addThemeChangeListener(final ThemeChangeListener listener) { + eventSupport.addListener(listener); + } + + void removeThemeChangeListener(final ThemeChangeListener listener) { + eventSupport.removeListener(listener); + } + + + void notifyThemeChanged(final Theme oldTheme, final Theme newTheme) { + if (oldTheme != newTheme) { + eventSupport.dispatchEvent(new ThemeChangeEvent(oldTheme, newTheme), ThemeChangeListener::themeChanged); + LOGGER.fine(() -> "Setting theme to " + newTheme); + } + } +} diff --git a/core/src/main/java/com/github/weisj/darklaf/LafManager.java b/core/src/main/java/com/github/weisj/darklaf/LafManager.java index f2126a25..818147e6 100644 --- a/core/src/main/java/com/github/weisj/darklaf/LafManager.java +++ b/core/src/main/java/com/github/weisj/darklaf/LafManager.java @@ -51,13 +51,12 @@ import com.github.weisj.darklaf.util.LogUtil; public final class LafManager { private static final Logger LOGGER = LogUtil.getLogger(LafManager.class); + private static final LafInstaller installer = new LafInstaller(); private static ThemeProvider themeProvider; private static Theme theme; private static final List registeredThemes = new ArrayList<>(); private static final Collection uiDefaultsTasks = new ArrayList<>(); private static final Collection uiInitTasks = new ArrayList<>(); - private static final ThemeEventSupport eventSupport = - new ThemeEventSupport<>(); static { setLogLevel(Level.WARNING); @@ -179,7 +178,7 @@ public final class LafManager { * @param listener the listener to add. */ public static void addThemeChangeListener(final ThemeChangeListener listener) { - eventSupport.addListener(listener); + installer.addThemeChangeListener(listener); } /** @@ -188,7 +187,7 @@ public final class LafManager { * @param listener the listener to add. */ public static void removeThemeChangeListener(final ThemeChangeListener listener) { - eventSupport.removeListener(listener); + installer.removeThemeChangeListener(listener); } /** @@ -373,10 +372,7 @@ public final class LafManager { public static void setTheme(final Theme theme) { Theme old = LafManager.theme; LafManager.theme = theme; - if (old != theme) { - eventSupport.dispatchEvent(new ThemeChangeEvent(old, theme), ThemeChangeListener::themeChanged); - LOGGER.fine(() -> "Setting theme to " + theme); - } + installer.notifyThemeChanged(old, theme); if (ThemeSettings.isInitialized()) ThemeSettings.getInstance().refresh(); } @@ -436,33 +432,12 @@ public final class LafManager { * {@link ThemeProvider}. This sets the current LaF and applies the given theme. */ public static void install() { - try { - getTheme(); - LOGGER.fine(() -> "Installing theme " + theme); - UIManager.setLookAndFeel(new DarkLaf()); - updateLaf(); - notifyThemeInstalled(); - } catch (final UnsupportedLookAndFeelException e) { - e.printStackTrace(); - } - } - - /* default */ static void notifyThemeInstalled() { - eventSupport.dispatchEvent(new ThemeChangeEvent(null, getTheme()), ThemeChangeListener::themeInstalled); + installer.install(getTheme()); } /** Update the component ui classes for all current windows. */ public static void updateLaf() { - for (final Window w : Window.getWindows()) { - updateLafRecursively(w); - } - } - - private static void updateLafRecursively(final Window window) { - for (final Window childWindow : window.getOwnedWindows()) { - updateLafRecursively(childWindow); - } - SwingUtilities.updateComponentTreeUI(window); + installer.updateLaf(); } /** @@ -537,4 +512,8 @@ public final class LafManager { } return themeForPreferredStyle(null); } + + static void notifyThemeInstalled() { + installer.notifyThemeInstalled(getTheme()); + } } diff --git a/core/src/main/java/com/github/weisj/darklaf/LafTransition.java b/core/src/main/java/com/github/weisj/darklaf/LafTransition.java new file mode 100644 index 00000000..15ab89f1 --- /dev/null +++ b/core/src/main/java/com/github/weisj/darklaf/LafTransition.java @@ -0,0 +1,166 @@ +/* + * MIT License + * + * Copyright (c) 2021 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.github.weisj.darklaf; + + + +import java.awt.AlphaComposite; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.Window; +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.swing.JComponent; +import javax.swing.JLayeredPane; +import javax.swing.RootPaneContainer; + +import com.github.weisj.darklaf.graphics.Animator; +import com.github.weisj.darklaf.graphics.DefaultInterpolator; +import com.github.weisj.darklaf.util.ImageUtil; +import com.github.weisj.darklaf.util.PropertyUtil; +import com.github.weisj.darklaf.util.graphics.ImagePainter; +import com.github.weisj.darklaf.util.value.SharedNonNull; + +class LafTransition { + + private static final String ANIMATED_LAF_CHANGE = DarkLaf.SYSTEM_PROPERTY_PREFIX + "animatedLafChange"; + + private LafTransition() {} + + static LafTransition showSnapshot() { + return PropertyUtil.getSystemFlag(ANIMATED_LAF_CHANGE) + ? new AnimatedLafTransition() + : new LafTransition(); + } + + void runTransition() { + // Do nothing. + } + + static final class AnimatedLafTransition extends LafTransition { + + private final Animator animator; + private final Map uiSnapshots; + private final SharedNonNull sharedAlpha; + + private AnimatedLafTransition() { + sharedAlpha = new SharedNonNull<>(1f); + animator = new TransitionAnimator(); + uiSnapshots = new LinkedHashMap<>(); + Window[] windows = Window.getWindows(); + for (Window window : windows) { + if (window instanceof RootPaneContainer && window.isShowing()) { + RootPaneContainer rootPaneContainer = (RootPaneContainer) window; + Image img = ImageUtil.scaledImageFromComponent(rootPaneContainer.getRootPane()); + JLayeredPane layeredPane = rootPaneContainer.getLayeredPane(); + JComponent imageLayer = new ImageLayer(layeredPane, img, sharedAlpha); + imageLayer.setSize(layeredPane.getSize()); + layeredPane.add(imageLayer, JLayeredPane.DRAG_LAYER); + uiSnapshots.put(layeredPane, imageLayer); + } + } + doPaint(); + } + + void runTransition() { + animator.resume(); + } + + private void disposeSnapshots() { + for (Map.Entry entry : uiSnapshots.entrySet()) { + entry.getKey().remove(entry.getValue()); + entry.getKey().revalidate(); + entry.getKey().repaint(); + } + uiSnapshots.clear(); + } + + private void doPaint() { + for (Map.Entry entry : uiSnapshots.entrySet()) { + if (entry.getKey().isShowing()) { + entry.getValue().revalidate(); + entry.getValue().repaint(); + } + } + } + + private class TransitionAnimator extends Animator { + + private static final int DURATION = 160; + private static final int RESOLUTION = 10; + + public TransitionAnimator() { + super(DURATION / RESOLUTION, DURATION, false, DefaultInterpolator.EASE_IN_SINE); + } + + @Override + public void resume() { + doPaint(); + super.resume(); + } + + @Override + public void paintNow(final float fraction) { + sharedAlpha.set(1f - fraction); + doPaint(); + } + + @Override + protected void paintCycleEnd() { + disposeSnapshots(); + } + } + } + + private static class ImageLayer extends JComponent { + + private final JLayeredPane layeredPane; + private final SharedNonNull sharedAlpha; + private final Image image; + + private ImageLayer(final JLayeredPane layeredPane, final Image image, final SharedNonNull sharedAlpha) { + this.layeredPane = layeredPane; + this.image = image; + this.sharedAlpha = sharedAlpha; + } + + @Override + public void updateUI() {} + + @Override + public void paint(final Graphics g) { + Graphics gg = g.create(); + ((Graphics2D) gg).setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, sharedAlpha.get())); + ImagePainter.drawImage(gg, image, 0, 0, this); + } + + @Override + public Rectangle getBounds() { + return layeredPane.getBounds(); + } + } + +} diff --git a/core/src/test/java/test/AbstractImageTest.java b/core/src/test/java/test/AbstractImageTest.java index a53b0922..1d6f4aa4 100644 --- a/core/src/test/java/test/AbstractImageTest.java +++ b/core/src/test/java/test/AbstractImageTest.java @@ -31,6 +31,7 @@ import javax.imageio.ImageIO; import com.github.weisj.darklaf.util.ImageUtil; import com.github.weisj.darklaf.util.Scale; +import com.github.weisj.darklaf.util.graphics.ScaledImage; abstract class AbstractImageTest { private static final int SCALING_FACTOR = 3; @@ -62,9 +63,9 @@ abstract class AbstractImageTest { File file = new File(name + ".png"); file.getParentFile().mkdirs(); Rectangle rect = new Rectangle(0, 0, c.getWidth(), c.getHeight()); - BufferedImage image = ImageUtil.scaledImageFromComponent(c, rect, scalingFactor, scalingFactor, false); - ImageIO.write(image, "png", file); - return image; + ScaledImage image = ImageUtil.scaledImageFromComponent(c, rect, scalingFactor, scalingFactor, false); + ImageIO.write(image.getDelegate(), "png", file); + return image.getDelegate(); } catch (final IOException e) { e.printStackTrace(); } diff --git a/utils/src/main/java/com/github/weisj/darklaf/util/ImageUtil.java b/utils/src/main/java/com/github/weisj/darklaf/util/ImageUtil.java index a1e1bbda..d5ef97f4 100644 --- a/utils/src/main/java/com/github/weisj/darklaf/util/ImageUtil.java +++ b/utils/src/main/java/com/github/weisj/darklaf/util/ImageUtil.java @@ -24,11 +24,25 @@ package com.github.weisj.darklaf.util; import java.awt.*; import java.awt.image.BufferedImage; +import com.github.weisj.darklaf.util.graphics.GraphicsUtil; +import com.github.weisj.darklaf.util.graphics.ScaledImage; + + /** @author Jannis Weis */ public final class ImageUtil { private ImageUtil() {} + /** + * Create image from component. + * + * @param c the component. + * @return image of the component. + */ + public static ScaledImage imageFromComponent(final Component c) { + return scaledImageFromComponent(c, new Rectangle(0, 0, c.getWidth(), c.getHeight()), 1.0, 1.0, true); + } + /** * Create image from component. * @@ -36,7 +50,7 @@ public final class ImageUtil { * @param bounds the bounds inside the component to capture. * @return image containing the captured area. */ - public static BufferedImage imageFromComponent(final Component c, final Rectangle bounds) { + public static ScaledImage imageFromComponent(final Component c, final Rectangle bounds) { return scaledImageFromComponent(c, bounds, 1.0, 1.0, true); } @@ -47,10 +61,20 @@ public final class ImageUtil { * @param bounds the bounds inside the component to capture. * @return image containing the captured area. */ - public static BufferedImage imageFromComponent(final Component c, final Rectangle bounds, final boolean print) { + public static ScaledImage imageFromComponent(final Component c, final Rectangle bounds, final boolean print) { return scaledImageFromComponent(c, bounds, 1.0, 1.0, print); } + /** + * Create image from component. + * + * @param c the component. + * @return image of the component. + */ + public static ScaledImage scaledImageFromComponent(final Component c) { + return scaledImageFromComponent(c, new Rectangle(0, 0, c.getWidth(), c.getHeight()), false); + } + /** * Create image from component. * @@ -58,9 +82,22 @@ public final class ImageUtil { * @param bounds the bounds inside the component to capture. * @return image containing the captured area. */ - public static BufferedImage scaledImageFromComponent(final Component c, final Rectangle bounds) { + public static ScaledImage scaledImageFromComponent(final Component c, final Rectangle bounds) { + return scaledImageFromComponent(c, bounds, false); + } + + /** + * Create image from component. + * + * @param c the component. + * @param bounds the bounds inside the component to capture. + * @param print whether the component should be painted in printing mode or not. + * @return image containing the captured area. + */ + public static ScaledImage scaledImageFromComponent(final Component c, final Rectangle bounds, + final boolean print) { GraphicsConfiguration gc = c.getGraphicsConfiguration(); - return scaledImageFromComponent(c, bounds, Scale.getScaleX(gc), Scale.getScaleY(gc), true); + return scaledImageFromComponent(c, bounds, Scale.getScaleX(gc), Scale.getScaleY(gc), print); } /** @@ -72,16 +109,19 @@ public final class ImageUtil { * @param scaley the y scale * @return image containing the captured area. */ - public static BufferedImage scaledImageFromComponent(final Component c, final Rectangle bounds, final double scalex, + public static ScaledImage scaledImageFromComponent(final Component c, final Rectangle bounds, final double scalex, final double scaley, final boolean print) { BufferedImage image; boolean scale = scalex != 1.0 || scaley != 1.0; if (scale) { - image = createCompatibleTransparentImage((int) (scalex * bounds.width), (int) (scaley * bounds.height)); + image = createCompatibleTransparentImage(c.getGraphicsConfiguration(), + (int) (scalex * bounds.width), (int) (scaley * bounds.height)); } else { - image = createCompatibleTransparentImage(bounds.width, bounds.height); + image = createCompatibleTransparentImage(c.getGraphicsConfiguration(), + bounds.width, bounds.height); } final Graphics2D g2d = (Graphics2D) image.getGraphics(); + GraphicsUtil.setupAntialiasing(g2d); if (scale) { g2d.scale(scalex, scaley); } @@ -93,22 +133,37 @@ public final class ImageUtil { } g2d.dispose(); - return image; + return new ScaledImage(image, scalex, scaley); + } + + public static BufferedImage createCompatibleImage(final GraphicsConfiguration gc, + final int width, final int height) { + return gc == null ? new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB) + : gc.createCompatibleImage(width, height, Transparency.OPAQUE); } public static BufferedImage createCompatibleImage(final int width, final int height) { - return isHeadless() ? new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB) - : getGraphicsConfiguration().createCompatibleImage(width, height, Transparency.OPAQUE); + return createCompatibleImage(getGraphicsConfiguration(), width, height); + } + + public static BufferedImage createCompatibleTransparentImage(final GraphicsConfiguration gc, + final int width, final int height) { + return gc == null ? new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) + : gc.createCompatibleImage(width, height, Transparency.BITMASK); } public static BufferedImage createCompatibleTransparentImage(final int width, final int height) { - return isHeadless() ? new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) - : getGraphicsConfiguration().createCompatibleImage(width, height, Transparency.BITMASK); + return createCompatibleTransparentImage(getGraphicsConfiguration(), width, height); + } + + public static BufferedImage createCompatibleTranslucentImage(final GraphicsConfiguration gc, + final int width, final int height) { + return gc == null ? new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) + : gc.createCompatibleImage(width, height, Transparency.TRANSLUCENT); } public static BufferedImage createCompatibleTranslucentImage(final int width, final int height) { - return isHeadless() ? new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB) - : getGraphicsConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT); + return createCompatibleTranslucentImage(getGraphicsConfiguration(), width, height); } private static boolean isHeadless() { @@ -116,6 +171,8 @@ public final class ImageUtil { } private static GraphicsConfiguration getGraphicsConfiguration() { - return GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); + return isHeadless() + ? null + : GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); } } diff --git a/utils/src/main/java/com/github/weisj/darklaf/util/graphics/ImagePainter.java b/utils/src/main/java/com/github/weisj/darklaf/util/graphics/ImagePainter.java new file mode 100644 index 00000000..014ba250 --- /dev/null +++ b/utils/src/main/java/com/github/weisj/darklaf/util/graphics/ImagePainter.java @@ -0,0 +1,168 @@ +/* + * MIT License + * + * Copyright (c) 2021 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.github.weisj.darklaf.util.graphics; + +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Rectangle; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; +import java.awt.image.ImageObserver; + +public final class ImagePainter { + /** + * A hidpi-aware wrapper over {@link Graphics#drawImage(Image, int, int, ImageObserver)}. + * + * @see #drawImage(Graphics, Image, Rectangle, Rectangle, ImageObserver) + */ + public static void drawImage(final Graphics g, final Image image, final int x, final int y, + final ImageObserver observer) { + drawImage(g, image, new Rectangle(x, y, -1, -1), null, null, observer); + } + + static void drawImage(final Graphics g, final Image image, final int x, final int y, final int width, + final int height, final BufferedImageOp op, + final ImageObserver observer) { + Rectangle srcBounds = width >= 0 && height >= 0 ? new Rectangle(x, y, width, height) : null; + drawImage(g, image, new Rectangle(x, y, width, height), srcBounds, op, observer); + } + + /** + * A hidpi-aware wrapper over {@link Graphics#drawImage(Image, int, int, int, int, ImageObserver)}. + * + * @see #drawImage(Graphics, Image, Rectangle, Rectangle, BufferedImageOp, ImageObserver) + */ + public static void drawImage(final Graphics g, final Image image, final Rectangle dstBounds, + final ImageObserver observer) { + drawImage(g, image, dstBounds, null, null, observer); + } + + /** + * @see #drawImage(Graphics, Image, Rectangle, Rectangle, BufferedImageOp, ImageObserver) + */ + public static void drawImage(final Graphics g, + final Image image, + final Rectangle dstBounds, + final Rectangle srcBounds, + final ImageObserver observer) { + drawImage(g, image, dstBounds, srcBounds, null, observer); + } + + /** + * A hidpi-aware wrapper over + * {@link Graphics#drawImage(Image, int, int, int, int, int, int, int, int, ImageObserver)}. + *

+ * The {@code dstBounds} and {@code srcBounds} are in the user space (just like the width/height of + * the image). If {@code dstBounds} is null or if its width/height is set to (-1) the image bounds + * or the image width/height is used. If {@code srcBounds} is null or if its width/height is set to + * (-1) the image bounds or the image right/bottom area to the provided x/y is used. + */ + public static void drawImage(Graphics g, + Image image, + final Rectangle dstBounds, + final Rectangle srcBounds, + final BufferedImageOp op, + final ImageObserver observer) { + int userWidth = image.getWidth(observer); + int userHeight = image.getHeight(observer); + + int dx = 0; + int dy = 0; + int dw = -1; + int dh = -1; + if (dstBounds != null) { + dx = dstBounds.x; + dy = dstBounds.y; + dw = dstBounds.width; + dh = dstBounds.height; + } + boolean hasDstSize = dw >= 0 && dh >= 0; + + Graphics2D invG = null; + double scaleX = 1; + double scaleY = 1; + if (image instanceof ScaledImage) { + ScaledImage scaledImage = (ScaledImage) image; + Image delegate = scaledImage.getDelegate(); + if (delegate != null) image = delegate; + scaleX = scaledImage.getScaleX(); + scaleY = scaledImage.getScaleY(); + + userWidth /= scaleX; + userHeight /= scaleY; + + AffineTransform tx = ((Graphics2D) g).getTransform(); + if (scaleX == tx.getScaleX() && scaleY == tx.getScaleY()) { + // The image has the same original scale as the graphics scale. However, the real image + // scale - userSize/realSize - can suffer from inaccuracy due to the image user size + // rounding to int (userSize = (int)realSize/originalImageScale). This may case quality + // loss if the image is drawn via Graphics.drawImage(image, , ) + // due to scaling in Graphics. To avoid that, the image should be drawn directly via + // Graphics.drawImage(image, 0, 0) on the unscaled Graphics. + double gScaleX = tx.getScaleX(); + double gScaleY = tx.getScaleY(); + tx.scale(1 / gScaleX, 1 / gScaleY); + tx.translate(dx * gScaleX, dy * gScaleY); + dx = dy = 0; + g = invG = (Graphics2D) g.create(); + invG.setTransform(tx); + } + } + try { + if (op != null && image instanceof BufferedImage) { + image = op.filter((BufferedImage) image, null); + } + if (invG != null && hasDstSize) { + dw = (int) Math.round(dw * scaleX); + dh = (int) Math.round(dh * scaleY); + } + if (srcBounds != null) { + int sx = (int) Math.round(srcBounds.x * scaleX); + int sy = (int) Math.round(srcBounds.y * scaleY); + int sw = srcBounds.width >= 0 + ? (int) Math.round(srcBounds.width * scaleX) + : (int) Math.round(userWidth * scaleX) - sx; + int sh = srcBounds.height >= 0 + ? (int) Math.round(srcBounds.height * scaleY) + : (int) Math.round(userHeight * scaleY) - sy; + if (!hasDstSize) { + dw = (int) Math.round(userWidth * scaleX); + dh = (int) Math.round(userHeight * scaleY); + } + g.drawImage(image, + dx, dy, dx + dw, dy + dh, + sx, sy, sx + sw, sy + sh, + observer); + } else if (hasDstSize) { + g.drawImage(image, dx, dy, dw, dh, observer); + } else if (invG == null) { + g.drawImage(image, dx, dy, userWidth, userHeight, observer); + } else { + g.drawImage(image, dx, dy, observer); + } + } finally { + if (invG != null) invG.dispose(); + } + } +} diff --git a/utils/src/main/java/com/github/weisj/darklaf/util/graphics/ScaledImage.java b/utils/src/main/java/com/github/weisj/darklaf/util/graphics/ScaledImage.java new file mode 100644 index 00000000..6ee30dd3 --- /dev/null +++ b/utils/src/main/java/com/github/weisj/darklaf/util/graphics/ScaledImage.java @@ -0,0 +1,78 @@ +/* + * MIT License + * + * Copyright (c) 2021 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.github.weisj.darklaf.util.graphics; + +import java.awt.Graphics; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.awt.image.ImageObserver; +import java.awt.image.ImageProducer; + +public class ScaledImage extends Image { + + private final BufferedImage delegate; + private final double scaleX; + private final double scaleY; + + public ScaledImage(final BufferedImage delegate, final double scaleX, final double scaleY) { + this.delegate = delegate; + this.scaleX = scaleX; + this.scaleY = scaleY; + } + + public BufferedImage getDelegate() { + return delegate; + } + + public double getScaleX() { + return scaleX; + } + + public double getScaleY() { + return scaleY; + } + + @Override + public int getWidth(final ImageObserver observer) { + return delegate.getWidth(observer); + } + + @Override + public int getHeight(final ImageObserver observer) { + return delegate.getHeight(observer); + } + + @Override + public ImageProducer getSource() { + return delegate.getSource(); + } + + @Override + public Graphics getGraphics() { + return delegate.getGraphics(); + } + + @Override + public Object getProperty(final String name, final ImageObserver observer) { + return delegate.getProperty(name, observer); + } +} diff --git a/utils/src/main/java/com/github/weisj/darklaf/util/value/Shared.java b/utils/src/main/java/com/github/weisj/darklaf/util/value/Shared.java new file mode 100644 index 00000000..7b93bd37 --- /dev/null +++ b/utils/src/main/java/com/github/weisj/darklaf/util/value/Shared.java @@ -0,0 +1,38 @@ +/* + * MIT License + * + * Copyright (c) 2021 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.github.weisj.darklaf.util.value; + +public class Shared { + private T value; + + public Shared(final T initial) { + set(initial); + } + + public T get() { + return value; + } + + public void set(final T value) { + this.value = value; + } +} diff --git a/utils/src/main/java/com/github/weisj/darklaf/util/value/SharedNonNull.java b/utils/src/main/java/com/github/weisj/darklaf/util/value/SharedNonNull.java new file mode 100644 index 00000000..ef2dfd4c --- /dev/null +++ b/utils/src/main/java/com/github/weisj/darklaf/util/value/SharedNonNull.java @@ -0,0 +1,36 @@ +/* + * MIT License + * + * Copyright (c) 2021 Jannis Weis + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, + * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT + * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ +package com.github.weisj.darklaf.util.value; + +import java.util.Objects; + +public class SharedNonNull extends Shared { + + public SharedNonNull(final T initial) { + super(initial); + } + + @Override + public void set(final T value) { + super.set(Objects.requireNonNull(value)); + } +}