Java tutorial
/* * Sweeper - Duplicate file cleaner * Copyright (C) 2012 Bogdan Ciprian Pistol * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package gg.pistol.sweeper.gui.component; import gg.pistol.sweeper.i18n.I18n; import gg.pistol.sweeper.i18n.SupportedLocale; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import javax.annotation.Nullable; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.SwingUtilities; import javax.swing.text.DefaultEditorKit; import javax.swing.text.JTextComponent; import com.google.common.base.Preconditions; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import org.apache.commons.lang3.StringEscapeUtils; /** * Extension of {@link DynamicPanel} that provides decorations: border, side image and/or close button. * * <p>The side image position is taking into account the locale (left-to-right or right-to-left language) and the close * button contains a localized string message. * * <p>Use this class only from the AWT event dispatching thread (see {@link SwingUtilities#invokeLater}). * * @author Bogdan Pistol */ public abstract class DecoratedPanel extends DynamicPanel { private static final int DEFAULT_BORDER = 5; private final int border; private final boolean closeButton; @Nullable private final Icon sideImage; private final Multimap<String, JComponent> sizeGroups; /** * Creates an instance with a default border value. * * @param i18n * the internationalization instance * @param closeButton * whether to have a close button or not * @param sideImage * the side image or null */ protected DecoratedPanel(I18n i18n, boolean closeButton, @Nullable Icon sideImage) { this(i18n, DEFAULT_BORDER, closeButton, sideImage); } /** * Constructor * * @param i18n * the internationalization instance * @param border * the border size or 0 for no border * @param closeButton * whether to have a close button or not * @param sideImage * the side image or null */ protected DecoratedPanel(I18n i18n, int border, boolean closeButton, @Nullable Icon sideImage) { super(Preconditions.checkNotNull(i18n)); Preconditions.checkArgument(border >= 0); this.border = border; this.closeButton = closeButton; this.sideImage = sideImage; sizeGroups = ArrayListMultimap.create(); if (border > 0) { setBorder(BorderFactory.createEmptyBorder(border, border, border, border)); } setLayout(new BorderLayout()); } /** * This method is final to not be further extended. To add components to the decorated panel extend * the {@link #addComponents(JPanel)} method. */ final protected void addComponents() { if (closeButton) { addCloseButton(); } if (sideImage != null) { addSideImage(); } JPanel contentPanel = createVerticalPanel(); add(contentPanel, BorderLayout.CENTER); addComponents(contentPanel); computePreferredSizes(); } private void computePreferredSizes() { for (String key : sizeGroups.keySet()) { int width = -1; int height = -1; for (JComponent component : sizeGroups.get(key)) { if (width < component.getPreferredSize().width) { width = component.getPreferredSize().width; } if (height < component.getPreferredSize().height) { height = component.getPreferredSize().height; } } // Sometimes the preferred width of a component can be too small to accommodate the contained text and in // that case the text is truncated and ellipsis will be shown. To fix it the width is increased. width += 2; for (JComponent component : sizeGroups.get(key)) { component.setMinimumSize(new Dimension(width, height)); component.setMaximumSize(new Dimension(width, height)); component.setPreferredSize(new Dimension(width, height)); } } sizeGroups.clear(); } /** * All the components should be added to the provided {@code contentPanel} based on the locale. The title of the * parent window should be configured based on the locale with this method. * * <p>This method will be called whenever the locale changes. * * @param contentPanel * the container where to add all the components, its layout manager is a horizontal {@link BoxLayout} * that takes into account the locale */ protected abstract void addComponents(JPanel contentPanel); private void addSideImage() { JPanel panel = createVerticalPanel(); panel.add(new JLabel(sideImage)); panel.add(Box.createVerticalGlue()); if (ComponentOrientation.getOrientation(i18n.getLocale()).isLeftToRight()) { panel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, border)); add(panel, BorderLayout.WEST); } else { panel.setBorder(BorderFactory.createEmptyBorder(0, border, 0, 0)); add(panel, BorderLayout.EAST); } } private void addCloseButton() { JPanel panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS)); panel.add(Box.createHorizontalGlue()); JButton button = new JButton(i18n.getString(I18n.BUTTON_CLOSE_ID)); panel.add(button); panel.add(Box.createHorizontalGlue()); button.addActionListener(closeAction()); panel.setBorder(BorderFactory.createEmptyBorder(border, 0, 0, 0)); add(panel, BorderLayout.SOUTH); } /** * The action to dispose the parent window. * * @return the action */ protected ActionListener closeAction() { return new ActionListener() { public void actionPerformed(ActionEvent e) { if (parentWindow == null) { // if there is no parent yet configured then ignore the action return; } parentWindow.dispose(); } }; } /** * Helper factory method for creating selectable text labels (for copy & paste support) * * @param text * the string that will be selectable * @return the selectable text component */ protected JComponent createTextLabel(String text) { Preconditions.checkNotNull(text); JTextField textLabel = new JTextField(text); textLabel.setEditable(false); textLabel.setBorder(null); textLabel.setOpaque(false); textLabel.setHorizontalAlignment(JTextField.CENTER); addCopyMenu(textLabel); return textLabel; } /** * Helper method to add a contextual menu on a text component that will allow for Copy & Select All text actions. */ protected void addCopyMenu(final JTextComponent component) { Preconditions.checkNotNull(component); if (!component.isEditable()) { component.setCursor(new Cursor(Cursor.TEXT_CURSOR)); } final JPopupMenu contextMenu = new JPopupMenu(); JMenuItem copy = new JMenuItem(component.getActionMap().get(DefaultEditorKit.copyAction)); copy.setText(i18n.getString(I18n.TEXT_COPY_ID)); contextMenu.add(copy); contextMenu.addSeparator(); JMenuItem selectAll = new JMenuItem(component.getActionMap().get(DefaultEditorKit.selectAllAction)); selectAll.setText(i18n.getString(I18n.TEXT_SELECT_ALL_ID)); contextMenu.add(selectAll); component.addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON3) { contextMenu.show(component, e.getX(), e.getY()); } } }); } /** * Helper factory method for creating clickable links. * * @param linkText * a string that will be displayed as a link * @param action * the action performed at click * @return the link component */ protected JComponent createLink(String linkText, final Runnable action) { Preconditions.checkNotNull(linkText); Preconditions.checkNotNull(action); JLabel link = new JLabel("<html><a href=''>" + linkText + "</a></html>"); link.setComponentOrientation(ComponentOrientation.getOrientation(i18n.getLocale())); link.setCursor(new Cursor(Cursor.HAND_CURSOR)); link.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { action.run(); } }); return link; } /** * Helper method to set the left alignment. * * @param component * the component to align * @return the aligned component */ protected <T extends JComponent> T alignLeft(T component) { return alignHorizontally(component, Component.LEFT_ALIGNMENT); } /** * Helper method to set the right alignment. * * @param component * the component to align * @return the aligned component */ protected <T extends JComponent> T alignRight(T component) { return alignHorizontally(component, Component.RIGHT_ALIGNMENT); } private <T extends JComponent> T alignHorizontally(T component, float alignment) { Preconditions.checkNotNull(component); if (!ComponentOrientation.getOrientation(i18n.getLocale()).isLeftToRight()) { if (alignment == Component.LEFT_ALIGNMENT) { alignment = Component.RIGHT_ALIGNMENT; } else if (alignment == Component.RIGHT_ALIGNMENT) { alignment = Component.LEFT_ALIGNMENT; } } component.setComponentOrientation(ComponentOrientation.getOrientation(i18n.getLocale())); component.setAlignmentX(alignment); return component; } /** * Helper method to set the center alignment. * * @param component * the component to align * @return the aligned component */ protected <T extends JComponent> T alignCenter(T component) { Preconditions.checkNotNull(component); component.setAlignmentX(Component.CENTER_ALIGNMENT); return component; } /** * Helper factory method for creating a horizontal box layout {@link JPanel} that takes into account the locale. * * @return the created panel */ protected JPanel createHorizontalPanel() { JPanel panel = new JPanel(); panel.setComponentOrientation(ComponentOrientation.getOrientation(i18n.getLocale())); panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS)); return panel; } /** * Helper factory method for creating a vertical box layout {@link JPanel}. * * @return the created panel */ protected JPanel createVerticalPanel() { JPanel panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); return panel; } /** * Creates an invisible, fixed-width component. This is useful for spacing components. * * <p>It provides the same functionality as {@link Box#createHorizontalStrut} with the difference that the created * fixed-width component does not expand vertically at all (has a maximum height of 0). * * @param width * the width of the invisible component in pixels * @return the horizontal fixed-width strut */ protected JComponent createHorizontalStrut(int width) { Preconditions.checkArgument(width >= 0); return new Box.Filler(new Dimension(width, 0), new Dimension(width, 0), new Dimension(width, 0)); } /** * Creates an invisible, fixed-height component. This is useful for spacing components. * * <p>It provides the same functionality as {@link Box#createVerticalStrut} with the difference that the created * fixed-height component does not expand horizontally at all (has a maximum width of 0). * * @param height * the height of the invisible component in pixels * @return the vertical fixed-height strut */ protected JComponent createVerticalStrut(int height) { Preconditions.checkArgument(height >= 0); return new Box.Filler(new Dimension(0, height), new Dimension(0, height), new Dimension(0, height)); } /** * Assign the provided {@code component} to a group of same preferred size components. All the components in * the group will have the preferred size of the biggest component from the group. * * @param groupId * an identifier for the group * @param component * the component to add to the group * @return the grouped component */ protected <T extends JComponent> T sizeGroup(String groupId, T component) { Preconditions.checkNotNull(groupId); Preconditions.checkNotNull(component); sizeGroups.put(groupId, component); return component; } /** * Helper factory method for creating a button. * * @param text * the text of the button * @param action * the action of the button * @return the newly created button */ protected JButton createButton(String text, ActionListener action) { Preconditions.checkNotNull(text); Preconditions.checkNotNull(action); JButton button = new JButton(text); button.addActionListener(action); return button; } /** * Helper factory method for creating a button and placing it in a group of same size components. * * @param text * the text of the button * @param action * the action of the button * @param sizeGroupId * the group identifier of the same size components * @return the newly created button */ protected JButton createButton(String text, ActionListener action, String sizeGroupId) { Preconditions.checkNotNull(text); Preconditions.checkNotNull(action); Preconditions.checkNotNull(sizeGroupId); JButton button = new JButton(text); button.addActionListener(action); return sizeGroup(sizeGroupId, button); } /** * Helper method to add an internationalized scroll pane (that is sensible to the left-to-right or right-to-left * languages) to the provided {@code component}. * * @param component * the component to wrap inside the scroll pane * @return the newly created scroll pane that wraps the provided {@code component} */ protected JComponent addScrollPane(JComponent component) { Preconditions.checkNotNull(component); JScrollPane scrollPane = new JScrollPane(component); scrollPane.setComponentOrientation(ComponentOrientation.getOrientation(i18n.getLocale())); return scrollPane; } /** * Helper method to create a {@link JLabel} with word-wrapping. */ protected JLabel createWordWrappingLabel(String text) { return new JLabel("<html>" + StringEscapeUtils.escapeHtml4(text) + "</html>"); } /** * Helper factory method for creating a language selector that provides functionality to change the locale. * * @param width * the preferred width of the component in pixels * @return the newly created language selector */ protected JComboBox createLanguageSelector(int width) { Preconditions.checkArgument(width >= 0); final JComboBox<SupportedLocale> selector = new JComboBox(i18n.getSupportedLocales().toArray()); selector.setSelectedItem(i18n.getCurrentSupportedLocale()); selector.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { i18n.setLocale((SupportedLocale) selector.getSelectedItem()); } }); int height = selector.getPreferredSize().height; selector.setMinimumSize(new Dimension(width, height)); selector.setMaximumSize(new Dimension(width, height)); selector.setPreferredSize(new Dimension(width, height)); selector.setMaximumRowCount(i18n.getSupportedLocales().size()); selector.setComponentOrientation(ComponentOrientation.getOrientation(i18n.getLocale())); return selector; } }