// Copyright (c) 2006 - 2008, Markus Strauch.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
package net.sf.sdedit.ui.impl;
import static java.lang.System.currentTimeMillis;
import static javax.swing.SwingUtilities.invokeLater;
import static javax.swing.SwingUtilities.isEventDispatchThread;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.util.LinkedList;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import net.sf.sdedit.Constants;
import net.sf.sdedit.config.Configuration;
import net.sf.sdedit.config.ConfigurationManager;
import net.sf.sdedit.diagram.Diagram;
import net.sf.sdedit.diagram.Lifeline;
import net.sf.sdedit.drawable.Arrow;
import net.sf.sdedit.drawable.Drawable;
import net.sf.sdedit.error.DiagramError;
import net.sf.sdedit.error.FatalError;
import net.sf.sdedit.text.TextHandler;
import net.sf.sdedit.ui.PanelPaintDevice;
import net.sf.sdedit.ui.PanelPaintDeviceListener;
import net.sf.sdedit.ui.components.AutoCompletion;
import net.sf.sdedit.ui.components.Stainable;
import net.sf.sdedit.ui.components.StainedListener;
import net.sf.sdedit.ui.components.TextArea;
import net.sf.sdedit.ui.components.ZoomPane;
import net.sf.sdedit.ui.components.AutoCompletion.SuggestionProvider;
import net.sf.sdedit.ui.components.configuration.Bean;
/**
* A single tab in the user interface, consisting of a diagram view, a text pane
* and a status bar that can be exchanged by a text field for entering a filter
* command applied to the text in the pane. All methods that depend on or change
* the state of GUI components on the screen use the event dispatch thread
* internally.
*
* @author Markus Strauch
*
*/
public class Tab extends JPanel implements Stainable, DocumentListener,
SuggestionProvider, PropertyChangeListener {
private static final long serialVersionUID = -4105088603920744983L;
private LinkedList<Diagram> diagramStack;
private DiagramError error;
final private RedrawThread redrawThread;
final private UserInterfaceImpl ui;
final private JLabel errorLabel;
final private JLabel statusLabel;
final private JPanel bottomPanel;
final private ZoomPane zoomPane;
final private TextArea textArea;
final private FilterCommandField filterField;
final private JPanel bottom;
/**
* The file that is associated to the content in the text-area.
*/
private File file;
/**
* Flag indicating if the text and the contents of the file are consistent,
* or if there is no file associated and no text has yet been entered.
*/
private boolean stained;
/**
* A list of listeners that are informed when the clean flag gets false.
*/
final private List<StainedListener> stainedListeners;
/**
* This string is set to the contents of the text area when it is to be
* declared to be consistent via <tt>setClean(true)</tt> or when a file is
* loaded.
*/
private String code;
/**
* The index of the character in the text-area where an erroreous line
* starts, or -1. See {@linkplain #setError(boolean, String, int, int)}.
*/
private int errorCharIndex;
/**
* The time in milliseconds since 1970, when a key has been typed for the
* last time. On {@linkplain #redraw()}, auto-scrolling will only be done
* if at most half a second has passed since that time.
*/
private long timeOfLastKeyChange;
private JPopupMenu menu;
private JSplitPane splitter;
private JScrollPane textScroller;
private boolean filterMode;
private Bean<Configuration> configuration;
private Bean<Configuration> oldConfiguration;
private List<PanelPaintDeviceListener> ppdListeners;
Tab(UserInterfaceImpl ui, RedrawThread redrawThread, Font codeFont,
Bean<Configuration> configuration
) {
this.ui = ui;
this.redrawThread = redrawThread;
diagramStack = new LinkedList<Diagram>();
textArea = new TextArea();
textArea.setFont(codeFont);
textArea.getDocument().addDocumentListener(this);
textArea.setMinimumSize(new Dimension(100, 100));
ppdListeners = new LinkedList<PanelPaintDeviceListener>();
new AutoCompletion(textArea, this, '=', ':', '>');
menu = createPopupMenu();
zoomPane = new ZoomPane();
zoomPane.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (getDiagram() != null && e.getButton() == MouseEvent.BUTTON3) {
menu.show((Component) e.getSource(), e.getX(), e.getY());
e.consume();
return;
}
}
});
filterField = new FilterCommandField(ui);
filterMode = false;
textScroller = new JScrollPane();
textScroller.getVerticalScrollBar().setUnitIncrement(30);
setLineWrap(configuration.getDataObject().isLineWrap());
splitter = new JSplitPane(JSplitPane.VERTICAL_SPLIT, zoomPane,
textScroller);
splitter.setOneTouchExpandable(true);
splitter.setResizeWeight(0.8);
setLayout(new BorderLayout());
stainedListeners = new LinkedList<StainedListener>();
add(splitter, BorderLayout.CENTER);
errorCharIndex = -1;
code = "";
errorLabel = new JLabel("");
statusLabel = new JLabel("");
errorLabel.addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(MouseEvent e) {
if (errorCharIndex > -1) {
errorLabel.setCursor(Constants.HAND_CURSOR);
}
}
@Override
public void mouseExited(MouseEvent e) {
errorLabel.setCursor(Cursor.getDefaultCursor());
}
@Override
public void mouseClicked(MouseEvent e) {
if (errorCharIndex > -1) {
moveCursorToPosition(errorCharIndex);
}
}
});
bottom = new JPanel();
bottom.setLayout(new BorderLayout());
add(bottom, BorderLayout.SOUTH);
bottomPanel = new JPanel();
bottomPanel.setLayout(new BorderLayout());
bottomPanel.add(errorLabel, BorderLayout.CENTER);
bottomPanel.add(statusLabel, BorderLayout.EAST);
bottomPanel.setPreferredSize(new Dimension(Integer.MAX_VALUE, 20));
bottom.add(bottomPanel, BorderLayout.CENTER);
LookAndFeelManager.instance().registerOrphan(this);
LookAndFeelManager.instance().registerOrphan(filterField);
this.configuration = configuration;
configuration.addPropertyChangeListener(this);
setClean();
}
void addPanelPaintDeviceListener(PanelPaintDeviceListener ppdl) {
ppdListeners.add(ppdl);
}
private void setLineWrap(boolean on) {
if (on) {
textScroller.setViewportView(textArea);
} else {
JPanel noWrapPanel = new JPanel(new BorderLayout());
noWrapPanel.add(textArea);
textScroller.setViewportView(noWrapPanel);
}
}
private void somethingChanged() {
redrawThread.indicateChange();
timeOfLastKeyChange = currentTimeMillis();
invokeLater(new Runnable() {
public void run() {
boolean isStained = textArea.getText().length() != code
.length()
|| !textArea.getText().equals(code)
|| !oldConfiguration.equals(configuration);
if (stained != isStained) {
fireStainedStatusChanged(isStained);
stained = isStained;
}
ui.enableComponents();
}
});
}
synchronized DiagramError getDiagramError() {
return error;
}
void renderDiagram() {
PanelPaintDevice paintDevice = new PanelPaintDevice(true);
for (PanelPaintDeviceListener listener : ppdListeners) {
paintDevice.addListener(listener);
}
TextHandler textHandler = new TextHandler(getCode());
Diagram diagram = new Diagram(configuration.getDataObject(),
textHandler, paintDevice);
DiagramError newError = null;
try {
diagram.generate();
} catch (RuntimeException e) {
newError = new FatalError(textHandler, e);
} catch (DiagramError e) {
newError = e;
}
synchronized (diagramStack) {
diagramStack.addLast(diagram);
synchronized (this) {
error = newError;
}
}
}
Diagram getDiagram() {
synchronized (diagramStack) {
switch (diagramStack.size()) {
case 0:
return null;
case 1:
return diagramStack.getLast();
default:
Diagram diagram = diagramStack.getLast();
diagramStack.clear();
diagramStack.addLast(diagram);
return diagram;
}
}
}
public void addStainedListener(StainedListener listener) {
stainedListeners.add(listener);
}
/**
*
* @param layout
* 0 for a split along the x-axis, 1 for the y-axis
*/
void layout(int layout) {
remove(splitter);
switch (layout) {
case 0:
splitter = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
textScroller, zoomPane);
splitter.setResizeWeight(0.2);
break;
case 1:
splitter = new JSplitPane(JSplitPane.VERTICAL_SPLIT, zoomPane,
textScroller);
splitter.setOneTouchExpandable(true);
splitter.setResizeWeight(0.8);
break;
default:
throw new IllegalArgumentException("layout " + layout
+ " not supported");
}
splitter.setOneTouchExpandable(true);
add(splitter, BorderLayout.CENTER);
revalidate();
configuration.getDataObject().setVerticallySplit(layout == 1);
ui.enableComponents();
}
TextArea getTextArea() {
return textArea;
}
boolean isClean() {
return !stained;
}
void setFile(File file) {
this.file = file;
}
File getFile() {
return file;
}
ZoomPane getZoomPane() {
return zoomPane;
}
void setClean() {
boolean fire = stained;
code = textArea.getText();
stained = false;
oldConfiguration = configuration.copy();
if (fire) {
fireStainedStatusChanged(false);
}
}
private void fireStainedStatusChanged(boolean status) {
for (StainedListener listener : stainedListeners) {
listener.stainedStatusChanged(status);
}
}
/**
* Clears the diagram view.
*/
void clear() {
synchronized (diagramStack) {
diagramStack.clear();
}
redraw();
}
/**
* Scrolls the diagram view to the top-left corner.
*/
void home() {
invokeLater(new Runnable() {
public void run() {
zoomPane.home();
}
});
}
/**
* Returns the code that is currently begin displayed by the text-area.
*
* @return the code that is currently being displayed by the text-area.
*/
String getCode() {
return textArea.getText();
}
/**
* Changes the code displayed by the text-area. After that,
* {@linkplain #isClean()} will return true.
*
* @param code
* the code to be displayed by the text-area
*/
void setCode(final String code) {
if (isEventDispatchThread()) {
textArea.setText(code);
setClean();
return;
}
invokeLater(new Runnable() {
public void run() {
textArea.setText(code);
setClean();
}
});
}
void redraw() {
invokeLater(new Runnable() {
public void run() {
Diagram diagram = getDiagram();
if (diagram != null) {
zoomPane.setViewportView(((PanelPaintDevice) diagram
.getPaintDevice()).getPanel());
if (ConfigurationManager.getGlobalConfiguration()
.isAutoScroll()
&& currentTimeMillis() - timeOfLastKeyChange <= 500) {
scrollToCurrentDrawable();
}
} else {
zoomPane.setViewportView(null);
}
ui.enableComponents();
}
});
}
void moveCursorToPosition(int position) {
textArea.requestFocusInWindow();
textArea.setCaretPosition(position);
}
void undo() {
textArea.undo();
}
void redo() {
textArea.redo();
}
void enterFilterMode() {
if (filterMode) {
return;
}
filterMode = true;
invokeLater(new Runnable() {
public void run() {
filterField.reset();
bottom.remove(bottomPanel);
bottom.add(filterField, BorderLayout.CENTER);
bottom.revalidate();
filterField.requestFocus();
}
});
}
void toggleFilterMode() {
if (filterMode) {
leaveFilterMode();
} else {
enterFilterMode();
}
}
void leaveFilterMode() {
if (!filterMode) {
return;
}
filterMode = false;
invokeLater(new Runnable() {
public void run() {
filterField.reset();
bottom.remove(filterField);
bottom.add(bottomPanel, BorderLayout.CENTER);
bottom.revalidate();
}
});
}
private void scrollToCurrentDrawable() {
int begin = textArea.getCurrentLineBegin();
Diagram diagram = getDiagram();
if (diagram != null) {
PanelPaintDevice ppd = (PanelPaintDevice) diagram.getPaintDevice();
Drawable drawable = diagram.getDrawableForState(begin);
if (drawable instanceof Arrow) {
Arrow arrow = (Arrow) drawable;
Point textPosition = arrow.getTextPosition();
int x = textPosition != null ? textPosition.x : arrow.getLeft();
float xratio = 1F * x / ppd.getWidth();
int y = drawable.getTop();
float yratio = 1F * y / ppd.getHeight();
zoomPane.scrollToPosition(xratio, yratio);
} else {
if (drawable != null) {
int x = drawable.getLeft();
float xratio = 1F * x / ppd.getWidth();
int y = drawable.getTop();
float yratio = 1F * y / ppd.getHeight();
zoomPane.scrollToPosition(xratio, yratio);
} else {
int caret = textArea.getCaretPosition();
if (textArea.getText().substring(caret).trim().length() == 0) {
zoomPane.scrollToBottom();
}
}
}
}
}
void append(final String text) {
if (isEventDispatchThread()) {
textArea.setText(textArea.getText() + text);
// happens automatically via DocumentListener
// redrawThread.indicateChange();
} else {
invokeLater(new Runnable() {
public void run() {
textArea.setText(textArea.getText() + text);
// redrawThread.indicateChange();
}
});
}
}
void setStatus(final String status) {
invokeLater(new Runnable() {
public void run() {
statusLabel.setText(status + " ");
}
});
}
void setError(final boolean warning, final String error, final int begin,
final int end) {
invokeLater(new Runnable() {
public void run() {
if (warning) {
errorLabel.setForeground(Color.ORANGE);
} else {
errorLabel.setForeground(Color.RED);
}
errorLabel.setText(error);
errorCharIndex = begin;
textArea.markError(begin, end);
}
});
}
@SuppressWarnings("serial")
private JPopupMenu createPopupMenu() {
JPopupMenu popup = new JPopupMenu();
Action fitSize = new AbstractAction() {
{
putValue(Action.NAME, "Fit size");
}
public void actionPerformed(ActionEvent e) {
zoomPane.fitSize();
}
};
Action originalZoom = new AbstractAction() {
{
putValue(Action.NAME, "100 %");
}
public void actionPerformed(ActionEvent e) {
zoomPane.setScale(1);
}
};
popup.add(fitSize);
popup.add(originalZoom);
return popup;
}
/**
* @see net.sf.sdedit.ui.components.AutoCompletion.SuggestionProvider#getSuggestions(java.lang.String)
*/
public List<String> getSuggestions(String prefix) {
List<String> suggestions = new LinkedList<String>();
Diagram diag = getDiagram();
if (diag != null) {
for (Lifeline lifeline : diag.getAllLifelines()) {
String name = lifeline.getName();
if (name.startsWith(prefix)) {
suggestions.add(name);
}
}
}
return suggestions;
}
Bean<Configuration> getConfiguration() {
return configuration;
}
void setConfiguration(Bean<Configuration> configuration) {
if (configuration != null) {
this.configuration.removePropertyChangeListener(this);
}
this.configuration = configuration;
this.configuration.addPropertyChangeListener(this);
oldConfiguration = configuration;
}
/**
* Called on configuration changes.
*/
public void propertyChange(PropertyChangeEvent evt) {
somethingChanged();
if (evt.getPropertyName().toLowerCase().equals("linewrap")) {
boolean wrap = (Boolean) evt.getNewValue();
setLineWrap(wrap);
}
}
/**
* Called on source text changes.
*/
public void changedUpdate(DocumentEvent e) {
somethingChanged();
}
/**
* Called on source text changes.
*/
public void insertUpdate(DocumentEvent e) {
somethingChanged();
}
/**
* Called on source text changes.
*/
public void removeUpdate(DocumentEvent e) {
somethingChanged();
}
}