/*
* PhotoSizeAndQualityPanel.java 25 nov 2012
*
* Sweet Home 3D, Copyright (c) 2012 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.BorderLayout;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.WeakReference;
import java.util.Locale;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.KeyStroke;
import javax.swing.SpinnerNumberModel;
import javax.swing.ToolTipManager;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import com.eteks.sweethome3d.j3d.Component3DManager;
import com.eteks.sweethome3d.model.AspectRatio;
import com.eteks.sweethome3d.model.Home;
import com.eteks.sweethome3d.model.UserPreferences;
import com.eteks.sweethome3d.tools.OperatingSystem;
import com.eteks.sweethome3d.tools.ResourceURLContent;
import com.eteks.sweethome3d.viewcontroller.AbstractPhotoController;
/**
* A panel to edit photo size and quality.
* @author Emmanuel Puybaret
*/
public class PhotoSizeAndQualityPanel extends JPanel {
private JLabel widthLabel;
private JSpinner widthSpinner;
private JLabel heightLabel;
private JSpinner heightSpinner;
private JCheckBox applyProportionsCheckBox;
private JComboBox aspectRatioComboBox;
private JLabel qualityLabel;
private JSlider qualitySlider;
private JLabel fastQualityLabel;
private JLabel bestQualityLabel;
public PhotoSizeAndQualityPanel(Home home,
UserPreferences preferences,
AbstractPhotoController controller) {
super(new GridBagLayout());
createComponents(home, preferences, controller);
setMnemonics(preferences);
layoutComponents();
preferences.addPropertyChangeListener(UserPreferences.Property.LANGUAGE, new LanguageChangeListener(this));
}
/**
* Creates and initializes components.
*/
private void createComponents(final Home home,
final UserPreferences preferences,
final AbstractPhotoController controller) {
// Create width label and spinner bound to WIDTH controller property
this.widthLabel = new JLabel();
final SpinnerNumberModel widthSpinnerModel = new SpinnerNumberModel(480, 10, 10000, 10);
this.widthSpinner = new AutoCommitSpinner(widthSpinnerModel);
widthSpinnerModel.setValue(controller.getWidth());
widthSpinnerModel.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent ev) {
controller.setWidth(((Number)widthSpinnerModel.getValue()).intValue());
}
});
controller.addPropertyChangeListener(AbstractPhotoController.Property.WIDTH,
new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
widthSpinnerModel.setValue(controller.getWidth());
}
});
// Create height label and spinner bound to HEIGHT controller property
this.heightLabel = new JLabel();
final SpinnerNumberModel heightSpinnerModel = new SpinnerNumberModel(480, 10, 10000, 10);
this.heightSpinner = new AutoCommitSpinner(heightSpinnerModel);
heightSpinnerModel.setValue(controller.getHeight());
heightSpinnerModel.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent ev) {
controller.setHeight(((Number)heightSpinnerModel.getValue()).intValue());
}
});
controller.addPropertyChangeListener(AbstractPhotoController.Property.HEIGHT,
new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
heightSpinnerModel.setValue(controller.getHeight());
}
});
// Create apply proportions check box bound to ASPECT_RATIO controller property
boolean notFreeAspectRatio = controller.getAspectRatio() != AspectRatio.FREE_RATIO;
this.applyProportionsCheckBox = new JCheckBox();
this.applyProportionsCheckBox.setSelected(notFreeAspectRatio);
this.applyProportionsCheckBox.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent ev) {
controller.setAspectRatio(applyProportionsCheckBox.isSelected()
? (AspectRatio)aspectRatioComboBox.getSelectedItem()
: AspectRatio.FREE_RATIO);
}
});
this.aspectRatioComboBox = new JComboBox(new Object [] {
AspectRatio.VIEW_3D_RATIO,
AspectRatio.SQUARE_RATIO,
AspectRatio.RATIO_4_3,
AspectRatio.RATIO_3_2,
AspectRatio.RATIO_16_9,
AspectRatio.RATIO_2_1});
this.aspectRatioComboBox.setRenderer(new DefaultListCellRenderer() {
@Override
public Component getListCellRendererComponent(JList list, Object value,
int index, boolean isSelected, boolean cellHasFocus) {
AspectRatio aspectRatio = (AspectRatio)value;
String displayedValue = "";
if (aspectRatio != AspectRatio.FREE_RATIO) {
switch (aspectRatio) {
case VIEW_3D_RATIO :
displayedValue = preferences.getLocalizedString(
PhotoSizeAndQualityPanel.class, "aspectRatioComboBox.view3DRatio.text");
break;
case SQUARE_RATIO :
displayedValue = preferences.getLocalizedString(
PhotoSizeAndQualityPanel.class, "aspectRatioComboBox.squareRatio.text");
break;
case RATIO_4_3 :
displayedValue = "4/3";
break;
case RATIO_3_2 :
displayedValue = "3/2";
break;
case RATIO_16_9 :
displayedValue = "16/9";
break;
case RATIO_2_1 :
displayedValue = "2/1";
break;
}
}
return super.getListCellRendererComponent(list, displayedValue, index, isSelected,
cellHasFocus);
}
});
this.aspectRatioComboBox.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent ev) {
controller.setAspectRatio((AspectRatio)aspectRatioComboBox.getSelectedItem());
}
});
this.aspectRatioComboBox.setEnabled(notFreeAspectRatio);
this.aspectRatioComboBox.setSelectedItem(controller.getAspectRatio());
controller.addPropertyChangeListener(AbstractPhotoController.Property.ASPECT_RATIO,
new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
boolean notFreeAspectRatio = controller.getAspectRatio() != AspectRatio.FREE_RATIO;
applyProportionsCheckBox.setSelected(notFreeAspectRatio);
aspectRatioComboBox.setEnabled(notFreeAspectRatio);
aspectRatioComboBox.setSelectedItem(controller.getAspectRatio());
}
});
// Quality label and slider bound to QUALITY controller property
this.qualityLabel = new JLabel();
this.fastQualityLabel = new JLabel();
this.bestQualityLabel = new JLabel();
this.qualitySlider = new JSlider(1, controller.getQualityLevelCount()) {
@Override
public String getToolTipText(MouseEvent ev) {
float valueUnderMouse = getSliderValueAt(this, ev.getX(), preferences);
float valueToTick = valueUnderMouse - (float)Math.floor(valueUnderMouse);
if (valueToTick < 0.25f || valueToTick > 0.75f) {
// Display a tooltip that explains the different quality levels
return "<html><table><tr valign='middle'>"
+ "<td><img border='1' src='"
+ new ResourceURLContent(PhotoSizeAndQualityPanel.class, "resources/quality" + Math.round(valueUnderMouse - qualitySlider.getMinimum()) + ".jpg").getURL() + "'></td>"
+ "<td>" + preferences.getLocalizedString(PhotoSizeAndQualityPanel.class, "quality" + Math.round(valueUnderMouse - qualitySlider.getMinimum()) + "DescriptionLabel.text") + "</td>"
+ "</tr></table>";
} else {
return null;
}
}
};
// Add a listener that displays also the tool tip when user clicks on the slider
this.qualitySlider.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(final MouseEvent ev) {
EventQueue.invokeLater(new Runnable() {
public void run() {
float valueUnderMouse = getSliderValueAt(qualitySlider, ev.getX(), preferences);
if (qualitySlider.getValue() == Math.round(valueUnderMouse)) {
ToolTipManager toolTipManager = ToolTipManager.sharedInstance();
int initialDelay = toolTipManager.getInitialDelay();
toolTipManager.setInitialDelay(Math.min(initialDelay, 150));
toolTipManager.mouseMoved(ev);
toolTipManager.setInitialDelay(initialDelay);
}
}
});
}
});
this.qualitySlider.setPaintTicks(true);
this.qualitySlider.setMajorTickSpacing(1);
this.qualitySlider.setSnapToTicks(true);
final boolean offScreenImageSupported = Component3DManager.getInstance().isOffScreenImageSupported();
this.qualitySlider.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent ev) {
if (!offScreenImageSupported) {
// Can't support 2 first quality levels if offscreen image isn't supported
qualitySlider.setValue(Math.max(qualitySlider.getMinimum() + 2, qualitySlider.getValue()));
}
controller.setQuality(qualitySlider.getValue() - qualitySlider.getMinimum());
}
});
controller.addPropertyChangeListener(AbstractPhotoController.Property.QUALITY,
new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
qualitySlider.setValue(qualitySlider.getMinimum() + controller.getQuality());
}
});
this.qualitySlider.setValue(this.qualitySlider.getMinimum() + controller.getQuality());
// Listener on 3D view notified when its size changes
final JComponent view3D = (JComponent)controller.get3DView();
final ComponentAdapter view3DSizeListener = new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent ev) {
controller.set3DViewAspectRatio((float)view3D.getWidth() / view3D.getHeight());
}
};
addAncestorListener(new AncestorListener() {
public void ancestorAdded(AncestorEvent ev) {
view3D.addComponentListener(view3DSizeListener);
ToolTipManager.sharedInstance().registerComponent(qualitySlider);
}
public void ancestorRemoved(AncestorEvent ev) {
ToolTipManager.sharedInstance().unregisterComponent(qualitySlider);
view3D.removeComponentListener(view3DSizeListener);
}
public void ancestorMoved(AncestorEvent ev) {
}
});
setComponentTexts(preferences);
}
/**
* Returns the slider value matching a given x.
*/
private float getSliderValueAt(JSlider qualitySlider, int x, UserPreferences preferences) {
int fastLabelOffset = 0;
int bestLabelOffset = 0;
int sliderWidth = qualitySlider.getWidth() - fastLabelOffset - bestLabelOffset;
return qualitySlider.getMinimum()
+ (float)(x - (qualitySlider.getComponentOrientation().isLeftToRight()
? fastLabelOffset
: bestLabelOffset))
/ sliderWidth * (qualitySlider.getMaximum() - qualitySlider.getMinimum());
}
/**
* Sets the texts of the components.
*/
private void setComponentTexts(UserPreferences preferences) {
this.widthLabel.setText(SwingTools.getLocalizedLabelText(preferences,
PhotoSizeAndQualityPanel.class, "widthLabel.text"));
this.heightLabel.setText(SwingTools.getLocalizedLabelText(preferences,
PhotoSizeAndQualityPanel.class, "heightLabel.text"));
this.applyProportionsCheckBox.setText(SwingTools.getLocalizedLabelText(preferences,
PhotoSizeAndQualityPanel.class, "applyProportionsCheckBox.text"));
this.qualityLabel.setText(SwingTools.getLocalizedLabelText(preferences,
PhotoSizeAndQualityPanel.class, "qualityLabel.text"));
this.fastQualityLabel.setText(SwingTools.getLocalizedLabelText(preferences,
PhotoSizeAndQualityPanel.class, "fastLabel.text"));
if (!Component3DManager.getInstance().isOffScreenImageSupported()) {
this.fastQualityLabel.setEnabled(false);
}
this.bestQualityLabel.setText(SwingTools.getLocalizedLabelText(preferences,
PhotoSizeAndQualityPanel.class, "bestLabel.text"));
}
/**
* Sets components mnemonics and label / component associations.
*/
private void setMnemonics(UserPreferences preferences) {
if (!OperatingSystem.isMacOSX()) {
this.widthLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
PhotoSizeAndQualityPanel.class, "widthLabel.mnemonic")).getKeyCode());
this.widthLabel.setLabelFor(this.widthSpinner);
this.heightLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
PhotoSizeAndQualityPanel.class, "heightLabel.mnemonic")).getKeyCode());
this.heightLabel.setLabelFor(this.heightSpinner);
this.applyProportionsCheckBox.setMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
PhotoSizeAndQualityPanel.class, "applyProportionsCheckBox.mnemonic")).getKeyCode());
this.qualityLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
PhotoSizeAndQualityPanel.class, "qualityLabel.mnemonic")).getKeyCode());
this.qualityLabel.setLabelFor(this.qualitySlider);
}
}
/**
* Preferences property listener bound to this panel with a weak reference to avoid
* strong link between user preferences and this panel.
*/
public static class LanguageChangeListener implements PropertyChangeListener {
private final WeakReference<PhotoSizeAndQualityPanel> photoPanel;
public LanguageChangeListener(PhotoSizeAndQualityPanel photoPanel) {
this.photoPanel = new WeakReference<PhotoSizeAndQualityPanel>(photoPanel);
}
public void propertyChange(PropertyChangeEvent ev) {
// If photo panel was garbage collected, remove this listener from preferences
PhotoSizeAndQualityPanel photoPanel = this.photoPanel.get();
UserPreferences preferences = (UserPreferences)ev.getSource();
if (photoPanel == null) {
preferences.removePropertyChangeListener(UserPreferences.Property.LANGUAGE, this);
} else {
photoPanel.setComponentOrientation(ComponentOrientation.getOrientation(Locale.getDefault()));
photoPanel.setComponentTexts(preferences);
photoPanel.setMnemonics(preferences);
}
}
}
/**
* Layouts panel components in panel with their labels.
*/
private void layoutComponents() {
int labelAlignment = OperatingSystem.isMacOSX()
? JLabel.TRAILING
: JLabel.LEADING;
// First row
Insets labelInsets = new Insets(0, 0, 0, 5);
add(this.widthLabel, new GridBagConstraints(
0, 0, 1, 1, 0, 0, GridBagConstraints.CENTER,
GridBagConstraints.HORIZONTAL, labelInsets, 0, 0));
// Use HORIZONTAL fill constraint with label alignment to ensure
// label is correctly sized in small dialogs
this.widthLabel.setHorizontalAlignment(labelAlignment);
Insets componentInsets = new Insets(0, 0, 0, 10);
add(this.widthSpinner, new GridBagConstraints(
1, 0, 1, 1, 0, 0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, componentInsets, 0, 0));
add(this.heightLabel, new GridBagConstraints(
2, 0, 1, 1, 0, 0, GridBagConstraints.CENTER,
GridBagConstraints.HORIZONTAL, labelInsets, 0, 0));
this.heightLabel.setHorizontalAlignment(labelAlignment);
add(this.heightSpinner, new GridBagConstraints(
3, 0, 1, 1, 0, 0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
// Second row
JPanel proportionsPanel = new JPanel();
proportionsPanel.add(this.applyProportionsCheckBox);
proportionsPanel.add(this.aspectRatioComboBox);
add(proportionsPanel, new GridBagConstraints(
0, 1, 4, 1, 0, 0, GridBagConstraints.CENTER,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
// Third row
add(this.qualityLabel, new GridBagConstraints(
0, 3, 1, 1, 0, 0, GridBagConstraints.CENTER,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 5), 0, 0));
this.qualityLabel.setHorizontalAlignment(labelAlignment);
add(this.qualitySlider, new GridBagConstraints(
1, 3, 3, 1, 0, 0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
// Fourth row
JPanel qualityLabelsPanel = new JPanel(new BorderLayout(20, 0));
qualityLabelsPanel.add(this.fastQualityLabel, BorderLayout.WEST);
qualityLabelsPanel.add(this.bestQualityLabel, BorderLayout.EAST);
add(qualityLabelsPanel, new GridBagConstraints(
1, 4, 3, 1, 0, 0, GridBagConstraints.CENTER,
GridBagConstraints.HORIZONTAL, new Insets(OperatingSystem.isWindows() ? 0 : -3, 0, 0, 0), 0, 0));
}
/**
* Enables or disables this panel and its components.
*/
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
this.widthSpinner.setEnabled(enabled);
this.heightSpinner.setEnabled(enabled);
this.applyProportionsCheckBox.setEnabled(enabled);
this.aspectRatioComboBox.setEnabled(enabled);
this.qualitySlider.setEnabled(enabled);
}
/**
* Enables or disables components that allow to force proportions.
*/
public void setProportionsChoiceEnabled(boolean enabled) {
this.applyProportionsCheckBox.setEnabled(enabled);
this.aspectRatioComboBox.setEnabled(enabled && this.applyProportionsCheckBox.isSelected());
}
}