/*
* MultipleLevelsPlanPanel.java 23 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.BorderLayout;
import java.awt.CardLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Point;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.WeakReference;
import java.util.List;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JViewport;
import javax.swing.SwingUtilities;
import javax.swing.TransferHandler;
import javax.swing.UIManager;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import com.eteks.sweethome3d.model.CollectionEvent;
import com.eteks.sweethome3d.model.CollectionListener;
import com.eteks.sweethome3d.model.DimensionLine;
import com.eteks.sweethome3d.model.Home;
import com.eteks.sweethome3d.model.Level;
import com.eteks.sweethome3d.model.Selectable;
import com.eteks.sweethome3d.model.TextStyle;
import com.eteks.sweethome3d.model.UserPreferences;
import com.eteks.sweethome3d.tools.OperatingSystem;
import com.eteks.sweethome3d.viewcontroller.PlanController;
import com.eteks.sweethome3d.viewcontroller.PlanController.EditableProperty;
import com.eteks.sweethome3d.viewcontroller.PlanView;
import com.eteks.sweethome3d.viewcontroller.View;
/**
* A panel for multiple levels plans where users can select the displayed level.
* @author Emmanuel Puybaret
*/
public class MultipleLevelsPlanPanel extends JPanel implements PlanView, Printable {
private static final String ONE_LEVEL_PANEL_NAME = "oneLevelPanel";
private static final String MULTIPLE_LEVELS_PANEL_NAME = "multipleLevelsPanel";
private PlanComponent planComponent;
private JScrollPane planScrollPane;
private JTabbedPane multipleLevelsTabbedPane;
private JPanel oneLevelPanel;
public MultipleLevelsPlanPanel(Home home,
UserPreferences preferences,
PlanController controller) {
super(new CardLayout());
createComponents(home, preferences, controller);
layoutComponents();
updateSelectedTab(home);
}
/**
* Creates components displayed by this panel.
*/
private void createComponents(final Home home,
final UserPreferences preferences, final PlanController controller) {
this.planComponent = createPlanComponent(home, preferences, controller);
UIManager.getDefaults().put("TabbedPane.contentBorderInsets", OperatingSystem.isMacOSX()
? new Insets(2, 2, 2, 2)
: new Insets(-1, 0, 2, 2));
this.multipleLevelsTabbedPane = new JTabbedPane();
if (OperatingSystem.isMacOSX()) {
this.multipleLevelsTabbedPane.setBorder(new EmptyBorder(-2, -6, -7, -6));
}
List<Level> levels = home.getLevels();
this.planScrollPane = new JScrollPane(this.planComponent);
this.planScrollPane.setMinimumSize(new Dimension());
if (OperatingSystem.isMacOSX()) {
this.planScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
this.planScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
}
createTabs(home, preferences);
final ChangeListener changeListener = new ChangeListener() {
public void stateChanged(ChangeEvent ev) {
Component selectedComponent = multipleLevelsTabbedPane.getSelectedComponent();
if (selectedComponent instanceof LevelLabel) {
controller.setSelectedLevel(((LevelLabel)selectedComponent).getLevel());
}
}
};
this.multipleLevelsTabbedPane.addChangeListener(changeListener);
// Add a mouse listener that will give focus to plan component only if a change in tabbed pane comes from the mouse
// and will add a level only if user clicks on the last tab
this.multipleLevelsTabbedPane.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent ev) {
int indexAtLocation = multipleLevelsTabbedPane.indexAtLocation(ev.getX(), ev.getY());
if (ev.getClickCount() == 1) {
if (indexAtLocation == multipleLevelsTabbedPane.getTabCount() - 1) {
controller.addLevel();
}
final Level oldSelectedLevel = home.getSelectedLevel();
EventQueue.invokeLater(new Runnable() {
public void run() {
if (oldSelectedLevel == home.getSelectedLevel()) {
planComponent.requestFocusInWindow();
}
}
});
} else if (indexAtLocation != -1) {
if (multipleLevelsTabbedPane.getSelectedIndex() == multipleLevelsTabbedPane.getTabCount() - 1) {
// May happen with a row of tabs is full
multipleLevelsTabbedPane.setSelectedIndex(multipleLevelsTabbedPane.getTabCount() - 2);
}
controller.modifySelectedLevel();
}
}
});
// Add listeners to levels to maintain tabs name and order
final PropertyChangeListener levelChangeListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
if (Level.Property.NAME.name().equals(ev.getPropertyName())) {
multipleLevelsTabbedPane.setTitleAt(home.getLevels().indexOf(ev.getSource()), (String)ev.getNewValue());
} else if (Level.Property.ELEVATION.name().equals(ev.getPropertyName())
|| Level.Property.HEIGHT.name().equals(ev.getPropertyName())) {
multipleLevelsTabbedPane.removeChangeListener(changeListener);
multipleLevelsTabbedPane.removeAll();
createTabs(home, preferences);
updateSelectedTab(home);
multipleLevelsTabbedPane.addChangeListener(changeListener);
}
}
};
for (Level level : levels) {
level.addPropertyChangeListener(levelChangeListener);
}
home.addLevelsListener(new CollectionListener<Level>() {
public void collectionChanged(CollectionEvent<Level> ev) {
multipleLevelsTabbedPane.removeChangeListener(changeListener);
switch (ev.getType()) {
case ADD:
multipleLevelsTabbedPane.insertTab(ev.getItem().getName(), null, new LevelLabel(ev.getItem()), null, ev.getIndex());
ev.getItem().addPropertyChangeListener(levelChangeListener);
break;
case DELETE:
ev.getItem().removePropertyChangeListener(levelChangeListener);
multipleLevelsTabbedPane.remove(ev.getIndex());
break;
}
updateLayout(home);
multipleLevelsTabbedPane.addChangeListener(changeListener);
}
});
home.addPropertyChangeListener(Home.Property.SELECTED_LEVEL, new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent ev) {
multipleLevelsTabbedPane.removeChangeListener(changeListener);
updateSelectedTab(home);
multipleLevelsTabbedPane.addChangeListener(changeListener);
}
});
this.oneLevelPanel = new JPanel(new BorderLayout());
preferences.addPropertyChangeListener(UserPreferences.Property.LANGUAGE,
new LanguageChangeListener(this));
}
/**
* Creates and returns the main plan component displayed and layout by this component.
*/
protected PlanComponent createPlanComponent(final Home home, final UserPreferences preferences,
final PlanController controller) {
return new PlanComponent(home, preferences, controller);
}
/**
* Preferences property listener bound to this component with a weak reference to avoid
* strong link between preferences and this component.
*/
private static class LanguageChangeListener implements PropertyChangeListener {
private WeakReference<MultipleLevelsPlanPanel> planPanel;
public LanguageChangeListener(MultipleLevelsPlanPanel planPanel) {
this.planPanel = new WeakReference<MultipleLevelsPlanPanel>(planPanel);
}
public void propertyChange(PropertyChangeEvent ev) {
// If help pane was garbage collected, remove this listener from preferences
MultipleLevelsPlanPanel planPanel = this.planPanel.get();
UserPreferences preferences = (UserPreferences)ev.getSource();
if (planPanel == null) {
preferences.removePropertyChangeListener(UserPreferences.Property.LANGUAGE, this);
} else {
// Update create level tooltip in new locale
String createNewLevelTooltip = preferences.getLocalizedString(MultipleLevelsPlanPanel.class, "ADD_LEVEL.ShortDescription");
planPanel.multipleLevelsTabbedPane.setToolTipTextAt(planPanel.multipleLevelsTabbedPane.getTabCount() - 1, createNewLevelTooltip);
}
}
}
/**
* Creates the tabs from <code>home</code> levels.
*/
private void createTabs(Home home, UserPreferences preferences) {
for (Level level : home.getLevels()) {
this.multipleLevelsTabbedPane.addTab(level.getName(), new LevelLabel(level));
}
String createNewLevelIcon = preferences.getLocalizedString(MultipleLevelsPlanPanel.class, "ADD_LEVEL.SmallIcon");
String createNewLevelTooltip = preferences.getLocalizedString(MultipleLevelsPlanPanel.class, "ADD_LEVEL.ShortDescription");
ImageIcon newLevelIcon = new ImageIcon(MultipleLevelsPlanPanel.class.getResource(createNewLevelIcon));
this.multipleLevelsTabbedPane.addTab("", newLevelIcon, new JLabel(), createNewLevelTooltip);
// Disable last tab to avoid user stops on it
this.multipleLevelsTabbedPane.setEnabledAt(this.multipleLevelsTabbedPane.getTabCount() - 1, false);
this.multipleLevelsTabbedPane.setDisabledIconAt(this.multipleLevelsTabbedPane.getTabCount() - 1, newLevelIcon);
}
/**
* Selects the tab matching the selected level in <code>home</code>.
*/
private void updateSelectedTab(Home home) {
List<Level> levels = home.getLevels();
Level selectedLevel = home.getSelectedLevel();
if (levels.size() >= 2 && selectedLevel != null) {
this.multipleLevelsTabbedPane.setSelectedIndex(levels.indexOf(selectedLevel));
displayPlanComponentAtSelectedIndex(home);
}
updateLayout(home);
}
/**
* Display the plan component at the selected tab index.
*/
private void displayPlanComponentAtSelectedIndex(Home home) {
int planIndex = this.multipleLevelsTabbedPane.indexOfComponent(this.planScrollPane);
if (planIndex != -1) {
// Replace plan component by a dummy label to avoid losing tab
this.multipleLevelsTabbedPane.setComponentAt(planIndex, new LevelLabel(home.getLevels().get(planIndex)));
}
this.multipleLevelsTabbedPane.setComponentAt(this.multipleLevelsTabbedPane.getSelectedIndex(), this.planScrollPane);
}
/**
* Switches between a simple plan component view and a tabbed pane for multiple levels.
*/
private void updateLayout(Home home) {
CardLayout layout = (CardLayout)getLayout();
List<Level> levels = home.getLevels();
boolean focus = this.planComponent.hasFocus();
if (levels.size() < 2 || home.getSelectedLevel() == null) {
int planIndex = this.multipleLevelsTabbedPane.indexOfComponent(this.planScrollPane);
if (planIndex != -1) {
// Replace plan component by a dummy label to avoid losing tab
this.multipleLevelsTabbedPane.setComponentAt(planIndex, new LevelLabel(home.getLevels().get(planIndex)));
}
this.oneLevelPanel.add(this.planScrollPane);
layout.show(this, ONE_LEVEL_PANEL_NAME);
} else {
layout.show(this, MULTIPLE_LEVELS_PANEL_NAME);
}
if (focus) {
this.planComponent.requestFocusInWindow();
}
}
/**
* Layouts the components displayed by this panel.
*/
private void layoutComponents() {
add(this.multipleLevelsTabbedPane, MULTIPLE_LEVELS_PANEL_NAME);
add(this.oneLevelPanel, ONE_LEVEL_PANEL_NAME);
SwingTools.installFocusBorder(this.planComponent);
setFocusTraversalPolicyProvider(false);
setMinimumSize(new Dimension());
}
@Override
public void setTransferHandler(TransferHandler newHandler) {
this.planComponent.setTransferHandler(newHandler);
}
@Override
public void setComponentPopupMenu(JPopupMenu popup) {
this.planComponent.setComponentPopupMenu(popup);
}
@Override
public void addMouseMotionListener(final MouseMotionListener l) {
this.planComponent.addMouseMotionListener(new MouseMotionListener() {
public void mouseMoved(MouseEvent ev) {
l.mouseMoved(SwingUtilities.convertMouseEvent(planComponent, ev, MultipleLevelsPlanPanel.this));
}
public void mouseDragged(MouseEvent ev) {
l.mouseDragged(SwingUtilities.convertMouseEvent(planComponent, ev, MultipleLevelsPlanPanel.this));
}
});
}
@Override
public void addMouseListener(final MouseListener l) {
this.planComponent.addMouseListener(new MouseListener() {
public void mouseReleased(MouseEvent ev) {
l.mouseReleased(SwingUtilities.convertMouseEvent(planComponent, ev, MultipleLevelsPlanPanel.this));
}
public void mousePressed(MouseEvent ev) {
l.mousePressed(SwingUtilities.convertMouseEvent(planComponent, ev, MultipleLevelsPlanPanel.this));
}
public void mouseExited(MouseEvent ev) {
l.mouseExited(SwingUtilities.convertMouseEvent(planComponent, ev, MultipleLevelsPlanPanel.this));
}
public void mouseEntered(MouseEvent ev) {
l.mouseEntered(SwingUtilities.convertMouseEvent(planComponent, ev, MultipleLevelsPlanPanel.this));
}
public void mouseClicked(MouseEvent ev) {
l.mouseClicked(SwingUtilities.convertMouseEvent(planComponent, ev, MultipleLevelsPlanPanel.this));
}
});
}
@Override
public void addFocusListener(final FocusListener l) {
FocusListener componentFocusListener = new FocusListener() {
public void focusGained(FocusEvent ev) {
l.focusGained(new FocusEvent(MultipleLevelsPlanPanel.this, FocusEvent.FOCUS_GAINED, ev.isTemporary(), ev.getOppositeComponent()));
}
public void focusLost(FocusEvent ev) {
l.focusLost(new FocusEvent(MultipleLevelsPlanPanel.this, FocusEvent.FOCUS_LOST, ev.isTemporary(), ev.getOppositeComponent()));
}
};
this.planComponent.addFocusListener(componentFocusListener);
this.multipleLevelsTabbedPane.addFocusListener(componentFocusListener);
}
/**
* Sets rectangle selection feedback coordinates.
*/
public void setRectangleFeedback(float x0, float y0, float x1, float y1) {
this.planComponent.setRectangleFeedback(x0, y0, x1, y1);
}
/**
* Ensures selected items are visible in the plan displayed by this component and moves
* its scroll bars if needed.
*/
public void makeSelectionVisible() {
this.planComponent.makeSelectionVisible();
}
/**
* Ensures the point at (<code>x</code>, <code>y</code>) is visible in the plan displayed by this component,
* moving its scroll bars if needed.
*/
public void makePointVisible(float x, float y) {
this.planComponent.makePointVisible(x, y);
}
/**
* Returns the scale used to display the plan displayed by this component.
*/
public float getScale() {
return this.planComponent.getScale();
}
/**
* Sets the scale used to display the plan displayed by this component.
*/
public void setScale(float scale) {
this.planComponent.setScale(scale);
}
/**
* Moves the plan displayed by this component from (dx, dy) unit in the scrolling zone it belongs to.
*/
public void moveView(float dx, float dy) {
this.planComponent.moveView(dx, dy);
}
/**
* Returns <code>x</code> converted in model coordinates space.
*/
public float convertXPixelToModel(int x) {
return this.planComponent.convertXPixelToModel(SwingUtilities.convertPoint(this, x, 0, this.planComponent).x);
}
/**
* Returns <code>y</code> converted in model coordinates space.
*/
public float convertYPixelToModel(int y) {
return this.planComponent.convertYPixelToModel(SwingUtilities.convertPoint(this, 0, y, this.planComponent).y);
}
/**
* Returns <code>x</code> converted in screen coordinates space.
*/
public int convertXModelToScreen(float x) {
return this.planComponent.convertXModelToScreen(x);
}
/**
* Returns <code>y</code> converted in screen coordinates space.
*/
public int convertYModelToScreen(float y) {
return this.planComponent.convertYModelToScreen(y);
}
/**
* Returns the length in centimeters of a pixel with the current scale.
*/
public float getPixelLength() {
return this.planComponent.getPixelLength();
}
/**
* Returns the coordinates of the bounding rectangle of the <code>text</code> displayed at
* the point (<code>x</code>,<code>y</code>).
*/
public float [][] getTextBounds(String text, TextStyle style, float x, float y, float angle) {
return this.planComponent.getTextBounds(text, style, x, y, angle);
}
/**
* Sets the cursor of this component as rotation cursor.
*/
public void setCursor(CursorType cursorType) {
this.planComponent.setCursor(cursorType);
}
/**
* Sets tool tip text displayed as feedback.
*/
public void setToolTipFeedback(String toolTipFeedback, float x, float y) {
this.planComponent.setToolTipFeedback(toolTipFeedback, x, y);
}
/**
* Set properties edited in tool tip.
*/
public void setToolTipEditedProperties(EditableProperty [] toolTipEditedProperties, Object [] toolTipPropertyValues,
float x, float y) {
this.planComponent.setToolTipEditedProperties(toolTipEditedProperties, toolTipPropertyValues, x, y);
}
/**
* Deletes tool tip text from screen.
*/
public void deleteToolTipFeedback() {
this.planComponent.deleteToolTipFeedback();
}
/**
* Sets whether the resize indicator of selected wall or piece of furniture
* should be visible or not.
*/
public void setResizeIndicatorVisible(boolean visible) {
this.planComponent.setResizeIndicatorVisible(visible);
}
/**
* Sets the location point for alignment feedback.
*/
public void setAlignmentFeedback(Class<? extends Selectable> alignedObjectClass, Selectable alignedObject, float x,
float y, boolean showPoint) {
this.planComponent.setAlignmentFeedback(alignedObjectClass, alignedObject, x, y, showPoint);
}
/**
* Sets the points used to draw an angle in the plan displayed by this component.
*/
public void setAngleFeedback(float xCenter, float yCenter, float x1, float y1, float x2, float y2) {
this.planComponent.setAngleFeedback(xCenter, yCenter, x1, y1, x2, y2);
}
/**
* Sets the feedback of dragged items drawn during a drag and drop operation,
* initiated from outside of the plan displayed by this component.
*/
public void setDraggedItemsFeedback(List<Selectable> draggedItems) {
this.planComponent.setDraggedItemsFeedback(draggedItems);
}
/**
* Sets the given dimension lines to be drawn as feedback.
*/
public void setDimensionLinesFeedback(List<DimensionLine> dimensionLines) {
this.planComponent.setDimensionLinesFeedback(dimensionLines);
}
/**
* Deletes all elements shown as feedback.
*/
public void deleteFeedback() {
this.planComponent.deleteFeedback();
}
/**
* Returns <code>true</code> if the given coordinates belong to the plan displayed by this component.
*/
public boolean canImportDraggedItems(List<Selectable> items, int x, int y) {
JViewport viewport = this.planScrollPane.getViewport();
Point point = SwingUtilities.convertPoint(this, x, y, viewport);
return viewport.contains(point);
}
/**
* Returns the component used as an horizontal ruler for the plan displayed by this component.
*/
public View getHorizontalRuler() {
return this.planComponent.getHorizontalRuler();
}
/**
* Returns the component used as a vertical ruler for the plan displayed by this component.
*/
public View getVerticalRuler() {
return this.planComponent.getVerticalRuler();
}
/**
* Prints the plan component.
*/
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
return this.planComponent.print(graphics, pageFormat, pageIndex);
}
/**
* Returns the preferred scale to print the plan component.
*/
public float getPrintPreferredScale(Graphics graphics, PageFormat pageFormat) {
return this.planComponent.getPrintPreferredScale(graphics, pageFormat);
}
/**
* A dummy label used to track tabs matching levels.
*/
private static class LevelLabel extends JLabel {
private final Level level;
public LevelLabel(Level level) {
this.level = level;
}
public Level getLevel() {
return this.level;
}
}
}