/*
* (c) Copyright 2010-2011 AgileBirds
*
* This file is part of OpenFlexo.
*
* OpenFlexo 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 3 of the License, or
* (at your option) any later version.
*
* OpenFlexo 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 OpenFlexo. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.openflexo.fge.view;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.util.Arrays;
import java.util.Observable;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.JViewport;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.plaf.basic.BasicScrollPaneUI;
import javax.swing.plaf.basic.BasicTextPaneUI;
import javax.swing.plaf.basic.BasicViewportUI;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DocumentFilter;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import org.openflexo.fge.DrawingGraphicalRepresentation;
import org.openflexo.fge.GraphicalRepresentation;
import org.openflexo.fge.GraphicalRepresentation.LabelMetricsProvider;
import org.openflexo.fge.GraphicalRepresentation.ParagraphAlignment;
import org.openflexo.fge.ShapeGraphicalRepresentation;
import org.openflexo.fge.controller.DrawingController;
import org.openflexo.fge.controller.DrawingPalette;
import org.openflexo.fge.graphics.TextStyle;
import org.openflexo.fge.notifications.FGENotification;
import org.openflexo.fge.notifications.LabelHasMoved;
import org.openflexo.fge.notifications.LabelWillMove;
import org.openflexo.fge.notifications.ObjectHasMoved;
import org.openflexo.fge.notifications.ObjectHasResized;
import org.openflexo.fge.notifications.ObjectWillMove;
import org.openflexo.fge.notifications.ObjectWillResize;
import org.openflexo.fge.notifications.ShapeNeedsToBeRedrawn;
import org.openflexo.fge.view.listener.LabelViewMouseListener;
import org.openflexo.swing.FlexoSwingUtils;
import org.openflexo.toolbox.ToolBox;
public class LabelView<O> extends JScrollPane implements FGEView<O>, LabelMetricsProvider {
private boolean mouseInsideLabel = false;
/**
* This class tries to keep trace if the mouse is inside the label or not. This partially works but it heavily relies on the fact that
* FGEViewMouseListener will simulate mouse in/out events. In some cases, this may not work.
*
* @author Guillaume
*
*/
private class InOutMouseListener extends MouseAdapter {
@Override
public void mouseEntered(MouseEvent e) {
mouseInsideLabel = true;
textComponent.updateCursor();
}
@Override
public void mouseExited(MouseEvent e) {
mouseInsideLabel = false;
textComponent.updateCursor();
}
}
public boolean isMouseInsideLabel() {
return mouseInsideLabel;
}
public class TextComponent extends JTextPane {
public TextComponent() {
setUI(new BasicTextPaneUI());
setOpaque(false);
setEditable(false);
setAutoscrolls(false);
setFocusable(true);
}
@Override
public Dimension getPreferredSize() {
if (getText().length() == 0) {
return new Dimension(30, getFont().getSize());
}
return super.getPreferredSize();
}
@Override
public void updateUI() {
}
protected void updateCursor() {
if (getDrawingView() != null) {
getDrawingView().setCursor(isMouseInsideLabel() ? getCursor() : null);
}
}
@Override
public void setCursor(Cursor cursor) {
super.setCursor(cursor);
updateCursor();
}
@Override
public boolean getScrollableTracksViewportWidth() {
return true;
}
@Override
public void setDoubleBuffered(boolean aFlag) {
super.setDoubleBuffered(aFlag && ToolBox.getPLATFORM() == ToolBox.MACOS);
}
@Override
public void setEditable(boolean b) {
super.setEditable(b);
setEnabled(b);
if (!initialized) {
return;
}
setDoubleBuffered(!b);
if (b) {
removeFGEMouseListener();
requestFocusInWindow();
selectAll();
} else {
addFGEMouseListener();
}
}
}
private static final Logger logger = Logger.getLogger(LabelView.class.getPackage().getName());
private GraphicalRepresentation<O> graphicalRepresentation;
private LabelViewMouseListener mouseListener;
private DrawingController<?> controller;
private FGEView<?> delegateView;
private boolean isEditing = false;
private TextComponent textComponent;
private boolean initialized = false;
public LabelView(GraphicalRepresentation<O> graphicalRepresentation, DrawingController<?> controller, FGEView<?> delegateView) {
setUI(new BasicScrollPaneUI());
getViewport().setUI(new BasicViewportUI());
this.controller = controller;
this.graphicalRepresentation = graphicalRepresentation;
this.delegateView = delegateView;
this.mouseListener = new LabelViewMouseListener(graphicalRepresentation, this);
this.textComponent = new TextComponent();
this.textComponentListener = new LabelDocumentListener();
textComponent.addMouseListener(new InOutMouseListener());
((AbstractDocument) textComponent.getDocument()).setDocumentFilter(new DocumentFilter() {
@Override
public void insertString(FilterBypass fb, int offset, String text, AttributeSet attr) throws BadLocationException {
if (!getGraphicalRepresentation().getIsMultilineAllowed()) {
if (text.equals("\n") || text.equals("\r\n")) {
return;
}
}
text = filteredText(text);
super.insertString(fb, offset, text, attr);
}
@Override
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
if (!getGraphicalRepresentation().getIsMultilineAllowed()) {
if (length == 0) {
if (text.equals("\n") || text.equals("\r\n")) {
return;
}
}
}
text = filteredText(text);
super.replace(fb, offset, length, text, attrs);
}
private String filteredText(String text) {
if (!getGraphicalRepresentation().getIsMultilineAllowed()) {
return text.replaceAll("\r?\n", " ");
}
return text;
}
});
getViewport().setBorder(null);
getViewport().setOpaque(false);
setViewportView(textComponent);
setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
// Note: if for debug purposes you add a Border to the textComponent, this could mess up the labels preferredSize computation.
setBorder(null);
setViewportBorder(null);
setOpaque(false);
graphicalRepresentation.setLabelMetricsProvider(this);
textComponent.setLocation(0, 0);
updateFont();
updateText();
getGraphicalRepresentation().addObserver(this);
validate();
initialized = true;
textComponent.setEditable(false);
}
@Override
public void updateUI() {
}
public void registerTextListener() {
textComponent.addKeyListener(textComponentListener);
textComponent.getDocument().addDocumentListener(textComponentListener);
textComponent.addCaretListener(textComponentListener);
}
public void unregisterTextListener() {
textComponent.removeKeyListener(textComponentListener);
textComponent.getDocument().removeDocumentListener(textComponentListener);
textComponent.removeCaretListener(textComponentListener);
}
@Override
protected JViewport createViewport() {
return new JViewport() {
@Override
public void setViewPosition(Point p) {
// We don't want to scroll so we prevent the view port
// from moving.
}
@Override
public void updateUI() {
}
};
}
public TextComponent getTextComponent() {
return textComponent;
}
private volatile boolean isDeleted = false;
private LabelDocumentListener textComponentListener;
@Override
public boolean isDeleted() {
return isDeleted;
}
@Override
public synchronized void delete() {
if (logger.isLoggable(Level.FINE)) {
logger.fine("Delete LabelView for " + getGraphicalRepresentation());
}
if (getController() != null && getController().getEditedLabel() == this) {
getController().resetEditedLabel(this);
}
removeFGEMouseListener();
FGELayeredView<?> parentView = getParentView();
if (parentView != null) {
// logger.warning("Unexpected not null parent, proceeding anyway");
parentView.remove(this);
parentView.revalidate();
if (getPaintManager() != null) {
getPaintManager().repaint(parentView);
}
}
if (getGraphicalRepresentation() != null) {
getGraphicalRepresentation().deleteObserver(this);
if (graphicalRepresentation instanceof ShapeGraphicalRepresentation) {
((ShapeGraphicalRepresentation<O>) graphicalRepresentation).setLabelMetricsProvider(null);
}
}
isDeleted = true;
controller = null;
mouseListener = null;
graphicalRepresentation = null;
}
public void enableTextComponentMouseListeners() {
removeFGEMouseListener();
}
public void disableTextComponentMouseListeners() {
addFGEMouseListener();
}
@Override
public O getModel() {
return getDrawable();
}
public O getDrawable() {
return getGraphicalRepresentation().getDrawable();
}
@Override
public DrawingView<?> getDrawingView() {
if (getController() != null) {
return getController().getDrawingView();
}
return null;
}
@Override
public LabelView<O> getLabelView() {
return this;
}
@Override
public FGELayeredView<?> getParent() {
return (FGELayeredView<?>) super.getParent();
}
public FGELayeredView<?> getParentView() {
return getParent();
}
@Override
public GraphicalRepresentation<O> getGraphicalRepresentation() {
return graphicalRepresentation;
}
public DrawingGraphicalRepresentation<?> getDrawingGraphicalRepresentation() {
return graphicalRepresentation.getDrawingGraphicalRepresentation();
}
@Override
public double getScale() {
if (getController() != null) {
return getController().getScale();
} else {
return 1.0;
}
}
@Override
public void rescale() {
updateFont();
}
@Override
public void paint(Graphics g) {
boolean skipPaint = getPaintManager().isPaintingCacheEnabled() && getPaintManager().getDrawingView().isBuffering()
&& (getPaintManager().isTemporaryObject(getGraphicalRepresentation()) || isEditing);
if (skipPaint || isDeleted() || !getGraphicalRepresentation().hasText()) {
return;
}
super.paint(g);
}
@Override
public DrawingController<?> getController() {
return controller;
}
@Override
public synchronized void update(final Observable o, final Object aNotification) {
if (isDeleted) {
// logger.warning("Received notifications for deleted view: observable="+(o!=null?o.getClass().getSimpleName():"null"));
return;
}
if (!SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
update(o, aNotification);
}
});
} else {
// System.out.println("Received: "+aNotification);
if (aNotification instanceof FGENotification) {
FGENotification notification = (FGENotification) aNotification;
if (notification.getParameter() == GraphicalRepresentation.Parameters.text
// There are some GR in WKF that rely on ShapeNeedsToBeRedrawn notification to update text (this can be removed once we
// properly use appropriate bindings
|| aNotification instanceof ShapeNeedsToBeRedrawn) {
updateText();
getPaintManager().repaint(this);
} else if (notification.getParameter() == GraphicalRepresentation.Parameters.textStyle) {
updateFont();
getPaintManager().repaint(this);
} else if (notification.getParameter() == GraphicalRepresentation.Parameters.paragraphAlignment) {
updateFont();
getPaintManager().repaint(this);
} else if (notification.getParameter() == GraphicalRepresentation.Parameters.horizontalTextAlignment
|| notification.getParameter() == GraphicalRepresentation.Parameters.verticalTextAlignment) {
updateBounds();
getPaintManager().repaint(this);
} else if (notification.getParameter() == ShapeGraphicalRepresentation.Parameters.relativeTextX
|| notification.getParameter() == ShapeGraphicalRepresentation.Parameters.relativeTextY
|| notification.getParameter() == GraphicalRepresentation.Parameters.absoluteTextX
|| notification.getParameter() == GraphicalRepresentation.Parameters.absoluteTextY
|| notification.getParameter() == ShapeGraphicalRepresentation.Parameters.isFloatingLabel) {
updateBounds();
getPaintManager().repaint(this);
} else if (notification instanceof ObjectWillMove || notification instanceof ObjectWillResize
|| notification instanceof LabelWillMove) {
setDoubleBuffered(false);
if (notification instanceof LabelWillMove) {
getPaintManager().addToTemporaryObjects(getGraphicalRepresentation());
getPaintManager().invalidate(getGraphicalRepresentation());
}
} else if (notification instanceof ObjectHasMoved || notification instanceof ObjectHasResized
|| notification instanceof LabelHasMoved) {
setDoubleBuffered(true);
if (notification instanceof LabelHasMoved) {
getPaintManager().removeFromTemporaryObjects(getGraphicalRepresentation());
}
}
}
}
}
protected void updateBounds() {
updateBounds(true);
}
protected synchronized void updateBounds(final boolean repeat) {
if (isDeleted()) {
return;
}
if (!SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
updateBounds(repeat);
}
});
return;
}
Rectangle bounds = graphicalRepresentation.getLabelBounds(getScale());
if (bounds.isEmpty() || bounds.width < 5) {
bounds.width = 20;
bounds.height = getFont().getSize();
}
if (!bounds.equals(getBounds())) {
setBounds(bounds);
validate();
if (repeat) {
updateBoundsLater();
}
}
}
public void updateBoundsLater() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
updateBounds(true);
}
});
}
@Override
public Dimension getScaledPreferredDimension(double scale) {
Dimension preferredSize = getCurrentPreferredSize(scale);
if (scale == getScale()) {
return preferredSize;
} else {
Dimension d = preferredSize;
d.width *= scale;
d.height *= scale;
d.width /= getScale();
d.height /= getScale();
return d;
}
}
private class PreferredSizeRetriever implements Callable<Dimension> {
private double scale;
protected PreferredSizeRetriever(double scale) {
super();
this.scale = scale;
}
@Override
public Dimension call() {
if (isDeleted()) {
return getSize();
}
return getCurrentPreferredSize(scale);
}
}
private Dimension getCurrentPreferredSize(final double scale) {
if (isDeleted || getGraphicalRepresentation() == null) {
return getSize();
}
if (!SwingUtilities.isEventDispatchThread()) {
final PreferredSizeRetriever retriever = new PreferredSizeRetriever(scale);
try {
return FlexoSwingUtils.syncRunInEDT(retriever);
} catch (Exception e) {
if (logger.isLoggable(Level.SEVERE)) {
logger.log(Level.SEVERE, "Exception when computing preferredSize of " + this, e);
}
}
}
int width = getGraphicalRepresentation().getAvailableLabelWidth(scale);
if (getGraphicalRepresentation().getLineWrap()) {
textComponent.setSize(width, Short.MAX_VALUE);
}
Dimension preferredSize = textComponent.getPreferredScrollableViewportSize();
if (preferredSize.width > width) {
preferredSize.width = width;
}
return preferredSize;
}
private void updateFont() {
AffineTransform at = AffineTransform.getScaleInstance(getScale(), getScale());
TextStyle ts = getGraphicalRepresentation().getTextStyle();
if (ts == null) {
ts = TextStyle.makeDefault();
}
if (ts.getOrientation() != 0) {
at.concatenate(AffineTransform.getRotateInstance(Math.toRadians(ts.getOrientation())));
}
Font font = ts.getFont().deriveFont(at);
textComponent.setFont(font);
SimpleAttributeSet set = new SimpleAttributeSet();
if (getGraphicalRepresentation().getParagraphAlignment() == ParagraphAlignment.CENTER) {
StyleConstants.setAlignment(set, StyleConstants.ALIGN_CENTER);
} else if (getGraphicalRepresentation().getParagraphAlignment() == ParagraphAlignment.LEFT) {
StyleConstants.setAlignment(set, StyleConstants.ALIGN_LEFT);
} else if (getGraphicalRepresentation().getParagraphAlignment() == ParagraphAlignment.RIGHT) {
StyleConstants.setAlignment(set, StyleConstants.ALIGN_RIGHT);
} else if (getGraphicalRepresentation().getParagraphAlignment() == ParagraphAlignment.JUSTIFY) {
StyleConstants.setAlignment(set, StyleConstants.ALIGN_JUSTIFIED);
}
textComponent.setOpaque(ts.getIsBackgroundColored());
textComponent.setBackground(ts.getBackgroundColor());
StyleConstants.setFontFamily(set, font.getFamily());
StyleConstants.setFontSize(set, (int) (ts.getFont().getSize() * getScale()));
if (font.isBold()) {
StyleConstants.setBold(set, true);
}
if (font.isItalic()) {
StyleConstants.setItalic(set, true);
}
Color color = ts.getColor();
if (color == null) {
color = Color.BLACK;
}
StyleConstants.setForeground(set, color);
textComponent.setForeground(color);
textComponent.setDisabledTextColor(color);
StyledDocument document = textComponent.getStyledDocument();
document.setParagraphAttributes(0, document.getLength(), set, true);
textComponent.validate();
updateBounds();
}
@Override
public void setDoubleBuffered(boolean aFlag) {
super.setDoubleBuffered(aFlag && ToolBox.getPLATFORM() == ToolBox.MACOS);
if (textComponent != null) {
textComponent.setDoubleBuffered(aFlag);
}
}
private void updateText() {
if (isEditing || isDeleted) {
return;
}
if (!SwingUtilities.isEventDispatchThread()) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
updateText();
}
});
return;
}
if (getGraphicalRepresentation().hasText()) {
textComponent.setText(getGraphicalRepresentation().getText());
} else {
textComponent.setText("");
}
updateBounds();
}
public void addFGEMouseListener() {
/*if (!Arrays.asList(getMouseListeners()).contains(mouseListener)) {
addMouseListener(mouseListener);
}
if (!Arrays.asList(getMouseMotionListeners()).contains(mouseListener)) {
addMouseMotionListener(mouseListener);
}*/
if (!Arrays.asList(textComponent.getMouseListeners()).contains(mouseListener)) {
textComponent.addMouseListener(mouseListener);
}
if (!Arrays.asList(textComponent.getMouseMotionListeners()).contains(mouseListener)) {
textComponent.addMouseMotionListener(mouseListener);
}
}
public void removeFGEMouseListener() {
// removeMouseListener(mouseListener);
// removeMouseMotionListener(mouseListener);
textComponent.removeMouseListener(mouseListener);
textComponent.removeMouseMotionListener(mouseListener);
}
public void startEdition() {
if (!getGraphicalRepresentation().getDrawing().isEditable() || !getGraphicalRepresentation().getIsLabelEditable()) {
return;
}
if (logger.isLoggable(Level.INFO)) {
logger.info("Start edition of " + getGraphicalRepresentation());
}
isEditing = true;
registerTextListener();
textComponent.setEditable(true);
setDoubleBuffered(false);
if (getController() != null) {
getController().setEditedLabel(LabelView.this);
}
getGraphicalRepresentation().notifyLabelWillBeEdited();
getPaintManager().invalidate(getGraphicalRepresentation());
getPaintManager().addToTemporaryObjects(getGraphicalRepresentation());
repaint();
}
public void stopEdition() {
if (!isEditing) {
return;
}
// If not continuous edition, do it now
if (!getGraphicalRepresentation().getContinuousTextEditing()) {
getGraphicalRepresentation().setText(textComponent.getText());
}
isEditing = false;
unregisterTextListener();
if (logger.isLoggable(Level.INFO)) {
logger.info("Stop edition of " + getGraphicalRepresentation() + " getController()=" + getController());
}
textComponent.setEditable(false);
setDoubleBuffered(true);
if (getController() != null) {
getController().resetEditedLabel(LabelView.this);
}
if (logger.isLoggable(Level.FINE)) {
logger.fine("Stop edition of " + getGraphicalRepresentation());
}
if (getGraphicalRepresentation() == null || getGraphicalRepresentation().isDeleted()) {
return;
}
getGraphicalRepresentation().notifyLabelHasBeenEdited();
getPaintManager().removeFromTemporaryObjects(getGraphicalRepresentation());
getPaintManager().invalidate(getGraphicalRepresentation());
getPaintManager().repaint(this);
}
class LabelDocumentListener extends KeyAdapter implements DocumentListener, CaretListener {
private boolean wasEdited = false;
private boolean enterPressed = false;
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
enterPressed = true;
if (!wasEdited) {
stopEdition();
}
}
}
@Override
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
enterPressed = false;
if (!wasEdited) {
stopEdition();
}
} else if (e.getKeyChar() != KeyEvent.CHAR_UNDEFINED && !e.isActionKey()) {
wasEdited = true;
}
}
@Override
public void insertUpdate(DocumentEvent event) {
if (!isEditing) {
return;
}
if (getGraphicalRepresentation().getIsMultilineAllowed()) {
// Hack to detect trivial edition (RETURN pressed)
// logger.info("event: "+event);
if (!wasEdited && enterPressed) {
return;
}
}
wasEdited = true;
if (getGraphicalRepresentation().getContinuousTextEditing()) {
getGraphicalRepresentation().setText(textComponent.getText());
}
updateBoundsLater();
}
@Override
public void removeUpdate(DocumentEvent event) {
if (!isEditing) {
return;
}
if (getGraphicalRepresentation().getIsMultilineAllowed()) {
// Hack to detect trivial edition (RETURN pressed)
// logger.info("event: "+event);
if (!wasEdited && enterPressed) {
return;
}
// if (!wasEdited) return;
}
wasEdited = true;
if (getGraphicalRepresentation().getContinuousTextEditing()) {
getGraphicalRepresentation().setText(textComponent.getText());
}
updateBoundsLater();
}
@Override
public void changedUpdate(DocumentEvent event) {
if (!isEditing) {
return;
}
wasEdited = true;
if (getGraphicalRepresentation().getContinuousTextEditing()) {
getGraphicalRepresentation().setText(textComponent.getText());
}
updateBoundsLater();
}
@Override
public void caretUpdate(CaretEvent e) {
// If the whole text is selected, we say that the text was not edited
int start = textComponent.getSelectionStart();
int end = textComponent.getSelectionEnd();
if (start != end) {
if (end == 0 && start == textComponent.getDocument().getLength() || start == 0
&& end == textComponent.getDocument().getLength()) {
wasEdited = false;
}
} else {// Otherwise, it means that user has moved manually the cursor, and we consider he wants to edit the text
wasEdited = true;
}
}
}
@Override
public void registerPalette(DrawingPalette aPalette) {
// Not applicable
}
@Override
public FGEPaintManager getPaintManager() {
return getDrawingView().getPaintManager();
}
public boolean isEditing() {
return isEditing;
}
@Override
public String getToolTipText(MouseEvent event) {
return getController().getToolTipText();
}
}