/** * Copyright (C) 2001-2017 by RapidMiner and the contributors * * Complete list of developers available at our web site: * * http://rapidminer.com * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This program 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 * Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.gui.tools; import java.awt.AlphaComposite; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Font; import java.awt.GradientPaint; import java.awt.Graphics2D; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.HeadlessException; import java.awt.Image; import java.awt.Insets; import java.awt.Point; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.Window; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.nio.file.Path; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import javax.swing.BorderFactory; import javax.swing.ImageIcon; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.filechooser.FileFilter; import javax.swing.filechooser.FileSystemView; import com.rapidminer.RapidMiner; import com.rapidminer.gui.ApplicationFrame; import com.rapidminer.gui.MainFrame; import com.rapidminer.gui.RapidMinerGUI; import com.rapidminer.gui.look.fc.Bookmark; import com.rapidminer.gui.look.fc.BookmarkIO; import com.rapidminer.gui.tools.components.ToolTipWindow; import com.rapidminer.gui.tools.components.ToolTipWindow.TipProvider; import com.rapidminer.gui.tools.components.ToolTipWindow.TooltipLocation; import com.rapidminer.gui.tools.dialogs.ConfirmDialog; import com.rapidminer.gui.tools.dialogs.ErrorDialog; import com.rapidminer.gui.tools.dialogs.ExtendedErrorDialog; import com.rapidminer.gui.tools.dialogs.InputDialog; import com.rapidminer.gui.tools.dialogs.InputValidator; import com.rapidminer.gui.tools.dialogs.LongMessageDialog; import com.rapidminer.gui.tools.dialogs.MessageDialog; import com.rapidminer.gui.tools.dialogs.RepositoryEntryInputDialog; import com.rapidminer.gui.tools.dialogs.ResultViewDialog; import com.rapidminer.gui.tools.dialogs.SelectionInputDialog; import com.rapidminer.gui.tools.syntax.SyntaxStyle; import com.rapidminer.gui.tools.syntax.SyntaxUtilities; import com.rapidminer.gui.tools.syntax.TextAreaDefaults; import com.rapidminer.gui.tools.syntax.Token; import com.rapidminer.operator.Operator; import com.rapidminer.operator.OperatorDescription; import com.rapidminer.operator.ports.Port; import com.rapidminer.operator.ports.Ports; import com.rapidminer.tools.FileSystemService; import com.rapidminer.tools.I18N; import com.rapidminer.tools.LogService; import com.rapidminer.tools.ParameterService; import com.rapidminer.tools.ParentResolvingMap; import com.rapidminer.tools.StringColorMap; import com.rapidminer.tools.SystemInfoUtilities; import com.rapidminer.tools.SystemInfoUtilities.OperatingSystem; import com.rapidminer.tools.Tools; import com.rapidminer.tools.plugin.Plugin; import com.rapidminer.tools.usagestats.ActionStatisticsCollector; /** * This helper class provides some static methods and properties which might be useful for several * GUI classes. These methods include * <ul> * <li>the creation of gradient paints</li> * <li>displaying (simple) error messages</li> * <li>creation of file chosers</li> * <li>creation of text panels</li> * <li>escaping HTML messages</li> * </ul> * * @author Ingo Mierswa */ public class SwingTools { /** * A container class for results generated by an implementation of {@link ResultRunnable}. */ private static class ResultContainer<T> { public T value; } /** * A simple interface which has a run method that returns a result. * * @since 6.5.0 * */ public static interface ResultRunnable<T> { T run(); } /** whether we are on a Mac or not */ private static final boolean IS_MAC = SystemInfoUtilities.getOperatingSystem() == OperatingSystem.OSX; /** Defines the maximal length of characters in a line of the tool tip text. */ private static final int TOOL_TIP_LINE_LENGTH = 100; /** Defines the extra height for each row in a table. */ public static final int TABLE_ROW_EXTRA_HEIGHT = 4; /** * Defines the extra height for rows in a table with components. If an {@link ExtendedJTable} is * used, this amount can be added additionally to the amount of {@link #TABLE_ROW_EXTRA_HEIGHT} * which is already added in the constructor. */ public static final int TABLE_WITH_COMPONENTS_ROW_EXTRA_HEIGHT = 10; /** Some color constants for Java Look and Feel. */ public static final Color DARKEST_YELLOW = new Color(250, 219, 172); /** Some color constants for Java Look and Feel. */ public static final Color DARK_YELLOW = new Color(250, 226, 190); /** Some color constants for Java Look and Feel. */ public static final Color LIGHT_YELLOW = new Color(250, 233, 207); /** Some color constants for Java Look and Feel. */ public static final Color LIGHTEST_YELLOW = new Color(250, 240, 225); /** Some color constants for Java Look and Feel. */ public static final Color TRANSPARENT_YELLOW = new Color(255, 245, 230, 190); /** Some color constants for Java Look and Feel. */ public static final Color VERY_DARK_BLUE = new Color(172, 172, 212); /** Some color constants for Java Look and Feel. */ public static final Color DARK_GREEN = new Color(0, 130, 50); /** Some color constants for Java Look and Feel. */ public static final Color DARKEST_BLUE = new Color(182, 202, 242); /** Some color constants for Java Look and Feel. */ public static final Color DARK_BLUE = new Color(199, 213, 242); /** Some color constants for Java Look and Feel. */ public static final Color LIGHT_BLUE = new Color(216, 224, 242); /** Some color constants for Java Look and Feel. */ public static final Color LIGHTEST_BLUE = new Color(233, 236, 242); /** Some color constants for Java Look and Feel. */ public static final Color FAINT_YELLOW = new Color(250, 240, 225, 150); /** Some color constants for Java Look and Feel. */ public static final Color FAINTER_YELLOW = new Color(255, 245, 230, 50); /** Some color constants for Java Look and Feel. */ public static final Color FAINT_BLUE = new Color(225, 230, 235, 150); /** Some color constants for Java Look and Feel. */ public static final Color FAINTER_BLUE = new Color(230, 235, 240, 50); /** * The RapidMiner orange color. * * @deprecated since RapidMiner 6.0, replaced by RAPIDMINER_ORANGE */ @Deprecated public static final Color RAPID_I_ORANGE = new Color(242, 146, 0); /** * The RapidMiner brown color. * * @deprecated since RapidMiner 6.0, replaced by RAPIDMINER_GRAY */ @Deprecated public static final Color RAPID_I_BROWN = new Color(97, 66, 11); /** * The RapidMiner beige color. * * @deprecated since RapidMiner 6.0, replaced by RAPIDMINER_LIGHT_GRAY */ @Deprecated public static final Color RAPID_I_BEIGE = new Color(202, 188, 165); /** The RapidMiner Orange. */ public static final Color RAPIDMINER_ORANGE = new Color(241, 96, 34); /** The RapidMiner Light Orange. */ public static final Color RAPIDMINER_LIGHT_ORANGE = new Color(235, 122, 3); /** The RapidMiner Yellow. */ public static final Color RAPIDMINER_YELLOW = new Color(244, 232, 11); /** The RapidMiner Orange. */ public static final Color RAPIDMINER_GRAY = new Color(50, 53, 62); /** The RapidMiner Light Orange. */ public static final Color RAPIDMINER_LIGHT_GRAY = new Color(121, 124, 130); /** Some color constants for Java Look and Feel. */ public static final Color LIGHTEST_RED = new Color(250, 210, 210); /** A brown font color. */ public static final Color BROWN_FONT_COLOR = new Color(63, 53, 24); /** A brown font color. */ public static final Color LIGHT_BROWN_FONT_COLOR = new Color(113, 103, 74); /** This set stores all lookup paths for icons */ private static Set<String> iconPaths = new LinkedHashSet<>(Collections.singleton("icons/")); /** Contains the small frame icons in all possible sizes. */ private static List<Image> allFrameIcons = new LinkedList<>(); private static FrameIconProvider frameIconProvider; private static final String DEFAULT_FRAME_ICON_BASE_NAME = "rapidminer_frame_icon_"; private static ParentResolvingMap<String, Color> GROUP_TO_COLOR_MAP = new StringColorMap(); /** step width used when cropping a string */ private static final int RESIZE_STEP_WIDTH = 3; /** separator used between two parts of a cropped string */ public static final String SEPARATOR = "[...]"; /** delay before showing the tool tip for help icons */ private static final int TOOL_TIP_DELAY = 80; /** * the path to the icon that is used when displaying help texts, the icon itself cannot be a * constant since icons are cached and the cache is only initialized later */ private static final String HELP_ICON_PATH = "13/" + I18N.getGUIMessage("gui.label.operator_pararameters.help_icon"); /** * the property that can be used on windows to disable clear type for a component, see * {@link #disableClearType(JComponent)} */ private static final Object AA_TEXT_PROPERTY = getAaTextProperty(); /** The replacement icon for missing icons */ private static final String UNKNOWN_ICON_NAME = "symbol_questionmark.png"; private static final URL UNKNOWN_ICON_URL = Tools.getResource("icons/16/" + UNKNOWN_ICON_NAME); private static final ImageIcon UNKNOWN_ICON = new ImageIcon(UNKNOWN_ICON_URL); /** The prefix for mono color icons */ private static final String MONO_COLOR_ICON_PREFIX = "/mono"; /** The prefix for retina icons */ private static final String RETINA_ICON_PREFIX = "/@2x"; private static enum Scaling { DEFAULT, RETINA } private static Scaling scaling = Scaling.DEFAULT; static { setupFrameIcons(DEFAULT_FRAME_ICON_BASE_NAME); try { GROUP_TO_COLOR_MAP.parseProperties("com/rapidminer/resources/groups.properties", "group.", ".color", OperatorDescription.class.getClassLoader(), null); } catch (IOException e) { LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.tools.SwingTools.loading_operator_group_colors_error"); } // Detect apple retina display. Will return 2.0 with retina, 1.0 without and null if not // running an apple system. if (isRetina()) { scaling = Scaling.RETINA; } } public static void setFrameIconProvider(final FrameIconProvider _frameIconProvider) { frameIconProvider = _frameIconProvider; reloadFrameIcons(); } public static void setupFrameIcons(final String frameIconBaseName) { setFrameIconProvider(new DefaultFrameIconProvider(frameIconBaseName)); reloadFrameIcons(); } private static void reloadFrameIcons() { if (frameIconProvider == null) { allFrameIcons = new LinkedList<>(); } else { allFrameIcons = frameIconProvider.getFrameIcons(); } } /** * Tries to determine whether we are on an Apple Retina display. * * @return {@code true} iff we are on OS X and a scaling factor is set */ private static boolean isRetina() { if (SystemInfoUtilities.getOperatingSystem() == OperatingSystem.OSX) { try { // The lines below might throw a HeadlessException. Do not move outside of try // block! GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); final GraphicsDevice device = env.getDefaultScreenDevice(); Field field = device.getClass().getDeclaredField("scale"); if (field != null) { field.setAccessible(true); Object scale = field.get(device); // On OS X the scale field contains the scaling factor of the device (e.g., 2 // for '@2x' Retina devices). if (scale instanceof Integer && ((Integer) scale).intValue() >= 2) { return true; } } } catch (HeadlessException headless) { // we do not care about scaling factors on headless systems } catch (Exception e) { LogService.getRoot().log(Level.INFO, "com.rapidminer.gui.tools.SwingTools.retina_detection_error", e); } } return false; } /** * Returns the list of all available program icon sizes. */ public static List<Image> getFrameIconList() { return allFrameIcons; } /** Returns the list of all possible frame icons. */ public static void setFrameIcon(final JFrame frame) { Method iconImageMethod = null; try { iconImageMethod = frame.getClass().getMethod("setIconImages", new Class[] { List.class }); } catch (Throwable e) { // ignore this and use single small icon below } if (iconImageMethod != null) { try { iconImageMethod.invoke(frame, new Object[] { allFrameIcons }); } catch (Throwable e) { // ignore this and use single small icon if (allFrameIcons.size() > 0) { frame.setIconImage(allFrameIcons.get(0)); } } } else { if (allFrameIcons.size() > 0) { frame.setIconImage(allFrameIcons.get(0)); } } } /** Returns the list of all possible frame icons. */ public static void setDialogIcon(final JDialog dialog) { Method iconImageMethod = null; try { iconImageMethod = dialog.getClass().getMethod("setIconImages", new Class[] { List.class }); } catch (Throwable e) { // ignore this and use no icons or parent icon } if (iconImageMethod != null) { try { iconImageMethod.invoke(dialog, new Object[] { allFrameIcons }); } catch (Throwable e) { // ignore this and use no or parent icon } } } /** Creates a red gradient paint. */ public static GradientPaint makeRedPaint(final double width, final double height) { return new GradientPaint(0f, 0f, new Color(200, 50, 50), (float) width / 2, (float) height / 2, new Color(255, 100, 100), true); } /** Creates a blue gradient paint. */ public static GradientPaint makeBluePaint(final double width, final double height) { return new GradientPaint(0f, 0f, LIGHT_BLUE, (float) width / 2, (float) height / 2, LIGHTEST_BLUE, true); } /** Creates a yellow gradient paint. */ public static GradientPaint makeYellowPaint(final double width, final double height) { return new GradientPaint(0f, 0f, LIGHT_YELLOW, (float) width / 2, (float) height / 2, LIGHTEST_YELLOW, true); } private static final Map<String, ImageIcon> ICON_CACHE = new HashMap<>(1000); private static final Object ICON_LOCK = new Object(); private static final String BRACKETS = " [...] "; /** * Tries to load the icon for the given resource. Returns {@code null} (and writes a warning) if * the resource file cannot be loaded. This method automatically adds all icon paths specified * since startup time. The default /icons is always searched. Additional paths might be * specified by {@link SwingTools#addIconStoragePath(String)}. * * The given names must contain '/' instead of backslashes! */ public static ImageIcon createIcon(final String iconName) { return createIcon(iconName, false); } /** * Tries to load the icon for the given resource. Returns {@code null} (and writes a warning) if * the resource file cannot be loaded. This method automatically adds all icon paths specified * since startup time. The default /icons is always searched. Additional paths might be * specified by {@link SwingTools#addIconStoragePath(String)}. * * The given names must contain '/' instead of backslashes! * * @param iconName * the name of the icon including the size path component * @param preferMonochrome * if {@code true} will chose a monochrome version over a colored version if it * exists (looking in the {@code [size]/mono} folder). Will fall back to colored if * no monochrome version exists. */ public static ImageIcon createIcon(final String iconName, final boolean preferMonochrome) { ImageIcon icon = null; for (String path : iconPaths) { if (preferMonochrome) { String newIconName = null; if (iconName.contains("16/")) { newIconName = iconName.replaceFirst("16/", "16/mono/"); } else if (iconName.contains("24/")) { newIconName = iconName.replaceFirst("24/", "24/mono/"); } else if (iconName.contains("32/")) { newIconName = iconName.replaceFirst("32/", "32/mono/"); } else if (iconName.contains("48/")) { newIconName = iconName.replaceFirst("48/", "48/mono/"); } icon = createImage(path + newIconName); } else { icon = createImage(path + iconName); } return icon; } return icon; } /** * This method returns the path of the icon given. * * @param iconName * @return */ public static String getIconPath(final String iconName) { for (String path : iconPaths) { ImageIcon icon = createImage(path + iconName); if (icon != null) { URL resource = Tools.getResource(path + iconName); if (resource != null) { return resource.toString(); } } } return UNKNOWN_ICON_URL.toString(); } /** * This method adds a path to the set of paths which are searched for icons if the * {@link SwingTools#createIcon(String)} is called. */ public static void addIconStoragePath(String path) { if (path.startsWith("/")) { path = path.substring(1); } if (!path.endsWith("/")) { path = path + "/"; } iconPaths.add(path); } /** * Tries to load the image for the given resource. Returns {@code null} (and writes a warning) * if the resource file cannot be loaded. */ public static ImageIcon createImage(final String imageName) { return createImage(imageName, true); } /** * Same as {@link #createImage(String)} but can return {@code null} if no placeholder should be * used. Uses a retina image, if running on a retina display and an icon is available. * * @param imageName * @param usePlaceholder * if {@code true} will try to replace missing icons via placeholder. Otherwise, * returns {@code null} * @return */ private static ImageIcon createImage(final String imageName, final boolean usePlaceholder) { synchronized (ICON_LOCK) { if (ICON_CACHE.containsKey(imageName)) { return ICON_CACHE.get(imageName); } // Whenever the image name points to a folder, return null // Before 7.2.3 this would return an uninitialized icon but that causes Swing NPEs int indexOfLastSlash = imageName.lastIndexOf("/"); if (indexOfLastSlash != -1 && indexOfLastSlash == imageName.length() - 1) { // we cannot return an uninitialized icon here. This would cause an NPE inside Swing // internals (disabled icon painting code) // therefore, always return null regardless of whether the user wants a placeholder return null; } // Try to load high-resolution icon (if appropriate) if (scaling == Scaling.RETINA) { // an icon path of the format ".../size(/mono)/icon.png" is expected if (indexOfLastSlash != -1) { String prefix = imageName.substring(0, indexOfLastSlash); if (prefix.endsWith(MONO_COLOR_ICON_PREFIX)) { prefix = prefix.substring(0, prefix.length() - MONO_COLOR_ICON_PREFIX.length()); } int indexOfSlashBeforeResolution = prefix.lastIndexOf("/"); if (indexOfSlashBeforeResolution >= 0) { String potentialSizeString = prefix.substring(indexOfSlashBeforeResolution + 1); try { int iconSize = Integer.parseInt(potentialSizeString); // load icon from high-dpi subfolder StringBuilder scaledIconName = new StringBuilder(32); scaledIconName.append(imageName.substring(0, indexOfLastSlash)); scaledIconName.append(RETINA_ICON_PREFIX); scaledIconName.append(imageName.substring(indexOfLastSlash, imageName.length())); URL scaledIconUrl = Tools.getResource(scaledIconName.toString()); if (scaledIconUrl != null) { ImageIcon icon = new ScaledImageIcon(scaledIconUrl, iconSize, iconSize); ICON_CACHE.put(imageName, icon); return icon; } } catch (NumberFormatException | NullPointerException e) { // Do nothing and fall back to normal icon. The NPE might occur if // the security manager intercepts the lookup of the icon (e.g., if // the icon is missing on OS X). } } } } // try to load standard-resolution icon URL iconUrl = Tools.getResource(imageName); if (iconUrl != null) { try { ImageIcon icon = new ImageIcon(iconUrl); ICON_CACHE.put(imageName, icon); return icon; } catch (NullPointerException e) { // Do nothing and fall back to placeholder icon. The NPE might occur if // the security manager intercepts the lookup of the icon (e.g., if // the icon is missing on OS X). } } // fall back to placeholder or null if (usePlaceholder) { LogService.getRoot().log(Level.FINE, "com.rapidminer.gui.tools.SwingTools.loading_image_error", imageName); if (indexOfLastSlash != -1) { String errorIconName = imageName.substring(0, indexOfLastSlash + 1) + UNKNOWN_ICON_NAME; iconUrl = Tools.getResource(errorIconName); if (iconUrl != null) { ImageIcon icon = new ImageIcon(iconUrl); return icon; } } // return default 16x16 unknown icon return UNKNOWN_ICON; } else { return null; } } } /** * This method transforms the given tool tip text into HTML. Lines are splitted at linebreaks * and additional line breaks are added after ca. {@link #TOOL_TIP_LINE_LENGTH} characters. */ public static String transformToolTipText(final String description) { return transformToolTipText(description, true, TOOL_TIP_LINE_LENGTH); } /** * This method transforms the given tool tip text into HTML. Lines are splitted at linebreaks * and additional line breaks are added after ca. {@link #TOOL_TIP_LINE_LENGTH} characters. * * @param escapeSlashes * Inidicates if forward slashes ("/") are escaped by the html code "/" */ public static String transformToolTipText(final String description, final boolean escapeSlashes) { return transformToolTipText(description, true, TOOL_TIP_LINE_LENGTH, escapeSlashes); } /** * This method transforms the given tool tip text into HTML. Lines are splitted at linebreaks * and additional line breaks are added after ca. {@link #TOOL_TIP_LINE_LENGTH} characters. * * @param escapeSlashes * Inidicates if forward slashes ("/") are escaped by the html code "/" * @param escapeHTML * Indicates if previously added html tags are escaped */ public static String transformToolTipText(final String description, final boolean escapeSlashes, final boolean escapeHTML) { return transformToolTipText(description, true, TOOL_TIP_LINE_LENGTH, escapeSlashes, escapeHTML); } public static String transformToolTipText(final String description, final boolean addHTMLTags, final int lineLength) { return transformToolTipText(description, addHTMLTags, lineLength, false); } /** * This method transforms the given tool tip text into HTML. Lines are splitted at linebreaks * and additional line breaks are added after ca. lineLength characters. * * @param escapeSlashes * Inidicates if forward slashes ("/") are escaped by the html code "/" */ public static String transformToolTipText(final String description, final boolean addHTMLTags, final int lineLength, final boolean escapeSlashes) { return transformToolTipText(description, addHTMLTags, lineLength, escapeSlashes, true); } /** * This method transforms the given tool tip text into HTML. Lines are splitted at linebreaks * and additional line breaks are added after ca. lineLength characters. * * @param escapeSlashes * Inidicates if forward slashes ("/") are escaped by the html code "/" * @param escapeHTML * Indicates if previously added html tags are escaped TODO: Use * <div style="width:XXXpx"> */ public static String transformToolTipText(final String description, final boolean addHTMLTags, final int lineLength, final boolean escapeSlashes, final boolean escapeHTML) { String completeText = description.trim(); if (escapeHTML) { completeText = Tools.escapeHTML(completeText); } if (escapeSlashes) { completeText = completeText.replaceAll("/", "/"); } StringBuffer result = new StringBuffer(); if (addHTMLTags) { result.append("<html>"); } // line.separator does not work here (transform and use \n) completeText = Tools.transformAllLineSeparators(completeText); String[] lines = completeText.split("\n"); for (String text : lines) { boolean first = true; while (text.length() > lineLength) { int spaceIndex = text.indexOf(" ", lineLength); if (!first) { result.append("<br>"); } first = false; if (spaceIndex >= 0) { result.append(text.substring(0, spaceIndex)); text = text.substring(spaceIndex + 1); } else { result.append(text); text = ""; } } if (!first && text.length() > 0) { result.append("<br>"); } result.append(text); result.append("<br>"); } if (addHTMLTags) { result.append("</html>"); } return result.toString(); } /** Adds line breaks after {@link #TOOL_TIP_LINE_LENGTH} letters. */ public static String addLinebreaks(String message) { if (message == null) { return null; } String completeText = message.trim(); StringBuffer result = new StringBuffer(); // line.separator does not work here (transform and use \n) completeText = Tools.transformAllLineSeparators(completeText); String[] lines = completeText.split("\n"); for (String text : lines) { boolean first = true; while (text.length() > TOOL_TIP_LINE_LENGTH) { int spaceIndex = text.indexOf(" ", TOOL_TIP_LINE_LENGTH); if (!first) { result.append(Tools.getLineSeparator()); } first = false; if (spaceIndex >= 0) { result.append(text.substring(0, spaceIndex)); text = text.substring(spaceIndex + 1); } else { result.append(text); text = ""; } } if (!first && text.length() > 0) { result.append(Tools.getLineSeparator()); } result.append(text); result.append(Tools.getLineSeparator()); } return result.toString(); } /** * The key will be used for the properties gui.dialog.-key-.title and * gui.dialog.results.-key-.icon */ public static void showResultsDialog(final String i18nKey, final JComponent results, final Object... i18nArgs) { showResultsDialog(ApplicationFrame.getApplicationFrame(), i18nKey, results, i18nArgs); } /** * The key will be used for the properties gui.dialog.-key-.title and * gui.dialog.results.-key-.icon * * @since 7.5.0 */ public static void showResultsDialog(final Window owner, final String i18nKey, final JComponent results, final Object... i18nArgs) { invokeLater(new Runnable() { @Override public void run() { ResultViewDialog dialog = new ResultViewDialog(owner, i18nKey, results, i18nArgs); dialog.setVisible(true); } }); } /** * The key will be used for the properties gui.dialog.-key-.title and * gui.dialog.message.-key-.icon */ public static void showMessageDialog(final String key, final Object... keyArguments) { showMessageDialog(key, null, keyArguments); } /** * The key will be used for the properties gui.dialog.-key-.title and * gui.dialog.message.-key-.icon * * @since 7.5.0 */ public static void showMessageDialog(final Window owner, final String key, final Object... keyArguments) { showMessageDialog(owner, key, null, keyArguments); } /** * The key will be used for the properties gui.dialog.-key-.title and * gui.dialog.message.-key-.icon */ public static void showMessageDialog(final String key, final JComponent component, final Object... keyArguments) { showMessageDialog(ApplicationFrame.getApplicationFrame(), key, component, keyArguments); } /** * The key will be used for the properties gui.dialog.-key-.title and * gui.dialog.message.-key-.icon * * @since 7.5.0 */ public static void showMessageDialog(final Window owner, final String key, final JComponent component, final Object... keyArguments) { invokeLater(new Runnable() { @Override public void run() { MessageDialog dialog = new MessageDialog(owner, key, component, keyArguments); dialog.setVisible(true); } }); } /** * The key will be used for the properties gui.dialog.-key-.title and * gui.dialog.confirm.-key-.icon * * See {@link ConfirmDialog} for details on the mode options. */ public static int showConfirmDialog(final String key, final int mode, final Object... keyArguments) { return showConfirmDialog(ApplicationFrame.getApplicationFrame(), key, mode, keyArguments); } /** * The key will be used for the properties gui.dialog.-key-.title and * gui.dialog.confirm.-key-.icon * * See {@link ConfirmDialog} for details on the mode options. * * @since 7.5.0 */ public static int showConfirmDialog(final Window owner, final String key, final int mode, final Object... keyArguments) { return invokeAndWaitWithResult(new ResultRunnable<Integer>() { @Override public Integer run() { ConfirmDialog dialog = new ConfirmDialog(owner, key, mode, false, keyArguments); dialog.setVisible(true); return dialog.getReturnOption(); } }); } /** * This method will present a dialog to enter a text. This text will be returned if the user * confirmed the edit. Otherwise {@code null} is returned. The key will be used for the * properties gui.dialog.input.-key-.title, gui.dialog.input.-key-.message and * gui.dialog.input.-key-.icon */ public static String showInputDialog(final String key, final String text, final Object... keyArguments) { return showInputDialog(ApplicationFrame.getApplicationFrame(), key, text, keyArguments); } /** * This method will present a dialog to enter a text. This text will be returned if the user * confirmed the edit. Otherwise {@code null} is returned. The key will be used for the * properties gui.dialog.input.-key-.title, gui.dialog.input.-key-.message and * gui.dialog.input.-key-.icon * * @since 7.5.0 */ public static String showInputDialog(final Window owner, final String key, final String text, final Object... keyArguments) { return showInputDialog(owner, key, text, null, keyArguments); } /** * This method will present a dialog to enter a text. This text will be returned if the user * confirmed the edit and the validator does not report an error. Otherwise {@code null} is * returned. The key will be used for the properties gui.dialog.input.-key-.title, * gui.dialog.input.-key-.message and gui.dialog.input.-key-.icon * * @since 7.0.0 */ public static String showInputDialog(final Window owner, final String key, final String text, final InputValidator<String> inputValidator, final Object... keyArguments) { return invokeAndWaitWithResult(new ResultRunnable<String>() { @Override public String run() { InputDialog dialog = new InputDialog(owner, key, text, inputValidator, keyArguments); dialog.setVisible(true); if (dialog.wasConfirmed()) { return dialog.getInputText(); } else { return null; } } }); } /** * This method will present a repository entry dialog to enter a text. This text will be * returned if the user confirmed the edit. Otherwise {@code null} is returned. Prevents invalid * repository names. The key will be used for the properties gui.dialog.input.-key-.title, * gui.dialog.input.-key-.message and gui.dialog.input.-key-.icon */ public static String showRepositoryEntryInputDialog(final String key, final String text, final Object... keyArguments) { return showRepositoryEntryInputDialog(ApplicationFrame.getApplicationFrame(), key, text, keyArguments); } /** * This method will present a repository entry dialog to enter a text. This text will be * returned if the user confirmed the edit. Otherwise {@code null} is returned. Prevents invalid * repository names. The key will be used for the properties gui.dialog.input.-key-.title, * gui.dialog.input.-key-.message and gui.dialog.input.-key-.icon * * @since 7.5.0 */ public static String showRepositoryEntryInputDialog(final Window owner, final String key, final String text, final Object... keyArguments) { return invokeAndWaitWithResult(new ResultRunnable<String>() { @Override public String run() { RepositoryEntryInputDialog dialog = new RepositoryEntryInputDialog(owner, key, text, keyArguments); dialog.setVisible(true); if (dialog.wasConfirmed()) { return dialog.getInputText(); } else { return null; } } }); } /** * The key will be used for the properties gui.dialog.-key-.title and * gui.dialog.input.-key-.icon */ public static <T> T showInputDialog(final String key, final T[] selectionValues, final T initialSelectionValue, final Object... keyArguments) { return showInputDialog(ApplicationFrame.getApplicationFrame(), key, selectionValues, initialSelectionValue, keyArguments); } /** * The key will be used for the properties gui.dialog.-key-.title and * gui.dialog.input.-key-.icon * * @since 7.5.0 */ public static <T> T showInputDialog(final Window owner, final String key, final T[] selectionValues, final T initialSelectionValue, final Object... keyArguments) { return showInputDialog(owner, key, false, Arrays.asList(selectionValues), initialSelectionValue, null, keyArguments); } /** * This will open a simple input dialog, where a comboBox presents the given values. The * Combobox might be editable depending on parameter setting. * * The key will be used for the properties gui.dialog.-key-.title and * gui.dialog.input.-key-.icon */ public static <T> T showInputDialog(final String key, final boolean editable, final T[] selectionValues, final T initialSelectionValue, final Object... keyArguments) { return showInputDialog(ApplicationFrame.getApplicationFrame(), key, editable, selectionValues, initialSelectionValue, null, keyArguments); } /** * This will open a simple input dialog, where a comboBox presents the given values. The * Combobox might be editable depending on parameter setting. * * The key will be used for the properties gui.dialog.-key-.title and * gui.dialog.input.-key-.icon * * @since 7.5.0 */ public static <T> T showInputDialog(final Window owner, final String key, final boolean editable, final T[] selectionValues, final T initialSelectionValue, final Object... keyArguments) { return showInputDialog(owner, key, editable, Arrays.asList(selectionValues), initialSelectionValue, null, keyArguments); } /** * This will open a simple input dialog, where a comboBox presents the given values. The * Combobox might be editable depending on parameter setting. The selection will be returned if * the user confirmed the dialog and the validator does not report an error. Otherwise * {@code null} is returned. * * The key will be used for the properties gui.dialog.-key-.title and * gui.dialog.input.-key-.icon * * @since 7.0.0 */ public static <T> T showInputDialog(final Window owner, final String key, final boolean editable, final Collection<T> selectionValues, final T initialSelectionValue, final InputValidator<T> inputValidator, final Object... keyArguments) { return invokeAndWaitWithResult(new ResultRunnable<T>() { @Override public T run() { SelectionInputDialog<T> dialog = new SelectionInputDialog<>(owner, key, editable, selectionValues, initialSelectionValue, inputValidator, keyArguments); dialog.setVisible(true); if (dialog.wasConfirmed()) { return dialog.getInputSelection(); } else { return null; } } }); } /** * Shows a very simple error message without any Java exception hints. * * @param key * the I18n-key which will be used to display the internationalized message * @param arguments * additional arguments for the internationalized message, which replace * <code>{0}</code>, <code>{1}</code>, etcpp. */ public static void showVerySimpleErrorMessage(final String key, final Object... arguments) { showVerySimpleErrorMessage(ApplicationFrame.getApplicationFrame(), key, arguments); } /** * Shows a very simple error message without any Java exception hints. * * @param owner * the owner of the opened dialog * @param key * the I18n-key which will be used to display the internationalized message * @param arguments * additional arguments for the internationalized message, which replace * <code>{0}</code>, <code>{1}</code>, etcpp. * @since 7.5.0 */ public static void showVerySimpleErrorMessage(final Window owner, final String key, final Object... arguments) { invokeLater(new Runnable() { @Override public void run() { ErrorDialog dialog = new ErrorDialog(owner, key, arguments); dialog.setModal(true); dialog.setVisible(true); } }); } public static void showVerySimpleErrorMessageAndWait(final String key, final Object... arguments) { showVerySimpleErrorMessageAndWait(ApplicationFrame.getApplicationFrame(), key, arguments); } /** * @since 7.5.0 */ public static void showVerySimpleErrorMessageAndWait(final Window owner, final String key, final Object... arguments) { invokeAndWait(new Runnable() { @Override public void run() { ErrorDialog dialog = new ErrorDialog(owner, key, arguments); dialog.setModal(true); dialog.setVisible(true); } }); } /** * This is the normal method which could be used by GUI classes for errors caused by some * exception (e.g. IO issues). Of course these error message methods should never be invoked by * operators or similar. * * @param key * the I18n-key which will be used to display the internationalized message * @param e * the exception associated to this message * @param arguments * additional arguments for the internationalized message, which replace * <code>{0}</code>, <code>{1}</code>, etcpp. */ public static void showSimpleErrorMessage(final String key, final Throwable e, final Object... arguments) { showSimpleErrorMessage(key, e, true, arguments); } /** * This is the normal method which could be used by GUI classes for errors caused by some * exception (e.g. IO issues). Of course these error message methods should never be invoked by * operators or similar. * * @param owner * the owner of the opened dialog * @param key * the I18n-key which will be used to display the internationalized message * @param e * the exception associated to this message * @param arguments * additional arguments for the internationalized message, which replace * <code>{0}</code>, <code>{1}</code>, etcpp. * @since 7.5.0 */ public static void showSimpleErrorMessage(final Window owner, final String key, final Throwable e, final Object... arguments) { showSimpleErrorMessage(owner, key, e, true, arguments); } /** * This is the normal method which could be used by GUI classes for errors caused by some * exception (e.g. IO issues). Of course these error message methods should never be invoked by * operators or similar. * * @param key * the I18n-key which will be used to display the internationalized message * @param e * the exception associated to this message * @param displayExceptionMessage * indicates if the exception message will be displayed in the dialog or just in the * detailed panel * @param arguments * additional arguments for the internationalized message, which replace * <code>{0}</code>, <code>{1}</code>, etcpp. */ public static void showSimpleErrorMessage(final String key, final Throwable e, final boolean displayExceptionMessage, final Object... arguments) { showSimpleErrorMessage(ApplicationFrame.getApplicationFrame(), key, e, displayExceptionMessage, arguments); } /** * This is the normal method which could be used by GUI classes for errors caused by some * exception (e.g. IO issues). Of course these error message methods should never be invoked by * operators or similar. * * @param owner * the owner of the opened dialog * @param key * the I18n-key which will be used to display the internationalized message * @param e * the exception associated to this message * @param displayExceptionMessage * indicates if the exception message will be displayed in the dialog or just in the * detailed panel * @param arguments * additional arguments for the internationalized message, which replace * <code>{0}</code>, <code>{1}</code>, etcpp. * @since 7.5.0 */ public static void showSimpleErrorMessage(final Window owner, final String key, final Throwable e, final boolean displayExceptionMessage, final Object... arguments) { ActionStatisticsCollector.getInstance().log(ActionStatisticsCollector.TYPE_ERROR, key, e != null ? e.getClass().getName() : null); // if debug mode is enabled, send exception to logger if ("true".equals(ParameterService.getParameterValue(RapidMiner.PROPERTY_RAPIDMINER_GENERAL_DEBUGMODE))) { LogService.getRoot().log(Level.WARNING, I18N.getMessage(LogService.getRoot().getResourceBundle(), "com.rapidminer.gui.tools.SwingTools.show_simple_get_message", e.getMessage()), e); } invokeLater(new Runnable() { @Override public void run() { ExtendedErrorDialog dialog = new ExtendedErrorDialog(owner, key, e, displayExceptionMessage, arguments); dialog.setVisible(true); } }); } /** * This is the normal method which could be used by GUI classes for errors caused by some * exception (e.g. IO issues). Of course these error message methods should never be invoked by * operators or similar. The key is constructed as gui.dialog.error.-key- and uses .title and * .icon properties * * @param key * the I18n-key which will be used to display the internationalized message * @param errorMessage * the error message associated to this message * @param displayExceptionMessage * indicates if the exception message will be displayed in the dialog or just in the * detailed panel * @param arguments * additional arguments for the internationalized message, which replace * <code>{0}</code>, <code>{1}</code>, etcpp. */ public static void showSimpleErrorMessage(final String key, final String errorMessage, final Object... arguments) { showSimpleErrorMessage(ApplicationFrame.getApplicationFrame(), key, errorMessage, arguments); } /** * This is the normal method which could be used by GUI classes for errors caused by some * exception (e.g. IO issues). Of course these error message methods should never be invoked by * operators or similar. The key is constructed as gui.dialog.error.-key- and uses .title and * .icon properties * * @param owner * the owner of the opened dialog * @param key * the I18n-key which will be used to display the internationalized message * @param errorMessage * the error message associated to this message * @param displayExceptionMessage * indicates if the exception message will be displayed in the dialog or just in the * detailed panel * @param arguments * additional arguments for the internationalized message, which replace * <code>{0}</code>, <code>{1}</code>, etcpp. */ public static void showSimpleErrorMessage(final Window owner, final String key, final String errorMessage, final Object... arguments) { invokeLater(new Runnable() { @Override public void run() { ExtendedErrorDialog dialog = new ExtendedErrorDialog(owner, key, errorMessage, arguments); dialog.setVisible(true); } }); // if debug mode is enabled, print throwable into logger if (ParameterService.getParameterValue(RapidMiner.PROPERTY_RAPIDMINER_GENERAL_DEBUGMODE).equals("true")) { LogService.getRoot().log(Level.WARNING, errorMessage); } } /** * Shows the final error message dialog. This dialog also allows to send a bug report if the * error was not (definitely) a user error. * * @param key * the I18n-key which will be used to display the internationalized message * @param e * the exception associated to this message * @param displayExceptionMessage * indicates if the exception message will be displayed in the dialog or just in the * detailed panel * @param arguments * additional arguments for the internationalized message, which replace * <code>{0}</code>, <code>{1}</code>, etcpp. */ public static void showFinalErrorMessage(final String key, final Throwable e, final boolean displayExceptionMessage, final Object... objects) { showFinalErrorMessage(ApplicationFrame.getApplicationFrame(), key, e, displayExceptionMessage, objects); } /** * Shows the final error message dialog. This dialog also allows to send a bug report if the * error was not (definitely) a user error. * * @param owner * the owner of the opened dialog * @param key * the I18n-key which will be used to display the internationalized message * @param e * the exception associated to this message * @param displayExceptionMessage * indicates if the exception message will be displayed in the dialog or just in the * detailed panel * @param arguments * additional arguments for the internationalized message, which replace * <code>{0}</code>, <code>{1}</code>, etcpp. * @since 7.5.0 */ public static void showFinalErrorMessage(final Window owner, final String key, final Throwable e, final boolean displayExceptionMessage, final Object... objects) { // if debug modus is enabled, print throwable into logger if (ParameterService.getParameterValue(RapidMiner.PROPERTY_RAPIDMINER_GENERAL_DEBUGMODE).equals("true")) { LogService.getRoot().log(Level.SEVERE, e.getMessage(), e); } invokeLater(new Runnable() { @Override public void run() { ExtendedErrorDialog dialog = new ExtendedErrorDialog(owner, key, e, displayExceptionMessage, objects); dialog.setVisible(true); } }); } /** * Shows the final error message dialog. This dialog also allows to send a bug report if the * error was not (definitely) a user error. * * @param key * the I18n-key which will be used to display the internationalized message * @param e * the exception associated to this message * @param arguments * additional arguments for the internationalized message, which replace * <code>{0}</code>, <code>{1}</code>, etcpp. */ public static void showFinalErrorMessage(final String key, final Throwable e, final Object... objects) { showFinalErrorMessage(key, e, false, objects); } /** * Opens a file chooser with a reasonable start directory. If the extension is null, no file * filters will be used. */ public static File chooseFile(final Component parent, final File file, final boolean open, final String extension, final String extensionDescription) { return chooseFile(parent, null, file, open, extension, extensionDescription); } public static File chooseFile(final Component parent, final String i18nKey, final File file, final boolean open, final String extension, final String extensionDescription) { return chooseFile(parent, i18nKey, file, open, false, extension, extensionDescription); } /** * Opens a file chooser with a reasonable start directory. If the extension is null, no file * filters will be used. This method allows choosing directories. */ public static File chooseFile(final Component parent, final File file, final boolean open, final boolean onlyDirs, final String extension, final String extensionDescription) { return chooseFile(parent, null, file, open, onlyDirs, extension, extensionDescription); } public static File chooseFile(final Component parent, final String i18nKey, final File file, final boolean open, final boolean onlyDirs, final String extension, final String extensionDescription) { return chooseFile(parent, i18nKey, file, open, onlyDirs, extension == null ? null : new String[] { extension }, extensionDescription == null ? null : new String[] { extensionDescription }); } public static File chooseFile(final Component parent, final String i18nKey, final File file, final boolean open, final boolean onlyDirs, final String extension, final String extensionDescription, final boolean acceptAllFiles) { return chooseFile(parent, i18nKey, file, open, onlyDirs, extension == null ? null : new String[] { extension }, extensionDescription == null ? null : new String[] { extensionDescription }, acceptAllFiles); } /** Returns the user selected file. */ public static File chooseFile(final Component parent, final File file, final boolean open, final boolean onlyDirs, final String[] extensions, final String[] extensionDescriptions) { return chooseFile(parent, null, file, open, onlyDirs, extensions, extensionDescriptions); } /** * @param addDefaultAllFileExtensionFilter * if set to <code>true</code> and more than one file extension is provided a new * filter that contains all file extension will be added as default filter. This * makes sense for file reading operations that can read files with different file * extensions. For file writing operations it is however not recommended as the new * file filter will not add the correct file ending when entering the path of a file * that does not exist. * @return the user selected file. */ public static File chooseFile(final Component parent, final File file, final boolean open, final boolean onlyDirs, final String[] extensions, final String[] extensionDescriptions, final boolean addDefaultAllFileExtensionFilter) { return chooseFile(parent, null, file, open, onlyDirs, addDefaultAllFileExtensionFilter, extensions, extensionDescriptions); } public static File chooseFile(final Component parent, final String i18nKey, final File file, final boolean open, final boolean onlyDirs, final String[] extensions, final String[] extensionDescriptions) { return chooseFile(parent, i18nKey, file, open, onlyDirs, false, extensions, extensionDescriptions); } public static File chooseFile(final Component parent, final String i18nKey, final File file, final boolean open, final boolean onlyDirs, boolean addDefaultAllFileExtensionFilter, final String[] extensions, final String[] extensionDescriptions) { List<FileFilter> fileFilters = new LinkedList<>(); if (extensions != null) { int i = 0; if (addDefaultAllFileExtensionFilter && extensions.length > 1) { /* * In case multiple formats are supported add a filter which shows all supported * formats first */ fileFilters.add(new SimpleFileFilter(getAllExtensionFilterDescription(extensions), extensions, -1)); } for (String extension : extensions) { if (extension != null) { fileFilters .add(new SimpleFileFilter(extensionDescriptions[i] + " (*." + extension + ")", "." + extension)); } ++i; } } return chooseFile(parent, i18nKey, file, open, onlyDirs, fileFilters.toArray(new FileFilter[fileFilters.size()]), true); } private static String getAllExtensionFilterDescription(String[] extensions) { StringBuilder builder = new StringBuilder(); boolean first = true; for (String extension : extensions) { if (!first) { builder.append(", "); } builder.append("*."); builder.append(extension); first = false; } return builder.toString(); } public static File chooseFile(final Component parent, final String i18nKey, final File file, final boolean open, final boolean onlyDirs, final String[] extensions, final String[] extensionDescriptions, final boolean acceptAllFiles) { FileFilter[] filters = null; if (extensions != null) { filters = new FileFilter[extensions.length]; for (int i = 0; i < extensions.length; i++) { filters[i] = new SimpleFileFilter(extensionDescriptions[i] + " (*." + extensions[i] + ")", "." + extensions[i]); } } return chooseFile(parent, i18nKey, file, open, onlyDirs, filters, acceptAllFiles); } /** * Opens a file chooser with a reasonable start directory. onlyDirs indidcates if only files or * only can be selected. * * @param file * The initially selected value of the file chooser dialog * @param open * Open or save dialog? * @param onlyDirs * Only allow directories to be selected * @param fileFilters * List of FileFilters to use */ private static File chooseFile(Component parent, String i18nKey, File file, boolean open, boolean onlyDirs, FileFilter[] fileFilters, boolean acceptAllFiles) { if (parent == null) { parent = RapidMinerGUI.getMainFrame(); } String key = "file_chooser." + (i18nKey != null ? i18nKey : open ? onlyDirs ? "open_directory" : "open" : "save"); JFileChooser fileChooser = createFileChooser(key, file, onlyDirs, fileFilters); fileChooser.setAcceptAllFileFilterUsed(acceptAllFiles); int returnValue = open ? fileChooser.showOpenDialog(parent) : fileChooser.showSaveDialog(parent); switch (returnValue) { case JFileChooser.APPROVE_OPTION: // check extension File selectedFile = fileChooser.getSelectedFile(); FileFilter selectedFilter = fileChooser.getFileFilter(); String extension = null; if (selectedFilter instanceof SimpleFileFilter) { SimpleFileFilter simpleFF = (SimpleFileFilter) selectedFilter; extension = simpleFF.getExtension(); } if (extension != null) { if (!selectedFile.getAbsolutePath().toLowerCase().endsWith(extension.toLowerCase())) { selectedFile = new File(selectedFile.getAbsolutePath() + extension); } } storeLastDirectory(selectedFile.toPath()); return selectedFile; default: return null; } } /** * Stores the directory of the selectedFile under the bookmark "--- Last Directory" * * @param selectedFile * the file defining the last selected directory * @since 7.0 */ public static void storeLastDirectory(Path selectedFile) { if (selectedFile != null) { Path parentFile = selectedFile.getParent(); if (parentFile != null) { File bookmarksFile = new File(FileSystemService.getUserRapidMinerDir(), ".bookmarks"); if (bookmarksFile.exists()) { List<Bookmark> bookmarks = BookmarkIO.readBookmarks(bookmarksFile); Iterator<Bookmark> b = bookmarks.iterator(); while (b.hasNext()) { Bookmark bookmark = b.next(); if (bookmark.getName().equals("--- Last Directory")) { b.remove(); } } bookmarks.add(new Bookmark("--- Last Directory", parentFile.toAbsolutePath().toString())); Collections.sort(bookmarks); BookmarkIO.writeBookmarks(bookmarks, bookmarksFile); } } } } /** * Creates file chooser with a reasonable start directory. You may use the following code * snippet in order to retrieve the file: * * <pre> * if (fileChooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) * File selectedFile = fileChooser.getSelectedFile(); * </pre> * * Usually, the method {@link #chooseFile(Component, File, boolean, boolean, FileFilter[])} or * one of the convenience wrapper methods can be used to do this. This method is only useful if * one is interested, e.g., in the selected file filter. * * @param file * The initially selected value of the file chooser dialog * @param onlyDirs * Only allow directories to be selected * @param fileFilters * List of FileFilters to use */ public static JFileChooser createFileChooser(final String i18nKey, final File file, final boolean onlyDirs, final FileFilter[] fileFilters) { File directory = null; if (file != null) { if (file.isDirectory()) { directory = file; } else { directory = file.getAbsoluteFile().getParentFile(); } } else { directory = FileSystemView.getFileSystemView().getDefaultDirectory(); } JFileChooser fileChooser = new ExtendedJFileChooser(i18nKey, directory); if (onlyDirs) { fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); } if (fileFilters != null) { fileChooser.setAcceptAllFileFilterUsed(true); for (FileFilter fileFilter : fileFilters) { fileChooser.addChoosableFileFilter(fileFilter); } if (fileFilters.length > 0) { fileChooser.setFileFilter(fileFilters[0]); } } if (file != null) { fileChooser.setSelectedFile(file); } return fileChooser; } /** * Creates a panel with title and text. The panel has a border layout and the text is placed * into the NORTH section. */ public static JPanel createTextPanel(final String title, final String text) { JPanel panel = new JPanel(new java.awt.BorderLayout()); JLabel label = new JLabel("<html><h3>" + title + "</h3>" + (text != null ? "<p>" + text + "</p>" : "") + "</html>"); label.setBorder(BorderFactory.createEmptyBorder(11, 11, 11, 11)); label.setFont(label.getFont().deriveFont(java.awt.Font.PLAIN)); panel.add(label, java.awt.BorderLayout.NORTH); return panel; } // ================================================================================ // /** // * Replaces simple html tags and quotes by RapidMiner specific text // elements. // * These can be used in XML files without confusing an XML parser. // */ // public static String html2RapidMinerText(String html) { // if (html == null) // return null; // String result = html.replaceAll("<", "#ylt#"); // result = result.replaceAll(">", "#ygt#"); // result = result.replaceAll("\"", "#yquot#"); // result = result.replaceAll(Tools.getLineSeparator(), ""); // return result; // } /** * Returns enlarged string, with a suffix and a prefix of blanks to reach the given minimal text * length * * Example: * * enlargeString("1234", 8) -> " 1234 " * * * @param string * The string to enlarge * @param minimalTextLength * Minimal length of the text */ public static String enlargeString(String string, int minimalTextLength) { if (string == null || string.isEmpty()) { throw new IllegalArgumentException("No string given"); } if (minimalTextLength < 0) { throw new IllegalArgumentException("Number of blanks has to be positive"); } int n = minimalTextLength - string.length(); // text is large enough if (n <= 0) { return string; } int nrSpaces = (int) Math.ceil(n / 2.0); String spaces = String.format("%" + nrSpaces + "s", ""); return spaces + string + spaces; } /** * Replaces the RapidMiner specific tag elements by normal HTML tags. The given text is also * embedded in an HTML and body tag with an appropriated style sheet definition. * * Currently, the only replaced tag is <icon>NAME</icon> which will be replaced by * <img src="path/to/NAME"/>. * */ public static String text2DisplayHtml(final String text) { String result = "<html><head><style type=text/css>body { font-family:sans-serif; font-size:12pt; }</style></head><body>" + text + "</body></html>"; result = text2SimpleHtml(result); // result = result.replaceAll("#yquot#", """); while (result.indexOf("<icon>") != -1) { int startIndex = result.indexOf("<icon>"); int endIndex = result.indexOf("</icon>"); String start = result.substring(0, startIndex); String end = result.substring(endIndex + 7); String icon = result.substring(startIndex + 6, endIndex).trim().toLowerCase(); java.net.URL url = Tools.getResource("icons/" + icon + ".png"); if (url != null) { result = start + "<img src=\"" + url + "\"/>" + end; } else { result = start + end; } } return result; } /** * Replaces the RapidMiner specific tag elements by normal HTML tags. This method does not embed * the given text in a root HTML tag. */ private static String text2SimpleHtml(String htmlText) { if (htmlText == null) { return null; } String replaceString = htmlText; // replaceString = htmlText.replaceAll("#ygt#", ">"); // replaceString = replaceString.replaceAll("#ylt#", "<"); StringBuffer result = new StringBuffer(); boolean afterClose = true; int currentLineLength = 0; for (int i = 0; i < replaceString.length(); i++) { char c = replaceString.charAt(i); // skip white space after close if (afterClose) { if (c == ' ') { continue; } } // opening bracket if (c == '<') { if (!afterClose) { result.append(Tools.getLineSeparator()); currentLineLength = 0; } } // append char afterClose = false; result.append(c); currentLineLength++; // break long lines if (currentLineLength > 70 && c == ' ') { result.append(Tools.getLineSeparator()); currentLineLength = 0; } // closing bracket if (c == '>') { result.append(Tools.getLineSeparator()); currentLineLength = 0; afterClose = true; } } return result.toString(); } /** * Returns a color equivalent to the value of <code>value</code>. The value has to be normalized * between 0 and 1. */ public static Color getPointColor(final double value) { return new Color(Color.HSBtoRGB((float) (0.68 * (1.0d - value)), 1.0f, 1.0f)); // all // colors } /** * Returns a color equivalent to the value of <code>value</code>. The value will be normalized * between 0 and 1 using the parameters max and min. Which are the minimum and maximum of the * complete dataset. */ public static Color getPointColor(double value, final double max, final double min) { value = (value - min) / (max - min); return getPointColor(value); } /** Returns JEditTextArea defaults with adapted syntax color styles. */ public static TextAreaDefaults getTextAreaDefaults() { TextAreaDefaults defaults = TextAreaDefaults.getDefaults(); defaults.styles = getSyntaxStyles(); return defaults; } /** * Returns adapted syntax color and font styles matching RapidMiner colors. */ public static SyntaxStyle[] getSyntaxStyles() { SyntaxStyle[] styles = SyntaxUtilities.getDefaultSyntaxStyles(); styles[Token.COMMENT1] = new SyntaxStyle(new Color(0x990033), true, false); styles[Token.COMMENT2] = new SyntaxStyle(Color.black, true, false); styles[Token.KEYWORD1] = new SyntaxStyle(Color.black, false, true); styles[Token.KEYWORD2] = new SyntaxStyle(new Color(255, 51, 204), false, false); styles[Token.KEYWORD3] = new SyntaxStyle(new Color(255, 51, 204), false, false); styles[Token.LITERAL1] = new SyntaxStyle(new Color(51, 51, 255), false, false); styles[Token.LITERAL2] = new SyntaxStyle(new Color(51, 51, 255), false, false); styles[Token.LABEL] = new SyntaxStyle(new Color(0x990033), false, true); styles[Token.OPERATOR] = new SyntaxStyle(Color.black, false, true); styles[Token.INVALID] = new SyntaxStyle(Color.red, false, true); return styles; } public static String toHTMLString(final Ports<? extends Port> ports) { StringBuilder b = new StringBuilder(); boolean first = true; for (Port port : ports.getAllPorts()) { if (!first) { b.append(", "); } else { first = false; } b.append(port.getName()); String desc = port.getDescription(); if (desc.length() > 0) { b.append(": "); b.append(port.getDescription()); } } return b.toString(); } public static void showLongMessage(final String i18nKey, final String message) { showLongMessage(ApplicationFrame.getApplicationFrame(), i18nKey, message); } /** * @since 7.5.0 */ public static void showLongMessage(final Window owner, final String i18nKey, final String message) { invokeLater(new Runnable() { @Override public void run() { LongMessageDialog dialog = new LongMessageDialog(owner, i18nKey, message); dialog.setVisible(true); } }); } public static void setEnabledRecursive(final Component c, final boolean enabled) { c.setEnabled(enabled); if (c instanceof Container) { for (Component child : ((Container) c).getComponents()) { setEnabledRecursive(child, enabled); } } } public static void setOpaqueRecursive(final Component c, final boolean enabled) { if (c instanceof JComponent) { ((JComponent) c).setOpaque(enabled); } if (c instanceof Container) { for (Component child : ((Container) c).getComponents()) { setOpaqueRecursive(child, enabled); } } } public static void setProcessEditorsEnabled(final boolean enabled) { MainFrame mainFrame = RapidMinerGUI.getMainFrame(); setEnabledRecursive(mainFrame.getProcessPanel().getComponent(), enabled); setEnabledRecursive(mainFrame.getPropertyPanel().getComponent(), enabled); setEnabledRecursive(mainFrame.getOperatorTree(), enabled); setEnabledRecursive(mainFrame.getProcessContextEditor().getComponent(), enabled); setEnabledRecursive(mainFrame.getXMLEditor(), enabled); mainFrame.getActions().enableActions(); } public static Color getOperatorColor(final Operator operator) { OperatorDescription operatorDescription = operator.getOperatorDescription(); String groupKey = operatorDescription.getGroup(); /* * Operators that use the extension tree root need to be handled differently. */ if (operatorDescription.isUsingExtensionTreeRoot() && groupKey.startsWith(OperatorDescription.EXTENSIONS_GROUP_IDENTIFIER)) { // remove extension group identifier groupKey = groupKey.substring(groupKey.indexOf('.') + 1, groupKey.length()); Color operatorColor = GROUP_TO_COLOR_MAP.get(groupKey, operatorDescription.getProvider()); if (operatorColor != StringColorMap.DEFAULT_COLOR) { return operatorColor; } else { // either remove extension name (if more groups are present) or use extension name // in case of top-level operator groupKey = groupKey.substring(groupKey.indexOf('.') + 1, groupKey.length()); } } return GROUP_TO_COLOR_MAP.get(groupKey, operatorDescription.getProvider()); } public static Color getOperatorColor(final String operatorGroup) { return GROUP_TO_COLOR_MAP.get(operatorGroup); } /** * This method adds the colors of the given property file to the global group colors */ public static void registerAdditionalGroupColors(final String groupProperties, final String pluginName, final ClassLoader classLoader, Plugin provider) { try { GROUP_TO_COLOR_MAP.parseProperties(groupProperties, "group.", ".color", classLoader, provider); } catch (IOException e) { LogService.getRoot().warning("Cannot load operator group colors for plugin " + pluginName + "."); } } /** * Returns <code>true</code> if the given {@link MouseEvent} only exited to child components of * the specified parent {@link Component}; <code>false</code> otherwise. This fixes the problem * that Swing triggers MouseExited events when moving the mouse for example over a button inside * the panel on which the mouse listener is. * * @param parent * @param e * @return */ public static boolean isMouseEventExitedToChildComponents(final Component parent, final MouseEvent e) { if (parent == null) { throw new IllegalArgumentException("parent must not be null!"); } return SwingUtilities.getDeepestComponentAt(parent, e.getX(), e.getY()) != null; } /** * Darkens the given {@link Color} by the specified factor. * * @param color * @param factor * @return */ public static Color darkenColor(final Color color, final float factor) { // convert to H(ue) S(aturation) B(rightness), which is designed for // this kind of operation float hsb[] = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null); // turn down brightness and return color return Color.getHSBColor(hsb[0], hsb[1], factor * hsb[2]); } /** * Changes the saturation of the given {@link Color} by multiplying the saturation with the * specified factor. * * @param color * the color to change * @param factor * the factor to multiply the current saturation with * @return color with changed saturation */ public static Color saturateColor(final Color color, final float factor) { // convert to H(ue) S(aturation) B(rightness), which is designed for // this kind of operation float hsb[] = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null); // adjust saturation and return color return Color.getHSBColor(hsb[0], factor * hsb[1], hsb[2]); } /** * Makes the given {@link Color} a bit brighter, though not as much as {@link Color#brighter()}. * * @param color * @return */ public static Color brightenColor(final Color color) { // convert to H(ue) S(aturation) B(rightness), which is designed for // this kind of operation float hsb[] = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null); // turn up brightness and return color return Color.getHSBColor(hsb[0], hsb[1], 0.5f * (1f + hsb[2])); } /** * Darkens the given {@link Color} a bit, though not as much as {@link Color#darker()}. * * @param color * @return */ public static Color darkenColor(final Color color) { return darkenColor(color, 0.9f); } /** * Returns the hex value of this color to use in html with starting # * * @param color * the color that should be converted to a hex string representation * @return the hex value of this color to use in html * @since 7.0 */ public static String getColorHexValue(Color color) { return String.format("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue()); } /** * Paints the given overlay icon over the base icon. This can be used for example to cross out * icons. Provided the overlay icon has a transparent background, the base icon will still be * visible. * * @param baseIcon * @param overlayIcon * @return */ public static ImageIcon createOverlayIcon(final ImageIcon baseIcon, final ImageIcon overlayIcon) { if (baseIcon == null) { throw new IllegalArgumentException("baseIcon must not be null!"); } if (overlayIcon == null) { throw new IllegalArgumentException("overlayIcon must not be null!"); } BufferedImage bufferedImg = new BufferedImage(baseIcon.getIconWidth(), baseIcon.getIconHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = (Graphics2D) bufferedImg.getGraphics(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); AffineTransform identityAT = new AffineTransform(); g2.drawImage(baseIcon.getImage(), identityAT, null); g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f)); g2.drawImage(overlayIcon.getImage(), identityAT, null); g2.dispose(); return new ImageIcon(bufferedImg); } /** * Creates an icon of the specified size and the given shape and colors. * * @param color * the color which is used to fill the shape * @param borderColor * the color for the shape border * @param width * the width of the icon * @param height * the height of the icon * @param shape * the shape of the icon * @return the icon, never {@code null} */ public static ImageIcon createIconFromColor(final Color color, final Color borderColor, final int width, final int height, final Shape shape) { BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = img.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setColor(color); g2.fill(shape); g2.setColor(borderColor); g2.draw(shape); g2.dispose(); return new ImageIcon(img); } /** * If the input name exceeds the maximum length, it is shortened by cutting the input into two * shortened parts and adding [...] in between. * * @param maxLength * the maximum allowed input length * @return the provided input if it does not exceed the maximum input length, otherwise the * shortened input is returned. */ public static String getShortenedDisplayName(String input, int maxLength) { if (input.length() > maxLength + BRACKETS.length()) { StringBuilder builder = new StringBuilder(); builder.append(input.substring(0, maxLength / 2)); builder.append(BRACKETS); builder.append(input.substring(input.length() - maxLength / 2 - 1)); return builder.toString(); } return input; } /** * Converts {@link KeyStroke} to a human-readable string. * * @param keyStroke * the keystroke * @return a human-readable string like 'Ctrl+E' */ public static String formatKeyStroke(KeyStroke keyStroke) { StringBuilder builder = new StringBuilder(); String modifierString = KeyEvent.getKeyModifiersText(keyStroke.getModifiers()); String keyString = KeyEvent.getKeyText(keyStroke.getKeyCode()); if (modifierString != null && !modifierString.trim().isEmpty()) { builder.append(modifierString); builder.append("+"); } builder.append(keyString); return builder.toString(); } /** * Gets stripped text for a JComponent object * * @since 6.4.0 * @param jComponent * The component object used for the calculation of the width * @param text * The text to show * @param maxWidth * Maximum width of component * @param suffixLength * Minimum length of the suffix after the separator */ public static String getStrippedJComponentText(JComponent jComponent, String text, int maxWidth, int suffixLength) { if (jComponent == null || text == null || text.isEmpty()) { return ""; } if (maxWidth < 0) { throw new IllegalArgumentException("Maximum width of JComponent has to be positive"); } int prefixLength = text.length() - suffixLength; while (getStringWidth(jComponent, text) > maxWidth) { prefixLength -= RESIZE_STEP_WIDTH; text = stripString(text, prefixLength, suffixLength); if (prefixLength < suffixLength) { break; } } return text; } /** * Gets precalculated width of string used in jComponent with the default font. * * @since 6.4.0 * @param jComponent * The component, whose font is used * @param string * The string, whose width is calculated */ public static int getStringWidth(JComponent jComponent, String string) { return getStringWidth(jComponent, string, jComponent.getFont()); } /** * Gets precalculated width of string used in jComponent. * * @since 6.4.0 * @param jComponent * The component, whose font is used * @param string * The string, whose width is calculated * @param font * The font of the string */ public static int getStringWidth(JComponent jComponent, String string, Font font) { if (jComponent == null) { return 0; } if (string == null || string.isEmpty()) { return 0; } return jComponent.getFontMetrics(font).stringWidth(string); } /** * Returns stripped string, if input string is too long. * * Examples: * * stripString("1234", 2, 2) -> 1234 * * stripString("1234", 2, 1) -> 12[...]4 * * @since 6.4.0 * @param string * The string to split * @param beginLength * Minimum length of prefix * @param endLength * Minimum length of suffix */ public static String stripString(String string, int beginLength, int endLength) { if (string == null || string.isEmpty()) { return ""; } if (beginLength < 0) { beginLength = 0; } if (endLength < 0) { endLength = 0; } if (string.length() > beginLength + endLength) { return string.substring(0, beginLength) + SEPARATOR + string.substring(string.length() - endLength); } else { return string; } } /** * Returns whether or not the control modifier is down during the event. This takes the OS into * account, so on Mac it will check if the meta modifier is down during the event. * * @param e * the event * @return {@code true} if the control modifier is down on Windows/Linux or the meta modifier is * down on Mac; {@code false} otherwise */ public static boolean isControlOrMetaDown(KeyEvent e) { if (!IS_MAC) { return e.isControlDown(); } else { return e.isMetaDown(); } } /** * Returns whether or not the control modifier is down during the event. This takes the OS into * account, so on Mac it will check if the meta modifier is down during the event. * * @param e * the event * @return {@code true} if the control modifier is down on Windows/Linux or the meta modifier is * down on Mac; {@code false} otherwise */ public static boolean isControlOrMetaDown(MouseEvent e) { if (!IS_MAC) { return e.isControlDown(); } else { return e.isMetaDown(); } } /** * Invokes a runnable on the EDT and waits until the execution has finished. If this method is * called in the EDT the runnable is executed directly. Otherwise * {@link SwingUtilities#invokeAndWait(Runnable)} is used to execute the runnable. * * @param runnable * the {@link Runnable} to execute */ public static void invokeAndWait(final Runnable runnable) { if (SwingUtilities.isEventDispatchThread()) { runnable.run(); } else { try { SwingUtilities.invokeAndWait(runnable); } catch (InvocationTargetException | InterruptedException ex) { LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.tools.SwingTools.edt_event_failed", ex); } } } /** * Invokes a runnable on the EDT and waits until the execution has finished. If this method is * called in the EDT the runnable is executed directly. Otherwise * {@link SwingUtilities#invokeAndWait(Runnable)} is used to execute the runnable. * * @param resultRunnable * the {@link ResultRunnable} to execute * @return the value returned by {@link ResultRunnable} * @since 6.5.0 */ public static <T> T invokeAndWaitWithResult(final ResultRunnable<T> resultRunnable) { final ResultContainer<T> resultContainer = new ResultContainer<>(); if (SwingUtilities.isEventDispatchThread()) { resultContainer.value = resultRunnable.run(); } else { try { SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { resultContainer.value = resultRunnable.run(); } }); } catch (InvocationTargetException | InterruptedException ex) { LogService.getRoot().log(Level.WARNING, "com.rapidminer.gui.tools.SwingTools.edt_event_failed", ex); } } return resultContainer.value; } /** * Invokes a runnable on the EDT via {@link SwingUtilities#invokeLater(Runnable)} or directly if * this method is called from within the EDT. * * @param runnable * the {@link Runnable} to execute * @since 6.5.0 */ public static void invokeLater(final Runnable runnable) { if (SwingUtilities.isEventDispatchThread()) { runnable.run(); } else { SwingUtilities.invokeLater(runnable); } } /** * Adds a help icon to the provided JPanel which shows a tooltip when hovering over it. * * @param tooltipContent * the content of the tooltip * @param labelPanel * the panel which will be used to add the label. The panel needs to have a * {@link BorderLayout} as layout manager as the label will be added with the * constraint {@link BorderLayout#EAST}. * @param owner * the dialog owner of the labelPanel * @since 7.0.0 */ public static void addTooltipHelpIconToLabel(final String tooltipContent, JPanel labelPanel, final JDialog owner) { final JLabel helpLabel = initializeHelpLabel(labelPanel); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { TipProvider tipProvider = new TipProvider() { @Override public String getTip(Object id) { if (id == null) { return null; } else { return tooltipContent; } } @Override public Object getIdUnder(Point point) { return helpLabel; } @Override public Component getCustomComponent(Object id) { return null; } }; setupTooltip(tipProvider, owner, helpLabel); } }); } /** * Adds a tooltip associated with the tipProvider to the helpLabel. * * @param tipProvider * provides the tooltip. * @param owner * the owner of the dialog of which contains the helpLabel * @param helpLabel * the label for which to display the tooltip * @since 7.0.0 */ public static void setupTooltip(TipProvider tipProvider, final JDialog owner, final JLabel helpLabel) { ToolTipWindow toolTipWindow = new ToolTipWindow(owner, tipProvider, helpLabel, TooltipLocation.BELOW); toolTipWindow.setOnlyWhenFocussed(false); toolTipWindow.setToolTipDelay(TOOL_TIP_DELAY); } /** * Creates a helpLabel and adds it to the labelPanel. The helpLabel shows a help icon. * * @param labelPanel * the panel which will be used to add the label. The panel needs to have a * {@link BorderLayout} as layout manager as the label will be added with the * constraint {@link BorderLayout#EAST}. * @return the helpLabel for further use * @since 7.0.0 */ public static JLabel initializeHelpLabel(JPanel labelPanel) { JPanel helpPanel = new JPanel(); helpPanel.setLayout(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); gbc.anchor = GridBagConstraints.NORTH; gbc.weightx = 1.0; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.insets = new Insets(0, 3, 0, 0); final JLabel helpLabel = new JLabel(); helpLabel.setIcon(createIcon(HELP_ICON_PATH)); helpLabel.setFocusable(false); gbc.anchor = GridBagConstraints.CENTER; helpPanel.add(helpLabel, gbc); gbc.gridy += 1; gbc.fill = GridBagConstraints.HORIZONTAL; gbc.weighty = 1.0; gbc.insets = new Insets(0, 0, 0, 0); helpPanel.add(new JLabel(), gbc); labelPanel.add(helpPanel, BorderLayout.EAST); return helpLabel; } /** * Attempts to disable the default clear type rendering on Windows such that rendering hints can * be used. Has no effect on operating systems other than Microsoft Windows. Might fail * silently. * * @param component * the component for which to disable clear type in order to use rendering hints */ public static void disableClearType(JComponent component) { if (SystemInfoUtilities.getOperatingSystem() == OperatingSystem.WINDOWS && AA_TEXT_PROPERTY != null) { component.putClientProperty(AA_TEXT_PROPERTY, null); } } /** * Looks up the internal property {@code SwingUtilities2.AA_TEXT_PROPERTY_KEY}, returns * {@code null} if not defined or if running on an operating system other than Microsoft * Windows. * <p> * The implementation uses reflection to look up the property, since it it not part of the * official Swing specification and might not be available. * * @return the property to disable clear type on windows or {@code null} * @see http://stackoverflow.com/questions/18764585/text-antialiasing-broken-in-java-1-7-windows */ private static Object getAaTextProperty() { Object aatextProperty = null; if (SystemInfoUtilities.getOperatingSystem() == OperatingSystem.WINDOWS) { try { Class<?> clazz = Class.forName("sun.swing.SwingUtilities2"); Field field = clazz.getField("AA_TEXT_PROPERTY_KEY"); if (field != null) { aatextProperty = field.get(null); } } catch (Throwable e) { // do nothing, cannot use the property } } return aatextProperty; } }