/*
* LevelPanel.java 27 oct 2011
*
* Sweet Home 3D, Copyright (c) 2011 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.Component;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.DefaultListSelectionModel;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JSpinner;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumnModel;
import com.eteks.sweethome3d.model.Level;
import com.eteks.sweethome3d.model.UserPreferences;
import com.eteks.sweethome3d.tools.OperatingSystem;
import com.eteks.sweethome3d.viewcontroller.DialogView;
import com.eteks.sweethome3d.viewcontroller.LevelController;
import com.eteks.sweethome3d.viewcontroller.View;
/**
* Level editing panel.
* @author Emmanuel Puybaret
*/
public class LevelPanel extends JPanel implements DialogView {
private final LevelController controller;
private JLabel nameLabel;
private JTextField nameTextField;
private JLabel elevationLabel;
private JSpinner elevationSpinner;
private JLabel floorThicknessLabel;
private JSpinner floorThicknessSpinner;
private JLabel heightLabel;
private JSpinner heightSpinner;
private JLabel levelsSummaryLabel;
private JTable levelsSummaryTable;
private String dialogTitle;
/**
* Creates a panel that displays home levels data according to the units
* set in <code>preferences</code>.
* @param preferences user preferences
* @param controller the controller of this panel
*/
public LevelPanel(UserPreferences preferences,
LevelController controller) {
super(new GridBagLayout());
this.controller = controller;
createComponents(preferences, controller);
setMnemonics(preferences);
layoutComponents(preferences, controller);
}
/**
* Creates and initializes components and spinners model.
*/
private void createComponents(final UserPreferences preferences,
final LevelController controller) {
// Get unit name matching current unit
String unitName = preferences.getLengthUnit().getName();
if (controller.isPropertyEditable(LevelController.Property.NAME)) {
// Create name label and its text field bound to NAME controller property
this.nameLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences, LevelPanel.class, "nameLabel.text"));
this.nameTextField = new AutoCompleteTextField(controller.getName(), 15, preferences.getAutoCompletionStrings("LevelName"));
if (!OperatingSystem.isMacOSXLeopardOrSuperior()) {
SwingTools.addAutoSelectionOnFocusGain(this.nameTextField);
}
final PropertyChangeListener nameChangeListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
nameTextField.setText(controller.getName());
}
};
controller.addPropertyChangeListener(LevelController.Property.NAME, nameChangeListener);
this.nameTextField.getDocument().addDocumentListener(new DocumentListener() {
public void changedUpdate(DocumentEvent ev) {
controller.removePropertyChangeListener(LevelController.Property.NAME, nameChangeListener);
String name = nameTextField.getText();
if (name == null || name.trim().length() == 0) {
controller.setName(null);
} else {
controller.setName(name);
}
controller.addPropertyChangeListener(LevelController.Property.NAME, nameChangeListener);
}
public void insertUpdate(DocumentEvent ev) {
changedUpdate(ev);
}
public void removeUpdate(DocumentEvent ev) {
changedUpdate(ev);
}
});
}
final float maximumLength = preferences.getLengthUnit().getMaximumLength();
if (controller.isPropertyEditable(LevelController.Property.ELEVATION)) {
// Create elevation label and its spinner bound to ELEVATION controller property
this.elevationLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences,
LevelPanel.class, "elevationLabel.text", unitName));
final NullableSpinner.NullableSpinnerLengthModel elevationSpinnerModel =
new NullableSpinner.NullableSpinnerLengthModel(preferences, -1000f, preferences.getLengthUnit().getMaximumElevation());
this.elevationSpinner = new NullableSpinner(elevationSpinnerModel);
elevationSpinnerModel.setNullable(controller.getElevation() == null);
elevationSpinnerModel.setLength(controller.getElevation());
final PropertyChangeListener elevationChangeListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
elevationSpinnerModel.setNullable(ev.getNewValue() == null);
elevationSpinnerModel.setLength((Float)ev.getNewValue());
}
};
controller.addPropertyChangeListener(LevelController.Property.ELEVATION,
elevationChangeListener);
elevationSpinnerModel.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent ev) {
controller.removePropertyChangeListener(LevelController.Property.ELEVATION,
elevationChangeListener);
controller.setElevation(elevationSpinnerModel.getLength());
controller.addPropertyChangeListener(LevelController.Property.ELEVATION,
elevationChangeListener);
}
});
}
final float minimumLength = preferences.getLengthUnit().getMinimumLength();
if (controller.isPropertyEditable(LevelController.Property.FLOOR_THICKNESS)) {
// Create floor thickness label and its spinner bound to FLOOR_THICKNESS controller property
this.floorThicknessLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences,
LevelPanel.class, "floorThicknessLabel.text", unitName));
final NullableSpinner.NullableSpinnerLengthModel floorThicknessSpinnerModel =
new NullableSpinner.NullableSpinnerLengthModel(preferences, minimumLength, maximumLength / 10);
this.floorThicknessSpinner = new NullableSpinner(floorThicknessSpinnerModel);
final PropertyChangeListener floorThicknessChangeListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
Float floorThickness = controller.getFloorThickness();
floorThicknessSpinnerModel.setNullable(floorThickness == null);
floorThicknessSpinnerModel.setLength(floorThickness);
}
};
floorThicknessChangeListener.propertyChange(null);
controller.addPropertyChangeListener(LevelController.Property.FLOOR_THICKNESS, floorThicknessChangeListener);
floorThicknessSpinnerModel.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent ev) {
controller.removePropertyChangeListener(LevelController.Property.FLOOR_THICKNESS, floorThicknessChangeListener);
controller.setFloorThickness(floorThicknessSpinnerModel.getLength());
controller.addPropertyChangeListener(LevelController.Property.FLOOR_THICKNESS, floorThicknessChangeListener);
}
});
}
if (controller.isPropertyEditable(LevelController.Property.HEIGHT)) {
// Create floor thickness label and its spinner bound to HEIGHT controller property
this.heightLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences,
LevelPanel.class, "heightLabel.text", unitName));
final NullableSpinner.NullableSpinnerLengthModel heightSpinnerModel =
new NullableSpinner.NullableSpinnerLengthModel(preferences, minimumLength, maximumLength);
this.heightSpinner = new NullableSpinner(heightSpinnerModel);
final PropertyChangeListener heightChangeListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
Float height = controller.getHeight();
heightSpinnerModel.setNullable(height == null);
heightSpinnerModel.setLength(height);
}
};
heightChangeListener.propertyChange(null);
controller.addPropertyChangeListener(LevelController.Property.HEIGHT, heightChangeListener);
heightSpinnerModel.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent ev) {
controller.removePropertyChangeListener(LevelController.Property.HEIGHT, heightChangeListener);
controller.setHeight(heightSpinnerModel.getLength());
controller.addPropertyChangeListener(LevelController.Property.HEIGHT, heightChangeListener);
}
});
}
this.levelsSummaryLabel = new JLabel(SwingTools.getLocalizedLabelText(preferences,
LevelPanel.class, "levelsSummaryLabel.text", unitName));
final String [] columnNames = {
SwingTools.getLocalizedLabelText(preferences, LevelPanel.class, "nameColumn"),
SwingTools.getLocalizedLabelText(preferences, LevelPanel.class, "elevationColumn"),
SwingTools.getLocalizedLabelText(preferences, LevelPanel.class, "floorThicknessColumn"),
SwingTools.getLocalizedLabelText(preferences, LevelPanel.class, "heightColumn")};
this.levelsSummaryTable = new JTable(new LevelsTableModel(controller.getLevels(), columnNames));
// Display lengths according to the current length unit
TableColumnModel columnModel = this.levelsSummaryTable.getColumnModel();
TableCellRenderer lengthRenderer = new DefaultTableCellRenderer() {
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus,
int row, int column) {
if (value != null) {
value = preferences.getLengthUnit().getFormat().format((Float)value);
setHorizontalAlignment(JLabel.RIGHT);
}
return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
}
};
columnModel.getColumn(1).setCellRenderer(lengthRenderer);
columnModel.getColumn(2).setCellRenderer(lengthRenderer);
columnModel.getColumn(3).setCellRenderer(lengthRenderer);
// Ensure only selected level is selected in the table
this.levelsSummaryTable.setSelectionModel(new DefaultListSelectionModel() {
public void setSelectionInterval(int index0, int index1) {
}
public void setLeadSelectionIndex(int index) {
}
public void setAnchorSelectionIndex(int index) {
}
public boolean isSelectionEmpty() {
return controller.getSelectedLevelIndex() != null;
}
public boolean isSelectedIndex(int index) {
return controller.getSelectedLevelIndex() == index;
}
public void insertIndexInterval(int index, int length, boolean before) {
}
});
this.levelsSummaryTable.setFocusable(false);
this.levelsSummaryTable.addAncestorListener(new AncestorListener() {
public void ancestorAdded(AncestorEvent ev) {
// Ensure selected row is visible when table is displayed
Integer selectedRow = controller.getSelectedLevelIndex();
if (selectedRow != null) {
levelsSummaryTable.scrollRectToVisible(levelsSummaryTable.getCellRect(selectedRow, 0, true));
}
ev.getComponent().removeAncestorListener(this);
}
public void ancestorRemoved(AncestorEvent ev) {
}
public void ancestorMoved(AncestorEvent ev) {
}
});
this.dialogTitle = preferences.getLocalizedString(LevelPanel.class, "level.title");
}
/**
* Sets components mnemonics and label / component associations.
*/
private void setMnemonics(UserPreferences preferences) {
if (!OperatingSystem.isMacOSX()) {
if (this.nameLabel != null) {
this.nameLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
LevelPanel.class, "nameLabel.mnemonic")).getKeyCode());
this.nameLabel.setLabelFor(this.nameTextField);
}
if (this.elevationLabel != null) {
this.elevationLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
LevelPanel.class, "elevationLabel.mnemonic")).getKeyCode());
this.elevationLabel.setLabelFor(this.elevationSpinner);
}
if (this.floorThicknessLabel != null) {
this.floorThicknessLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
LevelPanel.class, "floorThicknessLabel.mnemonic")).getKeyCode());
this.floorThicknessLabel.setLabelFor(this.floorThicknessSpinner);
}
if (this.heightLabel != null) {
this.heightLabel.setDisplayedMnemonic(KeyStroke.getKeyStroke(preferences.getLocalizedString(
LevelPanel.class, "heightLabel.mnemonic")).getKeyCode());
this.heightLabel.setLabelFor(this.heightSpinner);
}
}
}
/**
* Layouts panel components in panel with their labels.
*/
private void layoutComponents(UserPreferences preferences,
final LevelController controller) {
int labelAlignment = OperatingSystem.isMacOSX()
? GridBagConstraints.LINE_END
: GridBagConstraints.LINE_START;
Insets labelInsets = new Insets(0, 0, 5, 5);
Insets rightComponentInsets = new Insets(0, 0, 5, 0);
if (this.nameLabel != null) {
// First row
add(this.nameLabel, new GridBagConstraints(
0, 0, 1, 1, 0, 0, labelAlignment,
GridBagConstraints.NONE, labelInsets, 0, 0));
add(this.nameTextField, new GridBagConstraints(
1, 0, 2, 1, 0, 0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, rightComponentInsets, 0, 0));
}
if (this.elevationLabel != null) {
// Second row
add(this.elevationLabel, new GridBagConstraints(
0, 1, 1, 1, 0, 0, labelAlignment,
GridBagConstraints.NONE, labelInsets, 0, 0));
add(this.elevationSpinner, new GridBagConstraints(
1, 1, 1, 1, 0.1, 0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, rightComponentInsets, -15, 0));
add(new JLabel(), new GridBagConstraints(
2, 1, 1, 1, 0.2, 0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, rightComponentInsets, 0, 0));
}
if (this.floorThicknessLabel != null) {
// Third row
add(this.floorThicknessLabel, new GridBagConstraints(
0, 2, 1, 1, 0, 0, labelAlignment,
GridBagConstraints.NONE, labelInsets, 0, 0));
add(this.floorThicknessSpinner, new GridBagConstraints(
1, 2, 1, 1, 0.1, 0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, rightComponentInsets, -15, 0));
add(new JLabel(), new GridBagConstraints(
2, 3, 1, 1, 0.2, 0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, rightComponentInsets, 0, 0));
}
if (this.heightLabel != null) {
// Last row
add(this.heightLabel, new GridBagConstraints(
0, 3, 1, 1, 0, 0, labelAlignment,
GridBagConstraints.NONE, new Insets(0, 0, 0, 5), 0, 0));
add(this.heightSpinner, new GridBagConstraints(
1, 3, 1, 1, 0.1, 0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), -15, 0));
add(new JLabel(), new GridBagConstraints(
2, 3, 1, 1, 0.2, 0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, rightComponentInsets, 0, 0));
}
add(new JSeparator(), new GridBagConstraints(
0, 4, 3, 1, 0, 0, GridBagConstraints.LINE_START,
GridBagConstraints.HORIZONTAL, new Insets(5, 0, 5, 0), 0, 0));
add(this.levelsSummaryLabel, new GridBagConstraints(
0, 5, 3, 1, 0, 0, GridBagConstraints.LINE_START,
GridBagConstraints.NONE, new Insets(0, 0, 5, 0), 0, 0));
JScrollPane levelsSummaryPane = new JScrollPane(this.levelsSummaryTable);
levelsSummaryPane.setPreferredSize(new Dimension(320, 150));
add(levelsSummaryPane, new GridBagConstraints(
0, 6, 4, 1, 1, 1, GridBagConstraints.CENTER,
GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
}
/**
* Displays this panel in a modal dialog box.
*/
public void displayView(View parentView) {
if (SwingTools.showConfirmDialog((JComponent)parentView,
this, this.dialogTitle, this.nameTextField) == JOptionPane.OK_OPTION) {
this.controller.modifyLevels();
}
}
/**
* The model of the table used to show levels information.
*/
private static final class LevelsTableModel extends AbstractTableModel {
private final Level [] levels;
private final String [] columnNames;
private LevelsTableModel(Level [] levels, String [] columnNames) {
this.levels = levels;
this.columnNames = columnNames;
}
public int getRowCount() {
return this.levels.length;
}
public int getColumnCount() {
return 4;
}
public Object getValueAt(int rowIndex, int columnIndex) {
Level level = this.levels [rowIndex];
switch (columnIndex) {
case 0 : return level.getName();
case 1 : return level.getElevation();
case 2 :
for ( ; rowIndex > 0; rowIndex--) {
if (this.levels [rowIndex - 1].getElevation() < level.getElevation()) {
break;
}
}
if (rowIndex == 0) {
return null; // Don't display floor thickness of first levels
} else {
return level.getFloorThickness();
}
case 3 : return level.getHeight();
}
return null;
}
@Override
public String getColumnName(int column) {
return this.columnNames [column];
}
}
}