/* * TextureChoiceComponent.java 05 oct. 2007 * * Sweet Home 3D, Copyright (c) 2007 Emmanuel PUYBARET / eTeks <info@eteks.com> * * 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 2 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, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.eteks.sweethome3d.swing; import java.awt.Color; import java.awt.Component; import java.awt.ComponentOrientation; import java.awt.Dimension; import java.awt.EventQueue; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.GridLayout; import java.awt.Insets; import java.awt.KeyboardFocusManager; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import javax.swing.AbstractListModel; import javax.swing.BorderFactory; import javax.swing.DefaultListCellRenderer; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.KeyStroke; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.TransferHandler; import javax.swing.border.EtchedBorder; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import com.eteks.sweethome3d.model.CatalogTexture; import com.eteks.sweethome3d.model.CollectionEvent; import com.eteks.sweethome3d.model.CollectionListener; import com.eteks.sweethome3d.model.HomeTexture; import com.eteks.sweethome3d.model.TextureImage; import com.eteks.sweethome3d.model.TexturesCatalog; import com.eteks.sweethome3d.model.TexturesCategory; import com.eteks.sweethome3d.model.UserPreferences; import com.eteks.sweethome3d.tools.OperatingSystem; import com.eteks.sweethome3d.viewcontroller.TextureChoiceController; import com.eteks.sweethome3d.viewcontroller.TextureChoiceView; import com.eteks.sweethome3d.viewcontroller.View; /** * Button displaying a texture as an icon. When the user clicks * on this button a dialog appears to let him choose an other texture. */ public class TextureChoiceComponent extends JButton implements TextureChoiceView { private final UserPreferences preferences; /** * Creates a texture button. */ public TextureChoiceComponent(final UserPreferences preferences, final TextureChoiceController controller) { this.preferences = preferences; JLabel dummyLabel = new JLabel("Text"); Dimension iconDimension = dummyLabel.getPreferredSize(); final int iconHeight = iconDimension.height; controller.addPropertyChangeListener(TextureChoiceController.Property.TEXTURE, new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent ev) { repaint(); } }); setIcon(new Icon() { public int getIconWidth() { return iconHeight; } public int getIconHeight() { return iconHeight; } public void paintIcon(Component c, Graphics g, int x, int y) { g.setColor(Color.BLACK); g.drawRect(x + 2, y + 2, iconHeight - 5, iconHeight - 5); HomeTexture texture = controller.getTexture(); if (texture != null) { Icon icon = IconManager.getInstance().getIcon( texture.getImage(), iconHeight - 6, TextureChoiceComponent.this); if (icon.getIconWidth() != icon.getIconHeight()) { Graphics2D g2D = (Graphics2D)g; AffineTransform previousTransform = g2D.getTransform(); g2D.translate(x + 3, y + 3); g2D.scale((float)icon.getIconHeight() / icon.getIconWidth(), 1); icon.paintIcon(c, g2D, 0, 0); g2D.setTransform(previousTransform); } else { icon.paintIcon(c, g, x + 3, y + 3); } } } }); // Add a listener to update texture addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ev) { final TexturePanel texturePanel = new TexturePanel(preferences, controller); texturePanel.displayView(TextureChoiceComponent.this); } }); } /** * Displays a dialog that let user choose whether he wants to delete * the selected texture from catalog or not. * @return <code>true</code> if user confirmed to delete. */ public boolean confirmDeleteSelectedCatalogTexture() { // Retrieve displayed text in buttons and message String message = this.preferences.getLocalizedString( TextureChoiceComponent.class, "confirmDeleteSelectedCatalogTexture.message"); String title = this.preferences.getLocalizedString( TextureChoiceComponent.class, "confirmDeleteSelectedCatalogTexture.title"); String delete = this.preferences.getLocalizedString( TextureChoiceComponent.class, "confirmDeleteSelectedCatalogTexture.delete"); String cancel = this.preferences.getLocalizedString( TextureChoiceComponent.class, "confirmDeleteSelectedCatalogTexture.cancel"); return JOptionPane.showOptionDialog( KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow(), message, title, JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, new Object [] {delete, cancel}, cancel) == JOptionPane.OK_OPTION; } /** * A panel that displays available textures in a list to let user make choose one. */ private static class TexturePanel extends JPanel { private static final int PREVIEW_ICON_HEIGHT = 64; private TextureChoiceController controller; private TextureImage previewTexture; private JLabel chosenTextureLabel; private JLabel texturePreviewLabel; private JLabel availableTexturesLabel; private JList availableTexturesList; private JButton importTextureButton; private JButton modifyTextureButton; private JButton deleteTextureButton; public TexturePanel(UserPreferences preferences, TextureChoiceController controller) { super(new GridBagLayout()); this.controller = controller; createComponents(preferences, controller); setMnemonics(preferences); layoutComponents(); } /** * Creates and initializes components. */ private void createComponents(final UserPreferences preferences, final TextureChoiceController controller) { this.availableTexturesLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences, TextureChoiceComponent.class, "availableTexturesLabel.text")); this.availableTexturesList = new JList(createListModel(preferences.getTexturesCatalog())); this.availableTexturesList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); this.availableTexturesList.setCellRenderer(new TextureListCellRenderer()); this.availableTexturesList.getSelectionModel().addListSelectionListener( new ListSelectionListener() { public void valueChanged(ListSelectionEvent ev) { CatalogTexture selectedTexture = (CatalogTexture)availableTexturesList.getSelectedValue(); setPreviewTexture(selectedTexture); if (modifyTextureButton != null) { modifyTextureButton.setEnabled(selectedTexture != null && selectedTexture.isModifiable()); } if (deleteTextureButton != null) { deleteTextureButton.setEnabled(selectedTexture != null && selectedTexture.isModifiable()); } } }); this.chosenTextureLabel = new JLabel(preferences.getLocalizedString( TextureChoiceComponent.class, "chosenTextureLabel.text")); this.texturePreviewLabel = new JLabel() { private int lastIconWidth; @Override protected void paintComponent(Graphics g) { // If icon width changed after its loading Icon icon = getIcon(); if (icon != null && icon.getIconWidth() != this.lastIconWidth) { // Revalidate label to layout again texture panel this.lastIconWidth = icon.getIconWidth(); revalidate(); } else { super.paintComponent(g); } } @Override public void setIcon(Icon icon) { if (icon != null) { this.lastIconWidth = icon.getIconWidth(); } super.setIcon(icon); } }; // Update edited texture in texture panel setPreviewTexture(controller.getTexture()); try { String importTextureButtonText = SwingTools.getLocalizedLabelText( preferences, TextureChoiceComponent.class, "importTextureButton.text"); this.texturePreviewLabel.setBorder(SwingTools.getDropableComponentBorder()); // Add to label a transfer handler to let user drag and drop a file on it this.texturePreviewLabel.setTransferHandler(new TransferHandler() { @Override public boolean canImport(JComponent comp, DataFlavor [] flavors) { return Arrays.asList(flavors).contains(DataFlavor.javaFileListFlavor); } @Override public boolean importData(JComponent comp, Transferable transferedFiles) { try { List<File> files = (List<File>)transferedFiles.getTransferData(DataFlavor.javaFileListFlavor); final String textureName = files.get(0).getAbsolutePath(); EventQueue.invokeLater(new Runnable() { public void run() { controller.importTexture(textureName); } }); return true; } catch (UnsupportedFlavorException ex) { return false; } catch (IOException ex) { return false; } } }); this.importTextureButton = new JButton(importTextureButtonText); this.importTextureButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ev) { controller.importTexture(); } }); this.modifyTextureButton = new JButton(SwingTools.getLocalizedLabelText(preferences, TextureChoiceComponent.class, "modifyTextureButton.text")); this.modifyTextureButton.setEnabled(false); this.modifyTextureButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ev) { controller.modifyTexture((CatalogTexture)availableTexturesList.getSelectedValue()); } }); this.deleteTextureButton = new JButton(SwingTools.getLocalizedLabelText(preferences, TextureChoiceComponent.class, "deleteTextureButton.text")); this.deleteTextureButton.setEnabled(false); this.deleteTextureButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ev) { controller.deleteTexture((CatalogTexture)availableTexturesList.getSelectedValue()); } }); preferences.getTexturesCatalog().addTexturesListener(new TexturesCatalogListener(this)); } catch (IllegalArgumentException ex) { // Do not support import texture if importTextureText isn't defined this.texturePreviewLabel.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED)); } } /** * Renderer used to display the textures in list. */ private static class TextureListCellRenderer extends DefaultListCellRenderer { private Font defaultFont; private Font modifiablePieceFont; @Override public Component getListCellRendererComponent(final JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { // Initialize fonts if not done if (this.defaultFont == null) { this.defaultFont = getFont(); this.modifiablePieceFont = new Font(this.defaultFont.getFontName(), Font.ITALIC, this.defaultFont.getSize()); } final CatalogTexture texture = (CatalogTexture)value; value = texture.getName(); value = texture.getCategory().getName() + " - " + value; Component component = super.getListCellRendererComponent( list, value, index, isSelected, cellHasFocus); setIcon(new Icon() { public int getIconWidth() { return 16; } public int getIconHeight() { return 16; } public void paintIcon(Component c, Graphics g, int x, int y) { Icon icon = IconManager.getInstance().getIcon( texture.getImage(), getIconHeight(), list); if (icon.getIconWidth() != icon.getIconHeight()) { Graphics2D g2D = (Graphics2D)g; AffineTransform previousTransform = g2D.getTransform(); g2D.translate(x, y); g2D.scale((float)icon.getIconHeight() / icon.getIconWidth(), 1); icon.paintIcon(c, g2D, 0, 0); g2D.setTransform(previousTransform); } else { icon.paintIcon(c, g, x, y); } } }); setFont(texture.isModifiable() ? this.modifiablePieceFont : this.defaultFont); return component; } } /** * Catalog listener that updates textures list each time a texture * is deleted or added in textures catalog. This listener is bound to this component * with a weak reference to avoid strong link between catalog and this component. */ private static class TexturesCatalogListener implements CollectionListener<CatalogTexture> { private WeakReference<TexturePanel> texturePanel; public TexturesCatalogListener(TexturePanel texturePanel) { this.texturePanel = new WeakReference<TexturePanel>(texturePanel); } public void collectionChanged(CollectionEvent<CatalogTexture> ev) { // If controller was garbage collected, remove this listener from catalog final TexturePanel texturePanel = this.texturePanel.get(); if (texturePanel == null) { ((TexturesCatalog)ev.getSource()).removeTexturesListener(this); } else { texturePanel.availableTexturesList.setModel( texturePanel.createListModel((TexturesCatalog)ev.getSource())); switch (ev.getType()) { case ADD: texturePanel.availableTexturesList.setSelectedValue(ev.getItem(), true); break; case DELETE: texturePanel.availableTexturesList.clearSelection(); break; } } } } /** * Sets components mnemonics and label / component associations. */ private void setMnemonics(UserPreferences preferences) { if (!OperatingSystem.isMacOSX()) { this.availableTexturesLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString( TextureChoiceComponent.class, "availableTexturesLabel.mnemonic")).getKeyCode()); this.availableTexturesLabel.setLabelFor(this.availableTexturesList); if (this.importTextureButton != null) { this.importTextureButton.setMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString( TextureChoiceComponent.class, "importTextureButton.mnemonic")).getKeyCode()); this.modifyTextureButton.setMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString( TextureChoiceComponent.class, "modifyTextureButton.mnemonic")).getKeyCode()); this.deleteTextureButton.setMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString( TextureChoiceComponent.class, "deleteTextureButton.mnemonic")).getKeyCode()); } } } /** * Layouts components in panel with their labels. */ private void layoutComponents() { // First row add(this.availableTexturesLabel, new GridBagConstraints( 0, 0, 1, 1, 0, 0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 5, 15), 0, 0)); add(this.chosenTextureLabel, new GridBagConstraints( 1, 0, 1, 1, 0, 0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, new Insets(0, 0, 5, 0), 0, 0)); // Second row add(new JScrollPane(this.availableTexturesList), new GridBagConstraints( 0, 1, 1, 2, 1, 1, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 5, 15), 50, 0)); SwingTools.installFocusBorder(this.availableTexturesList); add(this.texturePreviewLabel, new GridBagConstraints( 1, 1, 1, 1, 0, 0, GridBagConstraints.NORTH, GridBagConstraints.NONE, new Insets(0, 0, 10, 0), 0, 0)); if (this.importTextureButton != null) { // Third row JPanel buttonsPanel = new JPanel(new GridLayout(3, 1, 2, 2)); buttonsPanel.add(this.importTextureButton); buttonsPanel.add(this.modifyTextureButton); buttonsPanel.add(this.deleteTextureButton); add(buttonsPanel, new GridBagConstraints( 1, 2, 1, 1, 0, 0, GridBagConstraints.NORTH, GridBagConstraints.NONE, new Insets(0, 0, 10, 0), 0, 0)); } } /** * Returns the chosen texture. */ public TextureImage getPreviewTexture() { return this.previewTexture; } /** * Sets the chosen texture. */ public void setPreviewTexture(TextureImage previewTexture) { this.previewTexture = previewTexture; if (previewTexture != null) { this.texturePreviewLabel.setIcon( IconManager.getInstance().getIcon(previewTexture.getImage(), PREVIEW_ICON_HEIGHT, this.texturePreviewLabel)); this.texturePreviewLabel.setToolTipText(previewTexture.getName()); } else { // Preview a dummy empty icon this.texturePreviewLabel.setIcon(new Icon() { public int getIconHeight() { return PREVIEW_ICON_HEIGHT; } public int getIconWidth() { return PREVIEW_ICON_HEIGHT; } public void paintIcon(Component c, Graphics g, int x, int y) { } }); this.texturePreviewLabel.setToolTipText(null); } // Update selection in texture list this.availableTexturesList.setSelectedValue(previewTexture, true); if (this.availableTexturesList.getSelectedValue() != previewTexture) { int selectedIndex = this.availableTexturesList.getSelectedIndex(); this.availableTexturesList.removeSelectionInterval(selectedIndex, selectedIndex); } } /** * Returns a list model from textures catalog. */ private AbstractListModel createListModel(TexturesCatalog texturesCatalog) { final CatalogTexture [] textures = getTextures(texturesCatalog); return new AbstractListModel() { public Object getElementAt(int index) { return textures [index]; } public int getSize() { return textures.length; } }; } /** * Returns the array of textures in catalog. */ private CatalogTexture [] getTextures(TexturesCatalog texturesCatalog) { List<CatalogTexture> textures = new ArrayList<CatalogTexture>(); for (TexturesCategory category : texturesCatalog.getCategories()) { for (CatalogTexture texture : category.getTextures()) { textures.add(texture); } } return textures.toArray(new CatalogTexture [textures.size()]); } public void displayView(View textureChoiceComponent) { // Show panel in a resizable modal dialog final JOptionPane optionPane = new JOptionPane(this, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION); JComponent parentComponent = SwingUtilities.getRootPane((JComponent)textureChoiceComponent); if (parentComponent != null) { optionPane.setComponentOrientation(parentComponent.getComponentOrientation()); } final JDialog dialog = optionPane.createDialog(parentComponent, controller.getDialogTitle()); dialog.applyComponentOrientation(ComponentOrientation.getOrientation(Locale.getDefault())); dialog.setResizable(true); // Pack again because resize decorations may have changed dialog preferred size dialog.pack(); dialog.setMinimumSize(getPreferredSize()); // Add a listener that transfer focus to focusable field of texture panel when dialog is shown dialog.addComponentListener(new ComponentAdapter() { @Override public void componentShown(ComponentEvent ev) { KeyboardFocusManager.getCurrentKeyboardFocusManager().focusNextComponent(TexturePanel.this); dialog.removeComponentListener(this); } }); this.availableTexturesList.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent ev) { // Close on double clicks in texture list if (ev.getClickCount() == 2) { optionPane.setValue(JOptionPane.OK_OPTION); availableTexturesList.removeMouseListener(this); } } }); dialog.setVisible(true); dialog.dispose(); if (Integer.valueOf(JOptionPane.OK_OPTION).equals(optionPane.getValue())) { TextureImage selectedTexture = getPreviewTexture(); if (selectedTexture instanceof HomeTexture || selectedTexture == null) { this.controller.setTexture((HomeTexture)selectedTexture); } else { this.controller.setTexture(new HomeTexture(selectedTexture)); } } } } }