/* * Copyright 2017 Laszlo Balazs-Csiki * * This file is part of Pixelitor. Pixelitor is free software: you * can redistribute it and/or modify it under the terms of the GNU * General Public License, version 3 as published by the Free * Software Foundation. * * Pixelitor is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Pixelitor. If not, see <http://www.gnu.org/licenses/>. */ package pixelitor.utils; import pixelitor.AppLogic; import pixelitor.Build; import pixelitor.Composition; import pixelitor.filters.gui.BooleanParam; import pixelitor.filters.gui.FilterSetting; import pixelitor.gui.BlendingModePanel; import pixelitor.gui.ImageComponent; import pixelitor.gui.ImageComponents; import pixelitor.gui.PixelitorWindow; import pixelitor.gui.utils.SliderSpinner; import pixelitor.utils.test.RandomGUITest; import javax.swing.*; import javax.swing.event.ListDataEvent; import javax.swing.event.ListDataListener; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.Shape; import java.awt.Toolkit; import java.awt.Transparency; import java.awt.color.ColorSpace; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import java.awt.event.KeyEvent; import java.awt.geom.GeneralPath; import java.awt.geom.Path2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.ComponentColorModel; import java.awt.image.DataBuffer; import java.awt.image.DirectColorModel; import java.awt.image.Raster; import java.awt.image.WritableRaster; import java.text.NumberFormat; import java.text.ParseException; import java.util.Locale; import java.util.Optional; import java.util.Random; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import static java.awt.image.BufferedImage.TYPE_4BYTE_ABGR_PRE; public final class Utils { private static final int BYTES_IN_1_KILOBYTE = 1_024; private static final int BYTES_IN_1_MEGABYTE = 1_048_576; private static final Cursor BUSY_CURSOR = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR); private static final Cursor DEFAULT_CURSOR = Cursor.getDefaultCursor(); private static final int WAIT_CURSOR_DELAY = 300; // in milliseconds /** * Utility class with static methods */ private Utils() { } public static void executeWithBusyCursor(Runnable task) { executeWithBusyCursor(PixelitorWindow.getInstance(), task); } /** * Executes a task with busy cursor */ public static void executeWithBusyCursor(Component parent, Runnable task) { Timer timer = new Timer(); TimerTask startBusyCursorTask = new TimerTask() { @Override public void run() { parent.setCursor(BUSY_CURSOR); } }; try { // if after WAIT_CURSOR_DELAY the original task is still running, // set the cursor to the delay cursor timer.schedule(startBusyCursorTask, WAIT_CURSOR_DELAY); task.run(); // on the current thread! } finally { // when the original task has stopped running, the cursor is reset timer.cancel(); parent.setCursor(DEFAULT_CURSOR); } } public static void setShowOriginal(boolean b) { Composition comp = ImageComponents.getActiveCompOrNull(); comp.getActiveDrawable().setShowOriginal(b); } /** * Replaces all the special characters in s string with an underscore */ public static String toFileName(String s) { return s.replaceAll("[^A-Za-z0-9_]", "_"); } public static void randomizeGUIWidgetsOn(JPanel panel) { int count = panel.getComponentCount(); Random rand = new Random(); for (int i = 0; i < count; i++) { Component child = panel.getComponent(i); //noinspection ChainOfInstanceofChecks if (child instanceof JComboBox) { @SuppressWarnings("rawtypes") JComboBox box = (JComboBox) child; int itemCount = box.getItemCount(); box.setSelectedIndex(rand.nextInt(itemCount)); } else if (child instanceof JCheckBox) { JCheckBox box = (JCheckBox) child; box.setSelected(rand.nextBoolean()); } else if (child instanceof SliderSpinner) { SliderSpinner spinner = (SliderSpinner) child; spinner.getModel().randomize(); } else if (child instanceof BlendingModePanel) { BlendingModePanel bmp = (BlendingModePanel) child; bmp.randomize(); } } } public static String float2String(float f) { if (f == 0.0f) { return ""; } return String.format("%.3f", f); } public static float string2float(String s) throws NotANumberException { String trimmed = s.trim(); if (trimmed.isEmpty()) { return 0.0f; } NumberFormat nf = NumberFormat.getInstance(); Number number; try { // try locale-specific parsing number = nf.parse(trimmed); } catch (ParseException e) { NumberFormat englishFormat = NumberFormat.getInstance(Locale.ENGLISH); try { // second chance: english number = englishFormat.parse(trimmed); } catch (ParseException e1) { throw new NotANumberException(s); } } return number.floatValue(); } public static void throwTestException() { if (Build.CURRENT != Build.FINAL) { throw new IllegalStateException("Test"); } } public static String bytesToString(int bytes) { if (bytes < BYTES_IN_1_KILOBYTE) { return bytes + " bytes"; } else if (bytes < BYTES_IN_1_MEGABYTE) { float kiloBytes = ((float) bytes) / BYTES_IN_1_KILOBYTE; return String.format("%.2f kilobytes", kiloBytes); } else { float megaBytes = ((float) bytes) / BYTES_IN_1_MEGABYTE; return String.format("%.2f megabytes", megaBytes); } } public static int getMaxHeapInMegabytes() { long heapMaxSize = Runtime.getRuntime().maxMemory(); return (int) (heapMaxSize / BYTES_IN_1_MEGABYTE); } public static int getUsedMemoryInMegabytes() { long usedMemory = Runtime.getRuntime().totalMemory(); return (int) (usedMemory / BYTES_IN_1_MEGABYTE); } @SuppressWarnings("SameReturnValue") // used in asserts public static boolean checkRasterMinimum(BufferedImage newImage) { if (RandomGUITest.isRunning()) { WritableRaster raster = newImage.getRaster(); if ((raster.getMinX() != 0) || (raster.getMinY() != 0)) { throw new IllegalArgumentException("Raster " + raster + " has minX or minY not equal to zero: " + raster.getMinX() + ' ' + raster.getMinY()); } } return true; } public static void copyStringToClipboard(String text) { Transferable stringSelection = new StringSelection(text); Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(stringSelection, null); } public static ProgressMonitor createPercentageProgressMonitor(String message) { return new ProgressMonitor(PixelitorWindow.getInstance(), message, "", 0, 100); } public static ProgressMonitor createPercentageProgressMonitor(String message, String cancelButtonText) { String oldText = UIManager.getString("OptionPane.cancelButtonText"); UIManager.put("OptionPane.cancelButtonText", cancelButtonText); ProgressMonitor pm = new ProgressMonitor(PixelitorWindow.getInstance(), message, "", 0, 100); UIManager.put("OptionPane.cancelButtonText", oldText); return pm; } public static double transformAtan2AngleToIntuitive(double angleInRadians) { double angle; if (angleInRadians <= 0) { angle = -angleInRadians; } else { angle = Math.PI * 2 - angleInRadians; } return angle; } public static Point2D calculateOffset(double distance, double angle) { double offsetX = distance * Math.cos(angle); double offsetY = distance * Math.sin(angle); return new Point2D.Double(offsetX, offsetY); } // makes sure that the returned rectangle has positive width, height public static Rectangle toPositiveRect(int x1, int x2, int y1, int y2) { int topX, topY, width, height; if (x2 >= x1) { topX = x1; width = x2 - x1; } else { topX = x2; width = x1 - x2; } if (y2 >= y1) { topY = y1; height = y2 - y1; } else { topY = y2; height = y1 - y2; } return new Rectangle(topX, topY, width, height); } // makes sure that the returned rectangle has positive width, height public static Rectangle2D toPositiveRect(Rectangle2D input) { double inX = input.getX(); double inY = input.getY(); double inWidth = input.getWidth(); double inHeight = input.getHeight(); if (inWidth >= 0) { if (inHeight >= 0) { return input; // should be the most common case } else { // negative height double newY = inY + inHeight; return new Rectangle2D.Double(inX, newY, inWidth, -inHeight); } } else { // negative width if (inHeight >= 0) { double newX = inX + inWidth; return new Rectangle2D.Double(newX, inY, -inWidth, inHeight); } else { // negative height double newX = inX + inWidth; double newY = inY + inHeight; return new Rectangle2D.Double(newX, newY, -inWidth, -inHeight); } } } public static float parseFloat(String input, float defaultValue) { if ((input != null) && !input.isEmpty()) { return Float.parseFloat(input); } return defaultValue; } public static int parseInt(String input, int defaultValue) { if ((input != null) && !input.isEmpty()) { return Integer.parseInt(input); } return defaultValue; } @SuppressWarnings("WeakerAccess") public static void debugImage(BufferedImage img) { debugImage(img, "Debug"); } @SuppressWarnings("WeakerAccess") public static void debugImage(BufferedImage img, String name) { BufferedImage copy = ImageUtils.copyImage(img); ImageComponent savedIC = ImageComponents.getActiveIC(); Optional<Composition> debugCompOpt = ImageComponents.findCompositionByName(name); if (debugCompOpt.isPresent()) { // if we already have a debug composition, simply replace the image Composition comp = debugCompOpt.get(); comp.getActiveDrawable().setImage(copy); comp.repaint(); } else { Composition comp = Composition.fromImage(copy, null, name); AppLogic.addCompAsNewImage(comp); } if (savedIC != null) { ImageComponents.setActiveIC(savedIC, true); } } public static void debugShape(Shape shape, String name) { // create a copy Shape shapeCopy = new Path2D.Double(shape); Rectangle shapeBounds = shape.getBounds(); int imgWidth = shapeBounds.x + shapeBounds.width + 50; int imgHeight = shapeBounds.y + shapeBounds.height + 50; BufferedImage img = ImageUtils.createSysCompatibleImage(imgWidth, imgHeight); Graphics2D g = img.createGraphics(); g.setColor(Color.WHITE); g.fillRect(0, 0, imgWidth, imgHeight); g.setColor(Color.BLACK); g.setStroke(new BasicStroke(3)); g.draw(shapeCopy); g.dispose(); debugImage(img, name); } public static void debugRaster(Raster raster, String name) { ColorModel colorModel; int numBands = raster.getNumBands(); if (numBands == 4) { // normal color image colorModel = new DirectColorModel( ColorSpace.getInstance(ColorSpace.CS_sRGB), 32, 0x00ff0000,// Red 0x0000ff00,// Green 0x000000ff,// Blue 0xff000000,// Alpha true, // Alpha Premultiplied DataBuffer.TYPE_INT ); } else if (numBands == 1) { // grayscale image ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY); int[] nBits = {8}; colorModel = new ComponentColorModel(cs, nBits, false, true, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); } else { throw new IllegalStateException("numBands = " + numBands); } Raster correctlyTranslated = raster.createChild(raster.getMinX(), raster.getMinY(), raster.getWidth(), raster.getHeight(), 0, 0, null); BufferedImage debugImage = new BufferedImage(colorModel, (WritableRaster) correctlyTranslated, true, null); debugImage(debugImage, name); } public static void debugRasterWithEmptySpace(Raster raster) { BufferedImage debugImage = new BufferedImage(raster.getMinX() + raster.getWidth(), raster.getMinY() + raster.getHeight(), TYPE_4BYTE_ABGR_PRE); debugImage.setData(raster); debugImage(debugImage); } @SuppressWarnings("SuspiciousNameCombination") public static GeneralPath createUnitArrow() { float arrowWidth = 0.3f; float arrowHeadWidth = 0.7f; float arrowHeadStart = 0.6f; float halfArrowWidth = arrowWidth / 2.0f; float halfArrowHeadWidth = arrowHeadWidth / 2; GeneralPath unitArrow = new GeneralPath(); unitArrow.moveTo(0.0f, -halfArrowWidth); unitArrow.lineTo(0.0f, halfArrowWidth); unitArrow.lineTo(arrowHeadStart, halfArrowWidth); unitArrow.lineTo(arrowHeadStart, halfArrowHeadWidth); unitArrow.lineTo(1.0f, 0.0f); unitArrow.lineTo(arrowHeadStart, -halfArrowHeadWidth); unitArrow.lineTo(arrowHeadStart, -halfArrowWidth); unitArrow.closePath(); return unitArrow; } public static String getRandomString(int length) { char[] chars = "abcdefghijklmnopqrstuvwxyz -".toCharArray(); StringBuilder sb = new StringBuilder(length); Random random = new Random(); for (int i = 0; i < length; i++) { char c = chars[random.nextInt(chars.length)]; sb.append(c); } return sb.toString(); } public static void checkThatAssertionsAreEnabled() { boolean assertsEnabled = false; //noinspection AssertWithSideEffects assert assertsEnabled = true; if (!assertsEnabled) { throw new IllegalStateException("assertions not enabled"); } } @VisibleForTesting public static void sleep(int duration, TimeUnit unit) { try { Thread.sleep(unit.toMillis(duration)); } catch (InterruptedException e) { throw new IllegalStateException("interrupted!"); } } public static String keystrokeAsText(KeyStroke keyStroke) { String s = ""; int modifiers = keyStroke.getModifiers(); if (modifiers > 0) { s = KeyEvent.getKeyModifiersText(modifiers); s += " "; } int keyCode = keyStroke.getKeyCode(); if (keyCode != 0) { s += KeyEvent.getKeyText(keyCode); } else { s += keyStroke.getKeyChar(); } return s; } public static String formatMillis(long millis) { long seconds = millis / 1000; long s = seconds % 60; long m = (seconds / 60) % 60; long h = (seconds / (60 * 60)) % 24; return String.format("%d:%02d:%02d", h, m, s); } public static boolean callingClassIs(String name) { // designed to be used in assertions: // it checks the caller of the caller String callingClassName = new Exception().getStackTrace()[2].getClassName(); return callingClassName.contains(name); } public static <T> void setupDisableOtherIf(ComboBoxModel<T> current, FilterSetting other, Predicate<T> condition) { current.addListDataListener(new ListDataListener() { @Override public void intervalAdded(ListDataEvent e) { } @Override public void intervalRemoved(ListDataEvent e) { } @Override public void contentsChanged(ListDataEvent e) { if (condition.test((T) current.getSelectedItem())) { other.setEnabled(false, FilterSetting.EnabledReason.APP_LOGIC); } else { other.setEnabled(true, FilterSetting.EnabledReason.APP_LOGIC); } } }); } public static <T> void setupEnableOtherIf(ComboBoxModel<T> current, FilterSetting other, Predicate<T> condition) { other.setEnabled(false, FilterSetting.EnabledReason.APP_LOGIC); current.addListDataListener(new ListDataListener() { @Override public void intervalAdded(ListDataEvent e) { } @Override public void intervalRemoved(ListDataEvent e) { } @Override public void contentsChanged(ListDataEvent e) { if (condition.test((T) current.getSelectedItem())) { other.setEnabled(true, FilterSetting.EnabledReason.APP_LOGIC); } else { other.setEnabled(false, FilterSetting.EnabledReason.APP_LOGIC); } } }); } public static void setupDisableOtherIf(BooleanParam current, FilterSetting other, Predicate<Boolean> condition) { current.addChangeListener(e -> { if (condition.test(current.isChecked())) { other.setEnabled(false, FilterSetting.EnabledReason.APP_LOGIC); } else { other.setEnabled(true, FilterSetting.EnabledReason.APP_LOGIC); } }); } public static void setupEnableOtherIf(BooleanParam current, FilterSetting other, Predicate<Boolean> condition) { other.setEnabled(false, FilterSetting.EnabledReason.APP_LOGIC); current.addChangeListener(e -> { if (condition.test(current.isChecked())) { other.setEnabled(true, FilterSetting.EnabledReason.APP_LOGIC); } else { other.setEnabled(false, FilterSetting.EnabledReason.APP_LOGIC); } }); } /** * Creates the name of the duplicated layers and compositions */ public static String createCopyName(String orig) { String copyString = "copy"; // could be longer or shorter in other languages int copyStringLength = copyString.length(); int index = orig.lastIndexOf(copyString); if (index == -1) { return orig + ' ' + copyString; } if (index == orig.length() - copyStringLength) { // it ends with the copyString - this was the first copy return orig + " 2"; } String afterCopyString = orig.substring(index + copyStringLength); int copyNr; try { copyNr = Integer.parseInt(afterCopyString.trim()); } catch (NumberFormatException e) { // the part after copy was not a number... return orig + ' ' + copyString; } copyNr++; return orig.substring(0, index + copyStringLength) + ' ' + copyNr; } }