org.eclipse.swt.internal.DPIUtil.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.swt.internal.DPIUtil.java

Source

/*******************************************************************************
 * Copyright (c) 2017 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.swt.internal;

import org.eclipse.swt.*;
import org.eclipse.swt.graphics.*;

/**
 * This class hold common constants and utility functions w.r.t. to SWT high DPI
 * functionality.
 * <p>
 * The {@code autoScaleUp(..)} methods convert from API coordinates (in
 * SWT points) to internal high DPI coordinates (in pixels) that interface with
 * native widgets.
 * </p>
 * <p>
 * The {@code autoScaleDown(..)} convert from high DPI pixels to API coordinates
 * (in SWT points).
 * </p>
 *
 * @since 3.105
 */
public class DPIUtil {

    private static final int DPI_ZOOM_100 = 96;

    private static int deviceZoom = 100;
    private static int nativeDeviceZoom = 100;

    private static enum AutoScaleMethod {
        AUTO, NEAREST, SMOOTH
    }

    private static AutoScaleMethod autoScaleMethodSetting = AutoScaleMethod.AUTO;
    private static AutoScaleMethod autoScaleMethod = AutoScaleMethod.NEAREST;

    private static String autoScaleValue;
    private static boolean useCairoAutoScale = false;

    /**
     * System property that controls the autoScale functionality.
     * <ul>
     * <li><b>false</b>: deviceZoom is set to 100%</li>
     * <li><b>integer</b>: deviceZoom depends on the current display resolution,
     *     but only uses integer multiples of 100%. The detected native zoom is
     *     generally rounded down (e.g. at 150%, will use 100%), unless close to
     *     the next integer multiple (currently at 175%, will use 200%).</li>
     * <li><b>integer200</b>: like <b>integer</b>, but the maximal zoom level is 200%.</li>
     * <li><b>quarter</b>: deviceZoom depends on the current display resolution,
     *     but only uses integer multiples of 25%. The detected native zoom is
     *     rounded to the closest permissible value.</li>
     * <li><b>exact</b>: deviceZoom uses the native zoom (with 1% as minimal
     *     step).</li>
     * <li><i>&lt;value&gt;</i>: deviceZoom uses the given integer value in
     *     percent as zoom level.</li>
     * </ul>
     * The current default is "integer200".
     */
    private static final String SWT_AUTOSCALE = "swt.autoScale";

    /**
     * System property that controls the method for scaling images:
     * <ul>
     * <li>"nearest": nearest-neighbor interpolation, may look jagged</li>
     * <li>"smooth": smooth edges, may look blurry</li>
     * </ul>
     * The current default is to use "nearest", except on
     * GTK when the deviceZoom is not an integer multiple of 100%.
     * The smooth strategy currently doesn't work on Win32 and Cocoa, see
     * <a href="https://bugs.eclipse.org/493455">bug 493455</a>.
     */
    private static final String SWT_AUTOSCALE_METHOD = "swt.autoScale.method";
    static {
        autoScaleValue = System.getProperty(SWT_AUTOSCALE);

        String value = System.getProperty(SWT_AUTOSCALE_METHOD);
        if (value != null) {
            if (AutoScaleMethod.NEAREST.name().equalsIgnoreCase(value)) {
                autoScaleMethod = autoScaleMethodSetting = AutoScaleMethod.NEAREST;
            } else if (AutoScaleMethod.SMOOTH.name().equalsIgnoreCase(value)) {
                autoScaleMethod = autoScaleMethodSetting = AutoScaleMethod.SMOOTH;
            }
        }
    }

    /**
     * Auto-scale down ImageData
     */
    public static ImageData autoScaleDown(Device device, final ImageData imageData) {
        if (deviceZoom == 100 || imageData == null || (device != null && !device.isAutoScalable()))
            return imageData;
        float scaleFactor = 1.0f / getScalingFactor();
        return autoScaleImageData(device, imageData, scaleFactor);
    }

