/* * SK's Minecraft Launcher * Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors * Please see LICENSE.txt for license information. */ package com.skcraft.launcher.swing; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.skcraft.launcher.LauncherException; import com.skcraft.launcher.util.SharedLocale; import com.skcraft.launcher.util.SwingExecutor; import lombok.NonNull; import lombok.extern.java.Log; import javax.imageio.ImageIO; import javax.swing.*; import javax.swing.border.Border; import javax.swing.plaf.basic.BasicSplitPaneDivider; import javax.swing.plaf.basic.BasicSplitPaneUI; import javax.swing.text.JTextComponent; import java.awt.*; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.ClipboardOwner; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import java.awt.event.ActionListener; import java.awt.image.BufferedImage; import java.io.*; import java.lang.reflect.InvocationTargetException; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import static com.skcraft.launcher.util.SharedLocale.tr; import static org.apache.commons.io.IOUtils.closeQuietly; /** * Swing utility methods. */ @Log public final class SwingHelper { private static final ClipboardOwner clipboardOwner = new ClipboardOwner() { @Override public void lostOwnership(Clipboard clipboard, Transferable contents) { } }; private static final Joiner NEW_LINE_JOINER = Joiner.on("\n"); private SwingHelper() { } public static String htmlEscape(String str) { return str.replace(">", ">") .replace("<", "<") .replace("&", "&"); } public static void setClipboard(String text) { Toolkit.getDefaultToolkit().getSystemClipboard().setContents( new StringSelection(text), clipboardOwner); } public static void browseDir(File file, Component component) { try { Desktop.getDesktop().open(file); } catch (IOException e) { JOptionPane.showMessageDialog(component, tr("errors.openDirError", file.getAbsolutePath()), SharedLocale.tr("errorTitle"), JOptionPane.ERROR_MESSAGE); } } /** * Opens a system web browser for the given URL. * * @param url the URL * @param parentComponent the component from which to show any errors */ public static void openURL(@NonNull String url, @NonNull Component parentComponent) { try { openURL(new URL(url), parentComponent); } catch (MalformedURLException e) { } } /** * Opens a system web browser for the given URL. * * @param url the URL * @param parentComponent the component from which to show any errors */ public static void openURL(URL url, Component parentComponent) { try { Desktop.getDesktop().browse(url.toURI()); } catch (IOException e) { showErrorDialog(parentComponent, tr("errors.openUrlError", url.toString()), SharedLocale.tr("errorTitle")); } catch (URISyntaxException e) { } } /** * Shows an popup error dialog, with potential extra details shown either immediately * or available on the dialog. * * @param parentComponent the frame from which the dialog is displayed, otherwise * null to use the default frame * @param message the message to display * @param title the title string for the dialog * @see #showMessageDialog(java.awt.Component, String, String, String, int) for details */ public static void showErrorDialog(Component parentComponent, @NonNull String message, @NonNull String title) { showErrorDialog(parentComponent, message, title, null); } /** * Shows an popup error dialog, with potential extra details shown either immediately * or available on the dialog. * * @param parentComponent the frame from which the dialog is displayed, otherwise * null to use the default frame * @param message the message to display * @param title the title string for the dialog * @param throwable the exception, or null if there is no exception to show * @see #showMessageDialog(java.awt.Component, String, String, String, int) for details */ public static void showErrorDialog(Component parentComponent, @NonNull String message, @NonNull String title, Throwable throwable) { String detailsText = null; // Get a string version of the exception and use that for // the extra details text if (throwable != null) { StringWriter sw = new StringWriter(); throwable.printStackTrace(new PrintWriter(sw)); detailsText = sw.toString(); } showMessageDialog(parentComponent, message, title, detailsText, JOptionPane.ERROR_MESSAGE); } /** * Show a message dialog using * {@link javax.swing.JOptionPane#showMessageDialog(java.awt.Component, Object, String, int)}. * * <p>The dialog will be shown from the Event Dispatch Thread, regardless of the * thread it is called from. In either case, the method will block until the * user has closed the dialog (or dialog creation fails for whatever reason).</p> * * @param parentComponent the frame from which the dialog is displayed, otherwise * null to use the default frame * @param message the message to display * @param title the title string for the dialog * @param messageType see {@link javax.swing.JOptionPane#showMessageDialog(java.awt.Component, Object, String, int)} * for available message types */ public static void showMessageDialog(final Component parentComponent, @NonNull final String message, @NonNull final String title, final String detailsText, final int messageType) { if (SwingUtilities.isEventDispatchThread()) { // To force the label to wrap, convert the message to broken HTML String htmlMessage = "<html><div style=\"width: 250px\">" + htmlEscape(message); JPanel panel = new JPanel(new BorderLayout(0, detailsText != null ? 20 : 0)); // Add the main message panel.add(new JLabel(htmlMessage), BorderLayout.NORTH); // Add the extra details if (detailsText != null) { JTextArea textArea = new JTextArea(SharedLocale.tr("errors.reportErrorPreface") + detailsText); JLabel tempLabel = new JLabel(); textArea.setFont(tempLabel.getFont()); textArea.setBackground(tempLabel.getBackground()); textArea.setTabSize(2); textArea.setEditable(false); textArea.setComponentPopupMenu(TextFieldPopupMenu.INSTANCE); JScrollPane scrollPane = new JScrollPane(textArea); scrollPane.setPreferredSize(new Dimension(350, 120)); panel.add(scrollPane, BorderLayout.CENTER); } JOptionPane.showMessageDialog( parentComponent, panel, title, messageType); } else { // Call method again from the Event Dispatch Thread try { SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { showMessageDialog( parentComponent, message, title, detailsText, messageType); } }); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (InvocationTargetException e) { throw new RuntimeException(e); } } } /** * Asks the user a binary yes or no question. * * @param parentComponent the component * @param message the message to display * @param title the title string for the dialog * @return whether 'yes' was selected */ public static boolean confirmDialog(final Component parentComponent, @NonNull final String message, @NonNull final String title) { if (SwingUtilities.isEventDispatchThread()) { return JOptionPane.showConfirmDialog( parentComponent, message, title, JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION; } else { // Use an AtomicBoolean to pass the result back from the // Event Dispatcher Thread final AtomicBoolean yesSelected = new AtomicBoolean(); try { SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { yesSelected.set(confirmDialog(parentComponent, title, message)); } }); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (InvocationTargetException e) { throw new RuntimeException(e); } return yesSelected.get(); } } /** * Equalize the width of the given components. * * @param component component */ public static void equalWidth(Component ... component) { double widest = 0; for (Component comp : component) { Dimension dim = comp.getPreferredSize(); if (dim.getWidth() > widest) { widest = dim.getWidth(); } } for (Component comp : component) { Dimension dim = comp.getPreferredSize(); comp.setPreferredSize(new Dimension((int) widest, (int) dim.getHeight())); } } /** * Remove all the opaqueness of the given components and child components. * * @param components list of components */ public static void removeOpaqueness(@NonNull Component ... components) { for (Component component : components) { if (component instanceof JComponent) { JComponent jComponent = (JComponent) component; jComponent.setOpaque(false); removeOpaqueness(jComponent.getComponents()); } } } public static Image createImage(Class<?> clazz, String name) { URL resource = clazz.getResource(name); if (resource != null) { return Toolkit.getDefaultToolkit().createImage(resource); } else { log.log(Level.WARNING, "Failed to read image from resource: " + name); return null; } } public static Icon createIcon(Class<?> clazz, String name) { Image image = createImage(clazz, name); if (image != null) { return new ImageIcon(image); } else { return new EmptyIcon(16, 16); } } public static Icon createIcon(Class<?> clazz, String name, int width, int height) { Image image = createImage(clazz, name); if (image != null) { return new ImageIcon(image.getScaledInstance(width, height, Image.SCALE_SMOOTH)); } else { return new EmptyIcon(width, height); } } public static BufferedImage readBufferedImage(Class<?> clazz, String path) { InputStream in = null; try { in = clazz.getResourceAsStream(path); if (in != null) { return ImageIO.read(in); } } catch (IOException e) { } finally { closeQuietly(in); } return null; } public static void setFrameIcon(JFrame frame, Class<?> clazz, String path) { BufferedImage image = readBufferedImage(clazz, path); if (image != null) { frame.setIconImage(image); } } /** * Focus a component. * * <p>The focus call happens in {@link javax.swing.SwingUtilities#invokeLater(Runnable)}.</p> * * @param component the component */ public static void focusLater(@NonNull final Component component) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (component instanceof JTextComponent) { ((JTextComponent) component).selectAll(); } component.requestFocusInWindow(); } }); } public static void flattenJSplitPane(JSplitPane splitPane) { splitPane.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); BasicSplitPaneUI flatDividerSplitPaneUI = new BasicSplitPaneUI() { @Override public BasicSplitPaneDivider createDefaultDivider() { return new BasicSplitPaneDivider(this) { @Override public void setBorder(Border b) { } }; } }; splitPane.setUI(flatDividerSplitPaneUI); splitPane.setBorder(null); } public static void addErrorDialogCallback(final Window owner, ListenableFuture<?> future) { Futures.addCallback(future, new FutureCallback<Object>() { @Override public void onSuccess(Object result) { } @Override public void onFailure(Throwable t) { if (t instanceof InterruptedException || t instanceof CancellationException) { return; } String message; if (t instanceof LauncherException) { message = t.getLocalizedMessage(); t = t.getCause(); } else { message = t.getLocalizedMessage(); if (message == null) { message = SharedLocale.tr("errors.genericError"); } } log.log(Level.WARNING, "Task failed", t); SwingHelper.showErrorDialog(owner, message, SharedLocale.tr("errorTitle"), t); } }, SwingExecutor.INSTANCE); } public static Component alignTabbedPane(Component component) { JPanel container = new JPanel(); container.setLayout(new BoxLayout(container, BoxLayout.Y_AXIS)); container.add(component); container.add(new Box.Filler(new Dimension(0, 0), new Dimension(0, 10000), new Dimension(0, 10000))); SwingHelper.removeOpaqueness(container); return container; } public static void setTextAndResetCaret(JTextComponent component, String text) { component.setText(text); component.setCaretPosition(0); } public static JScrollPane wrapScrollPane(Component component) { JScrollPane scrollPane = new JScrollPane(component); scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); return scrollPane; } public static String listToLines(List<String> lines) { return SwingHelper.NEW_LINE_JOINER.join(lines); } public static List<String> linesToList(String text) { String[] tokens = text.replace("\r", "\n").split("\n"); List<String> values = Lists.newArrayList(); for (String token : tokens) { String value = token.trim(); if (!value.isEmpty()) { values.add(value); } } return values; } public static void addActionListeners(AbstractButton button, ActionListener[] listeners) { for (ActionListener listener : listeners) { button.addActionListener(listener); } } public static boolean setLookAndFeel(String lookAndFeel) { try { UIManager.setLookAndFeel(lookAndFeel); return true; } catch (Exception e) { log.log(Level.WARNING, "Failed to set look and feel to " + lookAndFeel, e); return false; } } public static void setSwingProperties(String appName) { UIManager.getDefaults().put("SplitPane.border", BorderFactory.createEmptyBorder()); System.setProperty("com.apple.mrj.application.apple.menu.about.name", appName); System.setProperty("apple.laf.useScreenMenuBar", "true"); } }