forked from dorkbox/SystemTray
Code polish/cleanup. Moved icon size determination to another class
This commit is contained in:
parent
9dbee8cfc5
commit
b8a3736fbf
395
src/dorkbox/systemTray/util/ImageResizeUtil.java
Normal file
395
src/dorkbox/systemTray/util/ImageResizeUtil.java
Normal file
@ -0,0 +1,395 @@
|
||||
/*
|
||||
* Copyright 2016 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.systemTray.util;
|
||||
|
||||
import static dorkbox.util.ImageUtil.getBufferedImage;
|
||||
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Image;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
|
||||
import dorkbox.systemTray.SystemTray;
|
||||
import dorkbox.util.CacheUtil;
|
||||
import dorkbox.util.IO;
|
||||
import dorkbox.util.ImageUtil;
|
||||
|
||||
public
|
||||
class ImageResizeUtil {
|
||||
public static final File TEMP_DIR = new File(CacheUtil.TEMP_DIR, "SystemTrayImages");
|
||||
|
||||
public static
|
||||
File getTransparentImage() {
|
||||
// here, it doesn't matter what size the image is, as long as there is an image, the text in the menu will be shifted correctly
|
||||
// it is HIGHLY unlikely that the menu entry will be smaller than 4px.
|
||||
|
||||
// NOTE: this does not need to be called on the EDT
|
||||
try {
|
||||
final File newFile = new File(TEMP_DIR, 4 + "_empty.png").getAbsoluteFile();
|
||||
return ImageUtil.getTransparentImage(4, newFile);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Unable to generate transparent image! Something is severely wrong!");
|
||||
}
|
||||
}
|
||||
|
||||
private static
|
||||
File getErrorImage(int size) {
|
||||
if (size == 0) {
|
||||
// default size
|
||||
size = 32;
|
||||
}
|
||||
|
||||
try {
|
||||
InputStream imageStream = ImageResizeUtil.class.getResource("error_32.png").openStream();
|
||||
|
||||
// have to resize the image to be whatever size we specify
|
||||
imageStream = makeByteArrayInputStream(imageStream);
|
||||
imageStream.mark(0);
|
||||
|
||||
// check if we already have this file information saved to disk, based on size + hash of data
|
||||
final String cacheName = size + "_" + CacheUtil.createNameAsHash(imageStream);
|
||||
((ByteArrayInputStream) imageStream).reset(); // casting to avoid unnecessary try/catch for IOException
|
||||
|
||||
|
||||
// if we already have this fileName, reuse it
|
||||
final File check = getIfCached(cacheName);
|
||||
if (check != null) {
|
||||
return check;
|
||||
}
|
||||
|
||||
// we have to hop through hoops.
|
||||
File resizedFile = resizeFileNoCheck(size, imageStream);
|
||||
|
||||
// now cache that file
|
||||
File save = CacheUtil.save(cacheName, resizedFile);
|
||||
return save;
|
||||
} catch (Exception e) {
|
||||
// this must be thrown
|
||||
throw new RuntimeException("Serious problems! Unable to extract error image, this should NEVER happen!", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static
|
||||
File getIfCached(final String cacheName) {
|
||||
try {
|
||||
final File check = CacheUtil.check(cacheName);
|
||||
if (check != null) {
|
||||
return check;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static synchronized
|
||||
File resizeAndCache(final int size, final File file) {
|
||||
return resizeAndCache(size, file.getAbsolutePath());
|
||||
}
|
||||
|
||||
private static synchronized
|
||||
File resizeAndCache(final int size, final String fileName) {
|
||||
if (fileName == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
FileInputStream fileInputStream = new FileInputStream(fileName);
|
||||
File file = resizeAndCache(size, fileInputStream);
|
||||
fileInputStream.close();
|
||||
|
||||
return file;
|
||||
} catch (Exception e) {
|
||||
// have to serve up the error image instead.
|
||||
SystemTray.logger.error("Error reading image. Using error icon instead", e);
|
||||
return getErrorImage(size);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("Duplicates")
|
||||
private static synchronized
|
||||
File resizeAndCache(final int size, InputStream imageStream) {
|
||||
if (imageStream == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String cacheName;
|
||||
|
||||
// no cached file, so we resize then save the new one.
|
||||
boolean needsResize = true;
|
||||
try {
|
||||
imageStream = makeByteArrayInputStream(imageStream);
|
||||
imageStream.mark(0);
|
||||
|
||||
// check if we already have this file information saved to disk, based on size + hash of data
|
||||
cacheName = size + "_" + CacheUtil.createNameAsHash(imageStream);
|
||||
((ByteArrayInputStream) imageStream).reset(); // casting to avoid unnecessary try/catch for IOException
|
||||
|
||||
|
||||
// if we already have this fileName, reuse it
|
||||
final File check = getIfCached(cacheName);
|
||||
if (check != null) {
|
||||
return check;
|
||||
}
|
||||
|
||||
|
||||
imageStream.mark(0);
|
||||
Dimension imageSize = ImageUtil.getImageSize(imageStream);
|
||||
//noinspection NumericCastThatLosesPrecision
|
||||
if (size == ((int) imageSize.getWidth())) {
|
||||
// we can reuse this URL (it's the correct size).
|
||||
needsResize = false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// have to serve up the error image instead.
|
||||
SystemTray.logger.error("Error getting image size. Using error icon instead", e);
|
||||
return getErrorImage(size);
|
||||
} finally {
|
||||
((ByteArrayInputStream) imageStream).reset(); // casting to avoid unnecessary try/catch for IOException
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (needsResize) {
|
||||
// we have to hop through hoops.
|
||||
try {
|
||||
File resizedFile = resizeFileNoCheck(size, imageStream);
|
||||
|
||||
// now cache that file
|
||||
try {
|
||||
return CacheUtil.save(cacheName, resizedFile);
|
||||
} catch (Exception e) {
|
||||
// have to serve up the error image instead.
|
||||
SystemTray.logger.error("Error caching image. Using error icon instead", e);
|
||||
return getErrorImage(size);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
// have to serve up the error image instead.
|
||||
SystemTray.logger.error("Error resizing image. Using error icon instead", e);
|
||||
return getErrorImage(size);
|
||||
}
|
||||
|
||||
} else {
|
||||
// no resize necessary, just cache as is.
|
||||
try {
|
||||
return CacheUtil.save(cacheName, imageStream);
|
||||
} catch (Exception e) {
|
||||
// have to serve up the error image instead.
|
||||
SystemTray.logger.error("Error caching image. Using error icon instead", e);
|
||||
return getErrorImage(size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if this input stream is NOT a ByteArrayInputStream, make it one.
|
||||
private static
|
||||
InputStream makeByteArrayInputStream(InputStream imageStream) throws IOException {
|
||||
if (!(imageStream instanceof ByteArrayInputStream)) {
|
||||
// have to make a copy of the inputStream, but only if necessary
|
||||
ByteArrayOutputStream byteArrayOutputStream = IO.copyStream(imageStream);
|
||||
imageStream.close();
|
||||
|
||||
imageStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
|
||||
}
|
||||
return imageStream;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resizes the given InputStream to the specified size. No checks are performed if it's the correct size to begin with.
|
||||
*
|
||||
* @return the file on disk that is the resized icon
|
||||
*/
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
private static
|
||||
File resizeFileNoCheck(final int size, InputStream inputStream) throws IOException {
|
||||
// have to resize the file (and return the new path)
|
||||
|
||||
// now have to resize this file.
|
||||
File newFile = new File(TEMP_DIR, "temp_resize.png").getAbsoluteFile();
|
||||
Image image;
|
||||
|
||||
|
||||
// resize the image, keep aspect
|
||||
image = ImageUtil.getImageImmediate(ImageIO.read(inputStream));
|
||||
image = image.getScaledInstance(size, -1, Image.SCALE_SMOOTH);
|
||||
|
||||
// make whatever dirs we need to.
|
||||
newFile.getParentFile().mkdirs();
|
||||
|
||||
// if it's already there, we have to delete it
|
||||
newFile.delete();
|
||||
|
||||
// now write out the new one
|
||||
BufferedImage bufferedImage = getBufferedImage(image);
|
||||
ImageIO.write(bufferedImage, "png", newFile); // made up extension
|
||||
|
||||
return newFile;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public static
|
||||
File shouldResizeOrCache(final boolean isTrayImage, final File imageFile) {
|
||||
if (imageFile == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (SystemTray.AUTO_SIZE) {
|
||||
return ImageResizeUtil.resizeAndCache(getSize(isTrayImage), imageFile);
|
||||
} else {
|
||||
return imageFile;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static
|
||||
File shouldResizeOrCache(final boolean isTrayImage, final String imagePath) {
|
||||
if (imagePath == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (SystemTray.AUTO_SIZE) {
|
||||
return ImageResizeUtil.resizeAndCache(getSize(isTrayImage), imagePath);
|
||||
} else {
|
||||
return new File(imagePath);
|
||||
}
|
||||
}
|
||||
|
||||
public static
|
||||
File shouldResizeOrCache(final boolean isTrayImage, final URL imageUrl) {
|
||||
if (imageUrl == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
if (SystemTray.AUTO_SIZE) {
|
||||
InputStream inputStream = imageUrl.openStream();
|
||||
File file = resizeAndCache(getSize(isTrayImage), inputStream);
|
||||
inputStream.close();
|
||||
|
||||
return file;
|
||||
} else {
|
||||
return CacheUtil.save(imageUrl);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// have to serve up the error image instead.
|
||||
SystemTray.logger.error("Error reading image. Using error icon instead", e);
|
||||
return getErrorImage(getSize(isTrayImage));
|
||||
}
|
||||
}
|
||||
|
||||
public static
|
||||
File shouldResizeOrCache(final boolean isTrayImage, final InputStream imageStream) {
|
||||
if (imageStream == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (SystemTray.AUTO_SIZE) {
|
||||
return ImageResizeUtil.resizeAndCache(getSize(isTrayImage), imageStream);
|
||||
} else {
|
||||
try {
|
||||
return CacheUtil.save(imageStream);
|
||||
} catch (IOException e) {
|
||||
SystemTray.logger.error("Error checking cache for information. Using error icon instead", e);
|
||||
return getErrorImage(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static
|
||||
File shouldResizeOrCache(final boolean isTrayImage, final Image image) {
|
||||
if (image == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final Image trayImage = ImageUtil.getImageImmediate(image);
|
||||
|
||||
BufferedImage bufferedImage = getBufferedImage(trayImage);
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
ImageIO.write(bufferedImage, "png", os);
|
||||
InputStream imageInputStream = new ByteArrayInputStream(os.toByteArray());
|
||||
|
||||
|
||||
if (SystemTray.AUTO_SIZE) {
|
||||
File file = resizeAndCache(getSize(isTrayImage), imageInputStream);
|
||||
imageInputStream.close(); // BAOS doesn't do anything, but here for completeness + documentation
|
||||
|
||||
return file;
|
||||
} else {
|
||||
File file = CacheUtil.save(imageInputStream);
|
||||
imageInputStream.close(); // BAOS doesn't do anything, but here for completeness + documentation
|
||||
|
||||
return file;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// have to serve up the error image instead.
|
||||
SystemTray.logger.error("Error reading image. Using error icon instead", e);
|
||||
return getErrorImage(getSize(isTrayImage));
|
||||
}
|
||||
}
|
||||
|
||||
public static
|
||||
File shouldResizeOrCache(final boolean isTrayImage, final ImageInputStream imageStream) {
|
||||
if (imageStream == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream byteArrayOutputStream = IO.copyStream(imageStream);
|
||||
ByteArrayInputStream fileStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
|
||||
|
||||
if (SystemTray.AUTO_SIZE) {
|
||||
return resizeAndCache(getSize(isTrayImage), fileStream);
|
||||
} else {
|
||||
return CacheUtil.save(fileStream);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// have to serve up the error image instead.
|
||||
SystemTray.logger.error("Error reading image. Using error icon instead", e);
|
||||
return getErrorImage(getSize(isTrayImage));
|
||||
}
|
||||
}
|
||||
|
||||
private static
|
||||
int getSize(final boolean isTrayImage) {
|
||||
int size;
|
||||
if (isTrayImage) {
|
||||
// system tray image
|
||||
size = SizeAndScalingUtil.TRAY_SIZE;
|
||||
} else {
|
||||
// menu image
|
||||
size = SizeAndScalingUtil.TRAY_MENU_SIZE;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
}
|
@ -1,731 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 dorkbox, llc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package dorkbox.systemTray.util;
|
||||
|
||||
import static dorkbox.systemTray.jna.windows.Gdi32.GetDeviceCaps;
|
||||
import static dorkbox.systemTray.jna.windows.Gdi32.LOGPIXELSX;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.Image;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.SwingUtilities;
|
||||
|
||||
import com.sun.jna.Pointer;
|
||||
|
||||
import dorkbox.systemTray.SystemTray;
|
||||
import dorkbox.systemTray.jna.windows.User32;
|
||||
import dorkbox.util.CacheUtil;
|
||||
import dorkbox.util.FileUtil;
|
||||
import dorkbox.util.IO;
|
||||
import dorkbox.util.LocationResolver;
|
||||
import dorkbox.util.OS;
|
||||
import dorkbox.util.OSUtil;
|
||||
import dorkbox.util.SwingUtil;
|
||||
import dorkbox.util.process.ShellProcessBuilder;
|
||||
|
||||
public
|
||||
class ImageUtils {
|
||||
|
||||
private static final File TEMP_DIR = new File(CacheUtil.TEMP_DIR, "ResizedImages");
|
||||
|
||||
// tray/menu-entry size.
|
||||
// for more complete info on the linux side of things...
|
||||
// https://wiki.archlinux.org/index.php/HiDPI
|
||||
public static volatile int TRAY_SIZE = 0;
|
||||
public static volatile int ENTRY_SIZE = 0;
|
||||
|
||||
public static
|
||||
void determineIconSize() {
|
||||
double trayScalingFactor = 0;
|
||||
double menuScalingFactor = 0;
|
||||
|
||||
if (SystemTray.AUTO_TRAY_SIZE) {
|
||||
if (OS.isWindows()) {
|
||||
int[] version = OSUtil.Windows.getVersion();
|
||||
|
||||
// windows 8/8.1/10 are the only windows OSes to do scaling properly (XP/Vista/7 do DPI scaling, which is terrible anyways)
|
||||
// we are going to let windows manage scaling the icon correctly, but we are BY DEFAULT going to give it a large size to scale
|
||||
|
||||
// vista - 8.0 - only global DPI settings
|
||||
// 8.1 - 10 - global + per-monitor DPI settings
|
||||
|
||||
// 1 = 16
|
||||
// 2 = 32
|
||||
// 4 = 64
|
||||
// 8 = 128
|
||||
|
||||
// scaling 1
|
||||
if (version[0] <= 5) {
|
||||
// Windows XP 5.1.2600 (2001-10-25)
|
||||
// Windows Server 2003 5.2.3790 (2003-04-24)
|
||||
// Windows Home Server 5.2.3790 (2007-06-16)
|
||||
trayScalingFactor = 2;
|
||||
} else if (version[0] == 6 && version[1] == 0) {
|
||||
// Windows Vista 6.0.6000 (2006-11-08)
|
||||
// Windows Server 2008 SP1 6.0.6001 (2008-02-27)
|
||||
// Windows Server 2008 SP2 6.0.6002 (2009-04-28)
|
||||
trayScalingFactor = 2;
|
||||
|
||||
} else if (version[0] == 6 && version[1] <= 2) {
|
||||
// Windows 7 6.1.7600 (2009-10-22)
|
||||
// Windows Server 2008 R2 6.1.7600 (2009-10-22)
|
||||
// Windows Server 2008 R2 SP1 6.1.7601 (?)
|
||||
//
|
||||
// Windows Home Server 2011 6.1.8400 (2011-04-05)
|
||||
//
|
||||
// Windows 8 6.2.9200 (2012-10-26)
|
||||
// Windows Server 2012 6.2.9200 (2012-09-04)
|
||||
trayScalingFactor = 4;
|
||||
} else {
|
||||
// Windows 8.1 6.3.9600 (2013-10-18)
|
||||
// Windows Server 2012 R2 6.3.9600 (2013-10-18)
|
||||
//
|
||||
// Windows 10 10.0.10240 (2015-07-29)
|
||||
// Windows 10 10.0.10586 (2015-11-12)
|
||||
// Windows 10 10.0.14393 (2016-07-18)
|
||||
//
|
||||
// Windows Server 2016 10.0.14393 (2016-10-12)
|
||||
trayScalingFactor = 4;
|
||||
}
|
||||
|
||||
// get's the HARDWARE DEVICE logical resolution. This will be set by the monitor, and will never change - even if
|
||||
// scaling is changed via the control panel
|
||||
Pointer screen = User32.GetDC(null);
|
||||
int dpiX = GetDeviceCaps(screen, LOGPIXELSX);
|
||||
User32.ReleaseDC(null, screen);
|
||||
|
||||
// 96 DPI = 100% scaling
|
||||
// 120 DPI = 125% scaling
|
||||
// 144 DPI = 150% scaling
|
||||
// 192 DPI = 200% scaling
|
||||
|
||||
// just a note on scaling...
|
||||
// We want to scale the image as best we can beforehand, so there is an attempt to have it look good. Java by default
|
||||
// does not scale this correctly.
|
||||
if (dpiX != 96) {
|
||||
// so there are additional scaling settings...
|
||||
// casting around for rounding/math stuff
|
||||
menuScalingFactor = ((double) dpiX) / 96.0;
|
||||
}
|
||||
|
||||
|
||||
if (SystemTray.DEBUG) {
|
||||
SystemTray.logger.debug("Windows version: '{}'", Arrays.toString(version));
|
||||
SystemTray.logger.debug("Windows DPI settings: '{}'", dpiX);
|
||||
}
|
||||
} else if (OS.isLinux() || OS.isUnix()) {
|
||||
// GtkStatusIcon will USUALLY automatically scale the icon
|
||||
// AppIndicator MIGHT scale the icon (depends on the OS)
|
||||
|
||||
|
||||
// KDE is bonkers. Gnome is "ok"
|
||||
String XDG = System.getenv("XDG_CURRENT_DESKTOP");
|
||||
if (XDG == null) {
|
||||
// Check if plasmashell is running, if it is -- then we are most likely KDE
|
||||
double plasmaVersion = OSUtil.DesktopEnv.getPlasmaVersion();
|
||||
if (plasmaVersion > 0) {
|
||||
XDG = "kde";
|
||||
}
|
||||
}
|
||||
|
||||
if ("kde".equalsIgnoreCase(XDG)) {
|
||||
double plasmaVersion = OSUtil.DesktopEnv.getPlasmaVersion();
|
||||
|
||||
// 1 = 16
|
||||
// 2 = 32
|
||||
// 4 = 64
|
||||
// 8 = 128
|
||||
if (plasmaVersion > 0) {
|
||||
trayScalingFactor = 2;
|
||||
// menuScalingFactor = 1.4;
|
||||
} else if (SystemTray.DEBUG) {
|
||||
SystemTray.logger.error("Cannot check plasmashell version");
|
||||
}
|
||||
} else {
|
||||
// it's likely a Gnome/unity environment
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(8196);
|
||||
PrintStream outputStream = new PrintStream(byteArrayOutputStream);
|
||||
|
||||
// gsettings get org.gnome.desktop.interface scaling-factor
|
||||
final ShellProcessBuilder shellVersion = new ShellProcessBuilder(outputStream);
|
||||
shellVersion.setExecutable("gsettings");
|
||||
shellVersion.addArgument("get");
|
||||
shellVersion.addArgument("org.gnome.desktop.interface");
|
||||
shellVersion.addArgument("scaling-factor");
|
||||
shellVersion.start();
|
||||
|
||||
String output = ShellProcessBuilder.getOutput(byteArrayOutputStream);
|
||||
|
||||
if (!output.isEmpty()) {
|
||||
if (SystemTray.DEBUG) {
|
||||
SystemTray.logger.debug("Checking scaling factor for GTK environment, should start with 'uint32', value: '{}'", output);
|
||||
}
|
||||
|
||||
// DEFAULT icon size is 16. HiDpi changes this scale, so we should use it as well.
|
||||
// should be: uint32 0 or something
|
||||
if (output.contains("uint32")) {
|
||||
String value = output.substring(output.indexOf("uint")+7, output.length());
|
||||
trayScalingFactor = Integer.parseInt(value);
|
||||
menuScalingFactor = Integer.parseInt(value);
|
||||
|
||||
// 0 is disabled (no scaling)
|
||||
// 1 is enabled (default scale)
|
||||
// 2 is 2x scale
|
||||
// 3 is 3x scale
|
||||
// etc
|
||||
|
||||
|
||||
// A setting of 2, 3, etc, which is all you can do with scaling-factor
|
||||
// To enable HiDPI, use gsettings:
|
||||
// gsettings set org.gnome.desktop.interface scaling-factor 2
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
if (SystemTray.DEBUG) {
|
||||
SystemTray.logger.error("Cannot check scaling factor", e);
|
||||
}
|
||||
}
|
||||
|
||||
// fedora 23+ has a different size for the indicator (NOT default 16px)
|
||||
int fedoraVersion = OSUtil.Linux.getFedoraVersion();
|
||||
if (trayScalingFactor == 0 && fedoraVersion >= 23) {
|
||||
if (SystemTray.DEBUG) {
|
||||
SystemTray.logger.debug("Adjusting tray/menu scaling for FEDORA " + fedoraVersion);
|
||||
}
|
||||
|
||||
trayScalingFactor = 2;
|
||||
}
|
||||
}
|
||||
} else if (OS.isMacOsX()) {
|
||||
// let's hope this wasn't just hardcoded like windows...
|
||||
int height;
|
||||
|
||||
if (!SwingUtilities.isEventDispatchThread()) {
|
||||
// MacOS must execute this on the EDT... It is very strict compared to win/linux
|
||||
final AtomicInteger h = new AtomicInteger(0);
|
||||
SwingUtil.invokeAndWaitQuietly(new Runnable() {
|
||||
@Override
|
||||
public
|
||||
void run() {
|
||||
h.set((int) java.awt.SystemTray.getSystemTray()
|
||||
.getTrayIconSize()
|
||||
.getHeight());
|
||||
}
|
||||
});
|
||||
height = h.get();
|
||||
} else {
|
||||
height = (int) java.awt.SystemTray.getSystemTray()
|
||||
.getTrayIconSize()
|
||||
.getHeight();
|
||||
}
|
||||
|
||||
if (height < 32) {
|
||||
// lock in at 32
|
||||
trayScalingFactor = 2;
|
||||
}
|
||||
else if ((height & (height - 1)) == 0) {
|
||||
// is this a power of 2 number? If so, we can use it
|
||||
trayScalingFactor = height/SystemTray.DEFAULT_TRAY_SIZE;
|
||||
}
|
||||
else {
|
||||
// don't know how exactly to determine this, but we are going to assume very high "HiDPI" for this...
|
||||
// the OS should go up/down as needed.
|
||||
trayScalingFactor = 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// windows, mac, linux(GtkStatusIcon) will automatically scale the tray size
|
||||
// the menu entry icon size will NOT get scaled (it will show whatever we specify)
|
||||
// we want to make sure our "scaled" size is appropriate for the OS.
|
||||
|
||||
// the DEFAULT scale is 16
|
||||
if (trayScalingFactor > 1) {
|
||||
TRAY_SIZE = (int) (SystemTray.DEFAULT_TRAY_SIZE * trayScalingFactor);
|
||||
} else {
|
||||
TRAY_SIZE = SystemTray.DEFAULT_TRAY_SIZE;
|
||||
}
|
||||
|
||||
if (menuScalingFactor > 1) {
|
||||
ENTRY_SIZE = (int) (SystemTray.DEFAULT_MENU_SIZE * menuScalingFactor);
|
||||
} else {
|
||||
ENTRY_SIZE = SystemTray.DEFAULT_MENU_SIZE;
|
||||
}
|
||||
|
||||
if (SystemTray.DEBUG) {
|
||||
SystemTray.logger.debug("ScalingFactor is '{}', tray icon size is '{}'.", trayScalingFactor, TRAY_SIZE);
|
||||
SystemTray.logger.debug("ScalingFactor is '{}', tray menu size is '{}'.", menuScalingFactor, ENTRY_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
public static
|
||||
File getTransparentImage(final int size) {
|
||||
final File newFile = new File(TEMP_DIR, size + "_empty.png").getAbsoluteFile();
|
||||
|
||||
if (newFile.canRead() && newFile.isFile()) {
|
||||
return newFile;
|
||||
}
|
||||
|
||||
// make sure the directory exists
|
||||
newFile.getParentFile().mkdirs();
|
||||
|
||||
try {
|
||||
final BufferedImage image = getTransparentImageAsBufferedImage(size);
|
||||
ImageIO.write(image, "png", newFile);
|
||||
} catch (Exception e) {
|
||||
SystemTray.logger.error("Error creating transparent image for size: {}", size, e);
|
||||
}
|
||||
|
||||
return newFile;
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public static
|
||||
BufferedImage getTransparentImageAsBufferedImage(final int size) {
|
||||
final BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = image.createGraphics();
|
||||
g2d.setColor(new Color(0,0,0,0));
|
||||
g2d.fillRect(0, 0, size, size);
|
||||
g2d.dispose();
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
private static
|
||||
File getErrorImage(final String cacheName) {
|
||||
try {
|
||||
final File save = CacheUtil.save(cacheName, ImageUtils.class.getResource("error_32.png"));
|
||||
// since it's the error file, we want to delete it on exit!
|
||||
save.deleteOnExit();
|
||||
return save;
|
||||
} catch (Exception e) {
|
||||
// this must be thrown
|
||||
throw new RuntimeException("Serious problems! Unable to extract error image, this should NEVER happen!", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static
|
||||
File getIfCachedOrError(final String cacheName) {
|
||||
try {
|
||||
final File check = CacheUtil.check(cacheName);
|
||||
if (check != null) {
|
||||
return check;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
SystemTray.logger.error("Error checking cache for information. Using error icon instead", e);
|
||||
return getErrorImage(cacheName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static synchronized
|
||||
File resizeAndCache(final int size, final File file) {
|
||||
return resizeAndCache(size, file.getAbsolutePath());
|
||||
}
|
||||
|
||||
public static synchronized
|
||||
File resizeAndCache(final int size, final String fileName) {
|
||||
if (fileName == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
FileInputStream fileInputStream = new FileInputStream(fileName);
|
||||
File file = resizeAndCache(size, fileInputStream);
|
||||
fileInputStream.close();
|
||||
|
||||
return file;
|
||||
} catch (Exception e) {
|
||||
// have to serve up the error image instead.
|
||||
SystemTray.logger.error("Error reading image. Using error icon instead", e);
|
||||
return getErrorImage(size + "default");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("Duplicates")
|
||||
public static synchronized
|
||||
File resizeAndCache(final int size, final URL imageUrl) {
|
||||
if (imageUrl == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
InputStream inputStream = imageUrl.openStream();
|
||||
File file = resizeAndCache(size, inputStream);
|
||||
inputStream.close();
|
||||
|
||||
return file;
|
||||
} catch (Exception e) {
|
||||
// have to serve up the error image instead.
|
||||
SystemTray.logger.error("Error reading image. Using error icon instead", e);
|
||||
return getErrorImage(size + "default");
|
||||
}
|
||||
}
|
||||
@SuppressWarnings("Duplicates")
|
||||
public static synchronized
|
||||
File resizeAndCache(final int size, final Image image) {
|
||||
if (image == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// stupid java won't scale it right away, so we have to do this twice to get the correct size
|
||||
final Image trayImage = new ImageIcon(image).getImage();
|
||||
trayImage.flush();
|
||||
|
||||
try {
|
||||
BufferedImage bufferedImage = getBufferedImage(trayImage);
|
||||
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
ImageIO.write(bufferedImage, "png", os);
|
||||
InputStream imageInputStream = new ByteArrayInputStream(os.toByteArray());
|
||||
|
||||
File file = resizeAndCache(size, imageInputStream);
|
||||
imageInputStream.close(); // BAOS doesn't do anything, but here for completeness + documentation
|
||||
|
||||
return file;
|
||||
} catch (Exception e) {
|
||||
// have to serve up the error image instead.
|
||||
SystemTray.logger.error("Error reading image. Using error icon instead", e);
|
||||
return getErrorImage(size + "default");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("Duplicates")
|
||||
public static synchronized
|
||||
File resizeAndCache(final int size, final ImageInputStream imageStream) {
|
||||
if (imageStream == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
ByteArrayOutputStream byteArrayOutputStream = IO.copyStream(imageStream);
|
||||
return resizeAndCache(size, new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
|
||||
} catch (Exception e) {
|
||||
// have to serve up the error image instead.
|
||||
SystemTray.logger.error("Error reading image. Using error icon instead", e);
|
||||
return getErrorImage(size + "default");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("Duplicates")
|
||||
public static synchronized
|
||||
File resizeAndCache(final int size, InputStream imageStream) {
|
||||
if (imageStream == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(imageStream instanceof ByteArrayInputStream)) {
|
||||
// have to make a copy of the inputStream, but only if necessary
|
||||
try {
|
||||
ByteArrayOutputStream byteArrayOutputStream = IO.copyStream(imageStream);
|
||||
imageStream.close();
|
||||
|
||||
imageStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
|
||||
} catch (Exception e) {
|
||||
// this must be thrown
|
||||
throw new RuntimeException("Unable to read from inputStream.", e);
|
||||
}
|
||||
}
|
||||
imageStream.mark(0);
|
||||
|
||||
// check if we already have this file information saved to disk, based on size + hash of data
|
||||
final String cacheName = size + "_" + CacheUtil.createNameAsHash(imageStream);
|
||||
((ByteArrayInputStream) imageStream).reset(); // casting to avoid unnecessary try/catch for IOException
|
||||
|
||||
|
||||
// if we already have this fileName, reuse it
|
||||
final File check = getIfCachedOrError(cacheName);
|
||||
if (check != null) {
|
||||
return check;
|
||||
}
|
||||
|
||||
// no cached file, so we resize then save the new one.
|
||||
boolean needsResize = true;
|
||||
try {
|
||||
imageStream.mark(0);
|
||||
Dimension imageSize = getImageSize(imageStream);
|
||||
//noinspection NumericCastThatLosesPrecision
|
||||
if (size == ((int) imageSize.getWidth()) && size == ((int) imageSize.getHeight())) {
|
||||
// we can reuse this URL (it's the correct size).
|
||||
needsResize = false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// have to serve up the error image instead.
|
||||
SystemTray.logger.error("Error resizing image. Using error icon instead", e);
|
||||
return getErrorImage(cacheName);
|
||||
} finally {
|
||||
((ByteArrayInputStream) imageStream).reset(); // casting to avoid unnecessary try/catch for IOException
|
||||
}
|
||||
|
||||
if (needsResize) {
|
||||
// we have to hop through hoops.
|
||||
try {
|
||||
File resizedFile = resizeFileNoCheck(size, imageStream);
|
||||
|
||||
// now cache that file
|
||||
try {
|
||||
return CacheUtil.save(cacheName, resizedFile);
|
||||
} catch (Exception e) {
|
||||
// have to serve up the error image instead.
|
||||
SystemTray.logger.error("Error caching image. Using error icon instead", e);
|
||||
return getErrorImage(cacheName);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
// have to serve up the error image instead.
|
||||
SystemTray.logger.error("Error resizing image. Using error icon instead", e);
|
||||
return getErrorImage(cacheName);
|
||||
}
|
||||
|
||||
} else {
|
||||
// no resize necessary, just cache as is.
|
||||
try {
|
||||
return CacheUtil.save(cacheName, imageStream);
|
||||
} catch (Exception e) {
|
||||
// have to serve up the error image instead.
|
||||
SystemTray.logger.error("Error caching image. Using error icon instead", e);
|
||||
return getErrorImage(cacheName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the given URL to the specified size. No checks are performed if it's the correct size to begin with.
|
||||
*
|
||||
* @return the file on disk that is the resized icon
|
||||
*/
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
private static
|
||||
File resizeFileNoCheck(final int size, final URL imageUrl) throws IOException {
|
||||
String extension = FileUtil.getExtension(imageUrl.getPath());
|
||||
if (extension.isEmpty()) {
|
||||
extension = "png"; // made up
|
||||
}
|
||||
|
||||
InputStream inputStream = imageUrl.openStream();
|
||||
|
||||
// have to resize the file (and return the new path)
|
||||
|
||||
// now have to resize this file.
|
||||
File newFile = new File(TEMP_DIR, "temp_resize." + extension).getAbsoluteFile();
|
||||
Image image;
|
||||
|
||||
|
||||
// resize the image, keep aspect
|
||||
image = new ImageIcon(ImageIO.read(inputStream)).getImage().getScaledInstance(size, -1, Image.SCALE_SMOOTH);
|
||||
image.flush();
|
||||
|
||||
// have to do this twice, so that it will finish loading the image (weird callback stuff is required if we don't do this)
|
||||
image = new ImageIcon(image).getImage();
|
||||
image.flush();
|
||||
|
||||
// make whatever dirs we need to.
|
||||
newFile.getParentFile().mkdirs();
|
||||
|
||||
// if it's already there, we have to delete it
|
||||
newFile.delete();
|
||||
|
||||
// now write out the new one
|
||||
BufferedImage bufferedImage = getBufferedImage(image);
|
||||
ImageIO.write(bufferedImage, extension, newFile);
|
||||
|
||||
return newFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the given InputStream to the specified size. No checks are performed if it's the correct size to begin with.
|
||||
*
|
||||
* @return the file on disk that is the resized icon
|
||||
*/
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
private static
|
||||
File resizeFileNoCheck(final int size, InputStream inputStream) throws IOException {
|
||||
// have to resize the file (and return the new path)
|
||||
|
||||
// now have to resize this file.
|
||||
File newFile = new File(TEMP_DIR, "temp_resize.png").getAbsoluteFile();
|
||||
Image image;
|
||||
|
||||
|
||||
// resize the image, keep aspect
|
||||
image = new ImageIcon(ImageIO.read(inputStream)).getImage().getScaledInstance(size, -1, Image.SCALE_SMOOTH);
|
||||
image.flush();
|
||||
|
||||
// have to do this twice, so that it will finish loading the image (weird callback stuff is required if we don't do this)
|
||||
image = new ImageIcon(image).getImage();
|
||||
image.flush();
|
||||
|
||||
// make whatever dirs we need to.
|
||||
newFile.getParentFile().mkdirs();
|
||||
|
||||
// if it's already there, we have to delete it
|
||||
newFile.delete();
|
||||
|
||||
// now write out the new one
|
||||
BufferedImage bufferedImage = getBufferedImage(image);
|
||||
ImageIO.write(bufferedImage, "png", newFile); // made up extension
|
||||
|
||||
return newFile;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resizes the image (as a FILE on disk, or as a RESOURCE name), saves it as a file on disk. This file will be OVER-WRITTEN by any
|
||||
* operation that calls this method.
|
||||
*
|
||||
* @return the file string on disk that is the resized icon
|
||||
*/
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
private static
|
||||
String resizeFile(final int size, final String fileName) throws IOException {
|
||||
FileInputStream fileInputStream = new FileInputStream(fileName);
|
||||
|
||||
Dimension imageSize = getImageSize(fileInputStream);
|
||||
//noinspection NumericCastThatLosesPrecision
|
||||
if (size == ((int) imageSize.getWidth()) && size == ((int) imageSize.getHeight())) {
|
||||
// we can reuse this file.
|
||||
return fileName;
|
||||
}
|
||||
|
||||
// have to resize the file (and return the new path)
|
||||
|
||||
String extension = FileUtil.getExtension(fileName);
|
||||
if (extension.isEmpty()) {
|
||||
extension = "png"; // made up
|
||||
}
|
||||
|
||||
// now have to resize this file.
|
||||
File newFile = new File(TEMP_DIR, "temp_resize." + extension).getAbsoluteFile();
|
||||
Image image;
|
||||
|
||||
// is file sitting on drive
|
||||
File iconTest = new File(fileName);
|
||||
if (iconTest.isFile() && iconTest.canRead()) {
|
||||
final String absolutePath = iconTest.getAbsolutePath();
|
||||
|
||||
// resize the image, keep aspect
|
||||
image = new ImageIcon(absolutePath).getImage().getScaledInstance(size, -1, Image.SCALE_SMOOTH);
|
||||
image.flush();
|
||||
}
|
||||
else {
|
||||
// suck it out of a URL/Resource (with debugging if necessary)
|
||||
final URL systemResource = LocationResolver.getResource(fileName);
|
||||
|
||||
// resize the image, keep aspect
|
||||
image = new ImageIcon(systemResource).getImage().getScaledInstance(size, -1, Image.SCALE_SMOOTH);
|
||||
image.flush();
|
||||
}
|
||||
|
||||
// have to do this twice, so that it will finish loading the image (weird callback stuff is required if we don't do this)
|
||||
image = new ImageIcon(image).getImage();
|
||||
image.flush();
|
||||
|
||||
// make whatever dirs we need to.
|
||||
newFile.getParentFile().mkdirs();
|
||||
|
||||
// if it's already there, we have to delete it
|
||||
newFile.delete();
|
||||
|
||||
|
||||
// now write out the new one
|
||||
BufferedImage bufferedImage = getBufferedImage(image);
|
||||
ImageIO.write(bufferedImage, extension, newFile);
|
||||
|
||||
return newFile.getAbsolutePath();
|
||||
}
|
||||
|
||||
private static
|
||||
BufferedImage getBufferedImage(Image image) {
|
||||
if (image instanceof BufferedImage) {
|
||||
return (BufferedImage) image;
|
||||
}
|
||||
|
||||
BufferedImage bimage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB);
|
||||
|
||||
Graphics2D bGr = bimage.createGraphics();
|
||||
bGr.addRenderingHints(new RenderingHints(RenderingHints.KEY_RENDERING,
|
||||
RenderingHints.VALUE_RENDER_QUALITY));
|
||||
bGr.drawImage(image, 0, 0, null);
|
||||
bGr.dispose();
|
||||
|
||||
// Return the buffered image
|
||||
return bimage;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reads the image size information from the specified file, without loading the entire file.
|
||||
*
|
||||
* @param fileStream the input stream of the file
|
||||
*
|
||||
* @return the image size dimensions. IOException if it could not be read
|
||||
*/
|
||||
private static
|
||||
Dimension getImageSize(InputStream fileStream) throws IOException {
|
||||
ImageInputStream in = null;
|
||||
ImageReader reader = null;
|
||||
try {
|
||||
// This will ONLY work for File, InputStream, and RandomAccessFile
|
||||
in = ImageIO.createImageInputStream(fileStream);
|
||||
|
||||
final Iterator<ImageReader> readers = ImageIO.getImageReaders(in);
|
||||
if (readers.hasNext()) {
|
||||
reader = readers.next();
|
||||
reader.setInput(in);
|
||||
|
||||
return new Dimension(reader.getWidth(0), reader.getHeight(0));
|
||||
}
|
||||
} finally {
|
||||
// `ImageInputStream` is not a closeable in 1.6, so we do this manually.
|
||||
if (in != null) {
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
if (reader != null) {
|
||||
reader.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
throw new IOException("Unable to read file inputStream for image size data.");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user