    public static int[] autoScaleDown(int[] pointArray) {
        if (deviceZoom == 100 || pointArray == null)
            return pointArray;
        float scaleFactor = getScalingFactor();
        int[] returnArray = new int[pointArray.length];
        for (int i = 0; i < pointArray.length; i++) {
            returnArray[i] = Math.round(pointArray[i] / scaleFactor);
        }
        return returnArray;
    }

    public static int[] autoScaleDown(Drawable drawable, int[] pointArray) {
        if (drawable != null && !drawable.isAutoScalable())
            return pointArray;
        return autoScaleDown(pointArray);
    }

    /**
     * Auto-scale up float array dimensions.
     */
    public static float[] autoScaleDown(float size[]) {
        if (deviceZoom == 100 || size == null)
            return size;
        float scaleFactor = getScalingFactor();
        float scaledSize[] = new float[size.length];
        for (int i = 0; i < scaledSize.length; i++) {
            scaledSize[i] = size[i] / scaleFactor;
        }
        return scaledSize;
    }

    /**
     * Auto-scale up float array dimensions if enabled for Drawable class.
     */
    public static float[] autoScaleDown(Drawable drawable, float size[]) {
        if (drawable != null && !drawable.isAutoScalable())
            return size;
        return autoScaleDown(size);
    }

    /**
     * Auto-scale down int dimensions.
     */
    public static int autoScaleDown(int size) {
        if (deviceZoom == 100 || size == SWT.DEFAULT)
            return size;
        float scaleFactor = getScalingFactor();
        return Math.round(size / scaleFactor);
    }

    /**
     * Auto-scale down int dimensions if enabled for Drawable class.
     */
    public static int autoScaleDown(Drawable drawable, int size) {
        if (drawable != null && !drawable.isAutoScalable())
            return size;
        return autoScaleDown(size);
    }

    /**
     * Auto-scale down float dimensions.
     */
    public static float autoScaleDown(float size) {
        if (deviceZoom == 100 || size == SWT.DEFAULT)
            return size;
        float scaleFactor = getScalingFactor();
        return (size / scaleFactor);
    }

    /**
     * Auto-scale down float dimensions if enabled for Drawable class.
     */
    public static float autoScaleDown(Drawable drawable, float size) {
        if (drawable != null && !drawable.isAutoScalable())
            return size;
        return autoScaleDown(size);
    }

    /**
     * Returns a new scaled down Point.
     */
    public static Point autoScaleDown(Point point) {
        if (deviceZoom == 100 || point == null)
            return point;
        float scaleFactor = getScalingFactor();
        Point scaledPoint = new Point(0, 0);
        scaledPoint.x = Math.round(point.x / scaleFactor);
        scaledPoint.y = Math.round(point.y / scaleFactor);
        return scaledPoint;
    }

    /**
     * Returns a new scaled down Point if enabled for Drawable class.
     */
    public static Point autoScaleDown(Drawable drawable, Point point) {
        if (drawable != null && !drawable.isAutoScalable())
            return point;
        return autoScaleDown(point);
    }

    /**
     * Returns a new scaled down Rectangle.
     */
    public static Rectangle autoScaleDown(Rectangle rect) {
        if (deviceZoom == 100 || rect == null)
            return rect;
        Rectangle scaledRect = new Rectangle(0, 0, 0, 0);
        Point scaledTopLeft = DPIUtil.autoScaleDown(new Point(rect.x, rect.y));
        Point scaledBottomRight = DPIUtil.autoScaleDown(new Point(rect.x + rect.width, rect.y + rect.height));

        scaledRect.x = scaledTopLeft.x;
        scaledRect.y = scaledTopLeft.y;
        scaledRect.width = scaledBottomRight.x - scaledTopLeft.x;
        scaledRect.height = scaledBottomRight.y - scaledTopLeft.y;
        return scaledRect;
    }

    /**
     * Returns a new scaled down Rectangle if enabled for Drawable class.
     */
    public static Rectangle autoScaleDown(Drawable drawable, Rectangle rect) {
        if (drawable != null && !drawable.isAutoScalable())
            return rect;
        return autoScaleDown(rect);
    }

    /**
     * Auto-scale image with ImageData
     */
    public static ImageData autoScaleImageData(Device device, final ImageData imageData, int targetZoom,
            int currentZoom) {
        if (imageData == null || targetZoom == currentZoom || (device != null && !device.isAutoScalable()))
            return imageData;
        float scaleFactor = (float) targetZoom / (float) currentZoom;
        return autoScaleImageData(device, imageData, scaleFactor);
    }

    private static ImageData autoScaleImageData(Device device, final ImageData imageData, float scaleFactor) {
        // Guards are already implemented in callers: if (deviceZoom == 100 || imageData == null || scaleFactor == 1.0f) return imageData;
        int width = imageData.width;
        int height = imageData.height;
        int scaledWidth = Math.round((float) width * scaleFactor);
        int scaledHeight = Math.round((float) height * scaleFactor);
        switch (autoScaleMethod) {
        case SMOOTH:
            Image original = new Image(device, (ImageDataProvider) zoom -> imageData);

            /* Create a 24 bit image data with alpha channel */
            final ImageData resultData = new ImageData(scaledWidth, scaledHeight, 24,
                    new PaletteData(0xFF, 0xFF00, 0xFF0000));
            resultData.alphaData = new byte[scaledWidth * scaledHeight];

            Image resultImage = new Image(device, (ImageDataProvider) zoom -> resultData);
            GC gc = new GC(resultImage);
            gc.setAntialias(SWT.ON);
            gc.drawImage(original, 0, 0, DPIUtil.autoScaleDown(width), DPIUtil.autoScaleDown(height),
                    /* E.g. destWidth here is effectively DPIUtil.autoScaleDown (scaledWidth), but avoiding rounding errors.
                     * Nevertheless, we still have some rounding errors due to the point-based API GC#drawImage(..).
                     */
                    0, 0, Math.round(DPIUtil.autoScaleDown((float) width * scaleFactor)),
                    Math.round(DPIUtil.autoScaleDown((float) height * scaleFactor)));
            gc.dispose();
            original.dispose();
            ImageData result = resultImage.getImageData(DPIUtil.getDeviceZoom());
            resultImage.dispose();
            return result;
        case NEAREST:
        default:
            return imageData.scaledTo(scaledWidth, scaledHeight);
        }
    }

    /**
     * Returns a new rectangle as per the scaleFactor.
     */
    public static Rectangle autoScaleBounds(Rectangle rect, int targetZoom, int currentZoom) {
        if (deviceZoom == 100 || rect == null || targetZoom == currentZoom)
            return rect;
        float scaleFactor = ((float) targetZoom) / (float) currentZoom;
        Rectangle returnRect = new Rectangle(0, 0, 0, 0);
        returnRect.x = Math.round(rect.x * scaleFactor);
        returnRect.y = Math.round(rect.y * scaleFactor);
        returnRect.width = Math.round(rect.width * scaleFactor);
        returnRect.height = Math.round(rect.height * scaleFactor);
        return returnRect;
    }

    /**
     * Auto-scale up ImageData
     */
    public static ImageData autoScaleUp(Device device, final ImageData imageData) {
        if (deviceZoom == 100 || imageData == null || (device != null && !device.isAutoScalable()))
            return imageData;
        float scaleFactor = deviceZoom / 100f;
        return autoScaleImageData(device, imageData, scaleFactor);
    }

    public static int[] autoScaleUp(int[] pointArray) {
        if (deviceZoom == 100 || pointArray == null)
            return pointArray;
        float scaleFactor = getScalingFactor();
        int[] returnArray = new int[pointArray.length];
        for (int i = 0; i < pointArray.length; i++) {
            returnArray[i] = Math.round(pointArray[i] * scaleFactor);
        }
        return returnArray;
    }

    public static int[] autoScaleUp(Drawable drawable, int[] pointArray) {
        if (drawable != null && !drawable.isAutoScalable())
            return pointArray;
        return autoScaleUp(pointArray);
    }

    /**
     * Auto-scale up int dimensions.
     */
    public static int autoScaleUp(int size) {
        if (deviceZoom == 100 || size == SWT.DEFAULT)
            return size;
        float scaleFactor = getScalingFactor();
        return Math.round(size * scaleFactor);
    }

    /**
     * Auto-scale up int dimensions using Native DPI
     */
    public static int autoScaleUpUsingNativeDPI(int size) {
        if (nativeDeviceZoom == 100 || size == SWT.DEFAULT)
            return size;
        float nativeScaleFactor = nativeDeviceZoom / 100f;
        return Math.round(size * nativeScaleFactor);
    }

    /**
     * Auto-scale up int dimensions if enabled for Drawable class.
     */
    public static int autoScaleUp(Drawable drawable, int size) {
        if (drawable != null && !drawable.isAutoScalable())
            return size;
        return autoScaleUp(size);
    }

    public static float autoScaleUp(float size) {
        if (deviceZoom == 100 || size == SWT.DEFAULT)
            return size;
        float scaleFactor = getScalingFactor();
        return (size * scaleFactor);
    }

    public static float autoScaleUp(Drawable drawable, float size) {
        if (drawable != null && !drawable.isAutoScalable())
            return size;
        return autoScaleUp(size);
    }

    /**
     * Returns a new scaled up Point.
     */
    public static Point autoScaleUp(Point point) {
        if (deviceZoom == 100 || point == null)
            return point;
        float scaleFactor = getScalingFactor();
        Point scaledPoint = new Point(0, 0);
        scaledPoint.x = Math.round(point.x * scaleFactor);
        scaledPoint.y = Math.round(point.y * scaleFactor);
        return scaledPoint;
    }

    /**
     * Returns a new scaled up Point if enabled for Drawable class.
     */
    public static Point autoScaleUp(Drawable drawable, Point point) {
        if (drawable != null && !drawable.isAutoScalable())
            return point;
        return autoScaleUp(point);
    }

    /**
     * Returns a new scaled up Rectangle.
     */
    public static Rectangle autoScaleUp(Rectangle rect) {
        if (deviceZoom == 100 || rect == null)
            return rect;
        Rectangle scaledRect = new Rectangle(0, 0, 0, 0);
        Point scaledTopLeft = DPIUtil.autoScaleUp(new Point(rect.x, rect.y));
        Point scaledBottomRight = DPIUtil.autoScaleUp(new Point(rect.x + rect.width, rect.y + rect.height));

        scaledRect.x = scaledTopLeft.x;
        scaledRect.y = scaledTopLeft.y;
        scaledRect.width = scaledBottomRight.x - scaledTopLeft.x;
        scaledRect.height = scaledBottomRight.y - scaledTopLeft.y;
        return scaledRect;
    }

    /**
     * Returns a new scaled up Rectangle if enabled for Drawable class.
     */
    public static Rectangle autoScaleUp(Drawable drawable, Rectangle rect) {
        if (drawable != null && !drawable.isAutoScalable())
            return rect;
        return autoScaleUp(rect);
    }

    /**
     * Returns Scaling factor from the display
     * @return float scaling factor
     */
    private static float getScalingFactor() {
        if (useCairoAutoScale) {
            return 1;
        }
        return deviceZoom / 100f;
    }

    /**
     * Compute the zoom value based on the DPI value.
     *
     * @return zoom
     */
    public static int mapDPIToZoom(int dpi) {
        double zoom = (double) dpi * 100 / DPI_ZOOM_100;
        int roundedZoom = (int) Math.round(zoom);
        return roundedZoom;
    }

    /**
     * Gets Image data at specified zoom level, if image is missing then
     * fall-back to 100% image. If provider or fall-back image is not available,
     * throw error.
     */
    public static ImageData validateAndGetImageDataAtZoom(ImageDataProvider provider, int zoom, boolean[] found) {
        if (provider == null)
            SWT.error(SWT.ERROR_NULL_ARGUMENT);
        ImageData data = provider.getImageData(zoom);
        found[0] = (data != null);
        /* If image is null when (zoom != 100%), fall-back to image at 100% zoom */
        if (zoom != 100 && !found[0])
            data = provider.getImageData(100);
        if (data == null)
            SWT.error(SWT.ERROR_INVALID_ARGUMENT, null,
                    ": ImageDataProvider [" + provider + "] returns null ImageData at 100% zoom.");
        return data;
    }

    /**
     * Gets Image file path at specified zoom level, if image is missing then
     * fall-back to 100% image. If provider or fall-back image is not available,
     * throw error.
     */
    public static String validateAndGetImagePathAtZoom(ImageFileNameProvider provider, int zoom, boolean[] found) {
        if (provider == null)
            SWT.error(SWT.ERROR_NULL_ARGUMENT);
        String filename = provider.getImagePath(zoom);
        found[0] = (filename != null);
        /* If image is null when (zoom != 100%), fall-back to image at 100% zoom */
        if (zoom != 100 && !found[0])
            filename = provider.getImagePath(100);
        if (filename == null)
            SWT.error(SWT.ERROR_INVALID_ARGUMENT, null,
                    ": ImageFileNameProvider [" + provider + "] returns null filename at 100% zoom.");
        return filename;
    }

    public static int getDeviceZoom() {
        return deviceZoom;
    }

    public static void setDeviceZoom(int nativeDeviceZoom) {
        DPIUtil.nativeDeviceZoom = nativeDeviceZoom;
        int deviceZoom = getZoomForAutoscaleProperty(nativeDeviceZoom);

        DPIUtil.deviceZoom = deviceZoom;
        System.setProperty("org.eclipse.swt.internal.deviceZoom", Integer.toString(deviceZoom));
        if (deviceZoom != 100 && autoScaleMethodSetting == AutoScaleMethod.AUTO) {
            if (deviceZoom / 100 * 100 == deviceZoom || !"gtk".equals(SWT.getPlatform())) {
                autoScaleMethod = AutoScaleMethod.NEAREST;
            } else {
                autoScaleMethod = AutoScaleMethod.SMOOTH;
            }
        }
    }

    public static void setUseCairoAutoScale(boolean cairoAutoScale) {
        useCairoAutoScale = cairoAutoScale;
    }

    public static boolean useCairoAutoScale() {
        return useCairoAutoScale;
    }

    public static int getZoomForAutoscaleProperty(int nativeDeviceZoom) {
        int zoom = 0;
        if (autoScaleValue != null) {
            if ("false".equalsIgnoreCase(autoScaleValue)) {
                zoom = 100;
            } else if ("quarter".equalsIgnoreCase(autoScaleValue)) {
                zoom = (int) (Math.round(nativeDeviceZoom / 25f) * 25);
            } else if ("exact".equalsIgnoreCase(autoScaleValue)) {
                zoom = nativeDeviceZoom;
            } else {
                try {
                    int zoomValue = Integer.parseInt(autoScaleValue);
                    zoom = Math.max(Math.min(zoomValue, 1600), 25);
                } catch (NumberFormatException e) {
                    // unsupported value, use default
                }
            }
        }
        if (zoom == 0) { // || "integer".equalsIgnoreCase (value) || "integer200".equalsIgnoreCase (value)
            zoom = Math.max((nativeDeviceZoom + 25) / 100 * 100, 100);
            if (!"integer".equalsIgnoreCase(autoScaleValue)) {
                // integer200, or default
                zoom = Math.min(zoom, 200);
            }
        }
        return zoom;
    }

    /**
     * AutoScale ImageDataProvider.
     */
    public static final class AutoScaleImageDataProvider implements ImageDataProvider {
        Device device;
        ImageData imageData;
        int currentZoom;

        public AutoScaleImageDataProvider(Device device, ImageData data, int zoom) {
            this.device = device;
            this.imageData = data;
            this.currentZoom = zoom;
        }

        @Override
        public ImageData getImageData(int zoom) {
            return DPIUtil.autoScaleImageData(device, imageData, zoom, currentZoom);
        }
    }
}