/*
* Copyright 2003-2010 Tufts University Licensed under the
* Educational Community License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may
* obtain a copy of the License at
*
* http://www.osedu.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS IS"
* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package tufts.vue;
import tufts.Util;
import tufts.vue.gui.GUI;
import tufts.vue.gui.TextRow;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Font;
import java.awt.Color;
import java.awt.BasicStroke;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.font.TextAttribute;
import java.awt.geom.Rectangle2D;
import java.awt.geom.Point2D;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JTextPane;
import javax.swing.TransferHandler;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.*;
//import java.awt.font.LineBreakMeasurer;
//import java.awt.font.TextAttribute;
/**
* A multi-line editable text object that supports left/center/right
* aligment for its lines of text.
*
* Used in two modes: (1) "normal" mode -- used to paint multi-line
* text objects (labels, notes, etc) and (2) "edit". In normal mode,
* this JComponent has no parent -- it isn't added to any AWT
* hierarchy -- it's only used to paint as part of the
* LWMap/LWContainer paint tree (via the draw(Graphics2D)) method. In
* edit mode, it's temporarily added to the canvas so it can receive
* user input. Only one instance of these is ever added and active in
* AWT at the same time. We have to do some funky wrangling to deal
* with zoom, because the JComponent can't paint and interact on a
* zoomed (scaled) graphics context (unless we were to implement mouse
* event retargeting, which is a future possibility). So if there is
* a scale active on the currently displayed map, we manually derive a
* new font for the whole text object (the Document) and set it to
* that temporarily while it's active in edit mode, and then re-set it
* upon removal. Note that users of this class (e.g., LWNode) should
* not bother to paint it (call draw()) if it's in edit mode
* (getParent() != null) as the AWT/Swing tree is dealing with that
* while it's in its activated edit state.
*
* We use a JTextPane because it supports a StyledDocument, which is
* what we need to be able to set left/center/right aligment for all
* the paragraphs in the document. This is a bit heavy weight for our
* uses right now as we only make use of one font at a time for the whole
* document (this is the heaviest weight text component in Swing).
* JTextArea would have worked for us, except it only supports its
* fixed default of left-aligned text. However, eventually we're
* probably going to want to suport intra-string formatting (fonts,
* colors, etc) and so we'll be ready for that, with the exception of
* the hack mentioned above to handle zooming (tho we could
* theoretically iterate through the whole document, individually
* deriving zoomed fonts for every font found in the Document.)
*
* Note that you can get center/variable alignment even with
* a JLabel if it's text starts with <HTML>, and you
* put in a center tag. Unfortunately, JTextArea's don't
* do HTML w/out setting up a StyledDocument manually.
*
*
* @author Scott Fraize
* @version $Revision: 1.69 $ / $Date: 2010-02-03 19:17:40 $ / $Author: mike $
*
*/
public class TextBox extends JTextPane
implements VueConstants
, FocusListener
, KeyListener
, DocumentListener
, MouseListener
, ActionListener
{
private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(TextBox.class);
// todo: duplicate not working[? for wrap only? ]
private static final boolean WrapText = LWNode.WrapText;
private static final Color SelectionColor = GUI.getTextHighlightColor();//VueResources.getColor("mapViewer.textBox.selection.color");
private static boolean TestDebug = false;
private static boolean TestHarness = false;
private LWComponent lwc;
/** bounds: generally used by the component as local coordinates (relative to the coordinate 0,0)
* The width/height are set here in TextBox */
private final Rectangle2D.Float mBounds = new Rectangle2D.Float();
private boolean wasOpaque; /** were we opaque before we started an edit? */
private MutableAttributeSet mAttributeSet;
private float mMaxCharWidth;
private float mMaxWordWidth;
private boolean mKeepHeight = false;
TextBox(LWComponent lwc)
{
this(lwc, null);
}
TextBox(LWComponent lwc, String text)
{
if (DEBUG.TEXT && DEBUG.LAYOUT) tufts.Util.printClassTrace("tufts.vue.", "NEW TextBox, txt=" + text);
if (TestDebug||DEBUG.TEXT) out("NEW [" + text + "] " + lwc);
this.lwc = lwc;
//setBorder(javax.swing.border.LineBorder.createGrayLineBorder());
// don't set border -- adds small margin that screws us up, especially
// at high scales
setDragEnabled(false);
setBorder(null);
if (text != null)
setText(text);
setMargin(null);
setOpaque(false); // don't bother to paint background
setVisible(true);
addMouseListener(this);
//setFont(SmallFont);
// PC text pane will pick this font up up as style for
// document, but mac ignores.
//setAlignmentX(1f);//nobody's paying attention to this
//setContentType("text/rtf"); for attributes + unicode, but will need lots of work
addKeyListener(this);
addFocusListener(this);
getDocument().addDocumentListener(this);
setSize(getPreferredSize());
if (VueUtil.isWindowsPlatform() && SelectionColor != null)
setSelectionColor(SelectionColor);
if (VueUtil.isWindowsPlatform() && SelectionColor != null)
setSelectedTextColor(Color.black);
mBounds.x = Float.NaN; // mark as uninitialized
mBounds.y = Float.NaN; // mark as uninitialized
if (TestDebug||DEBUG.TEXT) out("constructed " + getSize());
}
/*
public String getText() {
//java.io.ByteArrayOutputStream buf = new java.io.ByteArrayOutputStream();
Document doc = getDocument();
String text = null;
try {
// better to use doc.getText(0, doc.getLength())
//getEditorKit().write(buf, doc, 0, doc.getLength());
text = doc.getText(0, doc.getLength());
} catch (Exception e) {
e.printStackTrace();
}
if (text == null || text.length() == 0)
return ":"+super.getText();
return text;
//return buf.toString();
}
*/
public float getMaxCharWidth() {
return mMaxCharWidth;
}
public float getMaxWordWidth() {
return mMaxWordWidth;
}
LWComponent getLWC() {
return this.lwc;
}
/*
* When activated for editing, draw an appropriate background color
* for the node -- the we need to do because if it's a small on-screen
* font at the moment (depending on zoom level), we make the
* text box always appear at the 100% zoomed font (because we're
* not managing scaled repaint of the added object or retargeting
* scaled mouse events, etc). Also, when it's transparent, the
* whole map has to be repainted each cursor blink just in case
* there is some LWComponent under the transparent text item.
* (Tho with a very small clip region).
*/
private Font preZoomFont = null;
private String mUnchangedText;
private Dimension mUnchangedSize;
private boolean keyWasPressed = false;
private static final int MinEditSize = 11; // todo: prefs
// todo bug: on PC, font edits at size < 11 are failing produce the
// right selection or cursor coordinates, and what you see
// is NOT what you get anymore. ACTUALLY, this may be due
// to special charactes in the string -- it was a piece of
// pasted HTML text with "1/2" chars and \226 dashes..
// Okay, no -- even vanilla text at 10 point does it
// (SansSerif-plain-10) -- okay, this is crap -- a "regular"
// node v.s. a text node is working fine down at nine-point,
// tho it does have only 3 lines -- ugh, this is going
// to require alot of fiddling and testing.
/** called by MapViewer before we go active on the map */
void saveCurrentState()
{
mUnchangedText = getText();
mUnchangedSize = getSize();
}
/** deprecated */
void saveCurrentText() {
saveCurrentState();
}
@Override
public void addNotify()
{
if (TestDebug||DEBUG.TEXT) out("*** ADDNOTIFY ***");
if (getText().length() < 1)
setText("<label>");
keyWasPressed = false;
Dimension size = getSize();
super.addNotify();
// note: we get a a flash/move if we add the border before the super.addNotify()
if (false && TestDebug)
; //setBorder(javax.swing.BorderFactory.createLineBorder(Color.green));
else {
// ADDING THIS BORDER INCREASES THE PREFERRED SIZE
// Width goes up by 4 pix and height goes up by a line because
// actual getSize width no longer fits. Very convoluted.
//setBorder(javax.swing.border.LineBorder.createGrayLineBorder());
}
java.awt.Container parent = getParent();
if (parent instanceof MapViewer) { // todo: could be a scroller?
double zoom = ((MapViewer)parent).getZoomFactor();
zoom *= lwc.getMapScale();
if (zoom != 1.0) {
final Font f = lwc.getFont();
float zoomedPointSize = (float) (f.getSize() * zoom);
if (zoomedPointSize < MinEditSize)
zoomedPointSize = MinEditSize;
preZoomFont = f;
final Font screenFont = f.deriveFont(f.getStyle(), zoomedPointSize);
setDocumentFont(screenFont);
if (TestDebug||DEBUG.TEXT) {
out("derived temporary screen font:"
+ "\n\t net zoom: " + zoom
+ "\n\t node font: " + f
+ "\n\tscreen font: " + screenFont + " size2D=" + screenFont.getSize2D()
);
}
if (WrapText) {
double boxZoom = zoomedPointSize / preZoomFont.getSize();
size.width *= boxZoom;
size.height *= boxZoom;
} else {
setSize(getPreferredSize());
}
} else {
// this forces the AWT to redraw this component
// (just calling repaint doesn't do it).
// When zoomed we must do this (see above), so
// in that case it's already handled.
setDocumentFont(lwc.getFont());
}
}
wasOpaque = isOpaque();
Color background = lwc.getRenderFillColor(null);
//if (c == null && lwc.getParent() != null && lwc.getParent() instanceof LWNode)
final LWContainer nodeParent = lwc.getParent();
if (background == null && nodeParent != null)
background = nodeParent.getRenderFillColor(null); // todo: only handles 1 level transparent embed!
// todo: could also consider using map background if the node itself
// is transpatent (has no fill color)
// TODO: this workaround until we can recursively find a real fill color
// node that for SLIDES, we'd have to get awfully fancy and
// usually pull the color of the master slide (unless the slide
// happened to have it's own special color). Only super clean
// way to do this would be to have established some kind of
// rendering pipeline record... (yeah, right)
if (background == null) background = Color.gray;
//out("BACKGROUND COLOR " + background);
// TODO: the *selection* color always appears to be gray in for edits
// in the slide viewer on the mac, even if we manually set the selection
// color (which works in the main MapViewer) -- this is an oddity...
if (background != null) {
// note that if we set opaque to false, interaction speed is
// noticably slowed down in edit mode because it has to consider
// repainting the entire map each cursor blink as the object
// is transparent, and thus it's background is the displayed
// map. So if we can guess at a reasonable fill color in edit mode,
// we temporarily set us to opaque.
setOpaque(true);
setBackground(background);
}
setSize(size);
Dimension preferred = getPreferredSize();
int w = getWidth() + 1;
if (w < preferred.width)
mKeepTextWidth = true;
else
mKeepTextWidth = false;
if (TestDebug||DEBUG.TEXT) out("width+1=" + w + " < preferred.width=" + preferred.width + " fixedWidth=" + mKeepTextWidth);
if (TestDebug||DEBUG.TEXT) out("addNotify end: insets="+getInsets());
mFirstAfterAddNotify = true;
}
/*
* Return to the regular transparent state.
*/
@Override
public void removeNotify()
{
if (TestDebug||DEBUG.TEXT) out("*** REMOVENOTIFY ***");
//------------------------------------------------------------------
// We need to clear any text selection here as a workaround
// for an obscure bug where sometimes if the focus change is
// to a pop-up menu, the edit properly goes inactive, but the
// selection within it is still drawn with it's highlighted
// background.
clearSelection();
//------------------------------------------------------------------
super.removeNotify();
if (mFirstAfterAddNotify == false) {
// if cleared, it was used
out("restoring expanded width");
setSize(new Dimension(getWidth()-1, getHeight()));
//out("SKPPING restoring expanded width");
} else
mFirstAfterAddNotify = false;
setBorder(null);
if (preZoomFont != null) {
setDocumentFont(preZoomFont);
preZoomFont = null;
if (WrapText) {
adjustSizeDynamically();
} else {
setSize(getPreferredSize());
// WE MUST DO THIS A SECOND TIME TO MAKE SURE THIS WORKS:
// JTextPane can actually produce inconsistent results
// when getPreferredSize() is called, especially if it's
// results were just use to set the size of the object.
// A second get/set produces more reliable results.
setSize(getPreferredSize());
}
}
if (wasOpaque != isOpaque())
setOpaque(wasOpaque);
if (TestDebug||DEBUG.TEXT) out("*** REMOVENOTIFY end: insets="+getInsets());
}
public void clearSelection() {
// this set's the "mark to the point" -- sets them to the same
// location, thus clearing the selection.
setCaretPosition(getCaretPosition());
}
@Override
public void setText(String text)
{
if (getDocument() == null) {
out("creating new document");
setStyledDocument(new DefaultStyledDocument());
}
/*try {
doc.insertString(0, text, null);
} catch (Exception e) {
System.err.println(e);
}*/
if (TestDebug||DEBUG.TEXT) out("setText[" + text + "]");
super.setText(text);
copyStyle(this.lwc);
if (WrapText) {
;
} else {
setSize(getPreferredSize());
}
}
public boolean keepHeight() {
if (mKeepHeight) {
mKeepHeight = false;
return true;
} else
return false;
}
private void setDocumentFont(Font f)
{
if (DEBUG.TEXT)
out("setDocumentFont " + f);
SimpleAttributeSet a = new SimpleAttributeSet();
setFontAttributes(a, f);
StyledDocument doc = getStyledDocument();
doc.setParagraphAttributes(0, doc.getEndPosition().getOffset(), a, false);
computeMinimumWidth(f, lwc.getLabel());
mKeepHeight = true;
}
private void setDocumentColor(Color c)
{
StyleConstants.setForeground(mAttributeSet, c);
StyledDocument doc = getStyledDocument();
doc.setParagraphAttributes(0, doc.getEndPosition().getOffset(), mAttributeSet, true);
}
private static void setFontAttributes(MutableAttributeSet a, Font f)
{
setFontAttributes(a,f,null);
}
private static void setFontAttributes(MutableAttributeSet a, Font f, LWComponent c)
{
if (DEBUG.TEXT)
System.out.println("setFontAttribnutes " + f);
StyleConstants.setFontFamily(a, f.getFamily());
StyleConstants.setFontSize(a, f.getSize());
StyleConstants.setItalic(a, f.isItalic());
StyleConstants.setBold(a, f.isBold());
if (c !=null)
{
String s = c.mFontUnderline.get();
if (s.equals("underline"))
StyleConstants.setUnderline(a, true);
else
StyleConstants.setUnderline(a, false);
}
}
// this called every time setText is called to ensure we get
// the font style encoded in our owning LWComponent
void copyStyle(LWComponent c)
{
if (DEBUG.TEXT)
out("copyStyle " + c);
SimpleAttributeSet a = new SimpleAttributeSet();
if (TestHarness || c instanceof LWNode && ((LWNode)c).isTextNode())
StyleConstants.setAlignment(a, StyleConstants.ALIGN_LEFT);
else
StyleConstants.setAlignment(a, StyleConstants.ALIGN_CENTER);
StyleConstants.setForeground(a, c.getTextColor());
final Font font = c.getFont();
setFontAttributes(a, font,c);
StyledDocument doc = getStyledDocument();
if (DEBUG.TEXT) getPreferredSize();
doc.setParagraphAttributes(0, doc.getEndPosition().getOffset(), a, false);
if (DEBUG.TEXT) getPreferredSize();
computeMinimumWidth(font, lwc.getLabel());
mKeepHeight = true;
mAttributeSet = a;
if (WrapText) {
// adjust WIDTH ONLY (or: attempt to keep aspect)
//adjustSize(false);
} else {
setSize(getPreferredSize());
setSize(getPreferredSize());
}
}
/** compute mMaxWordWidth and mMaxCharWidth */
private void computeMinimumWidth(Font font, String text) {
mMaxCharWidth = (float) font.getMaxCharBounds(DefaultFontContext).getWidth();
try {
mMaxWordWidth = maxWordWidth(font, text);
} catch (Exception e) {
mMaxWordWidth = mMaxCharWidth;
}
}
private static final boolean DebugWord = false;
private static final int BigWordLen = 9;
private float maxWordWidth(Font font, String text) {
if (text == null || text.length() == 0)
return mMaxCharWidth;
if (text.length() > 512) // provide a rough figure if string is long
return mMaxCharWidth * BigWordLen;
if (text.indexOf(' ') < 0 && text.indexOf('\n') < 0) // if no spaces, specal case no wrapping
return (float) font.getStringBounds(text, DefaultFontContext).getWidth();
int maxRunIdx = 0;
int maxRunLen = 0;
int curRunIdx = 0;
int curRunLen = 0;
boolean lastWasBreak = false;
float maxWidth = 0;
final int len = text.length();
for (int i = 0; i <= len; i++) {
char c;
if (i < len) {
c = text.charAt(i);
curRunLen++;
} else
c = 0;
if (DebugWord) out("char[" + c + "] ci="+curRunIdx + " cl=" + curRunLen);
// add '/' as word break character if no whitespace?
if (c == 0 || Character.isWhitespace(c) || c == '.' || c == ',')
; // treat as a word break
else
continue;
if (c != 0) { // if we're not at the end
try {
char whiteChar;
do {
curRunLen++;
whiteChar = text.charAt(++i);
if (DebugWord) out("char{" + whiteChar + "} ci="+curRunIdx + " cl=" + curRunLen);
} while (Character.isWhitespace(whiteChar));
} catch (StringIndexOutOfBoundsException e) {
if (DebugWord) out("charEOS ci="+curRunIdx + " cl=" + curRunLen);
}
curRunLen--;
i--;
}
float wordWidth = (float)
font.getStringBounds(text,
curRunIdx,
curRunIdx + curRunLen,
DefaultFontContext).getWidth();
if (DebugWord) out("word[" + text.substring(curRunIdx, curRunIdx + curRunLen) + "] w=" + wordWidth);
if (wordWidth > maxWidth) {
if (c == 0 && curRunIdx == 0) {
// If no whitespace in the whole thing, allow some breaking (should never happen currently)
return wordWidth < mMaxCharWidth * BigWordLen ? wordWidth : mMaxCharWidth * BigWordLen;
} else {
maxWidth = wordWidth;
maxRunIdx = curRunIdx;
maxRunLen = curRunLen;
if (DebugWord) out("MI="+curRunIdx + " ML=" + curRunLen + " w=" + wordWidth);
}
}
curRunIdx = i + 1;
curRunLen = 0;
}
if (DebugWord || DEBUG.TEXT) out("maxWord[" + text.substring(maxRunIdx, maxRunIdx + maxRunLen) + "] w=" + maxWidth);
return maxWidth;
}
/** override Container.doLayout: only called when we've been added to a map for interactive editing.
* Is called during interactive edit's after each modification.
*/
public void doLayout()
{
if (getParent() instanceof MapViewer) {
// Automatic layout (e.g. FlowLayout)
// produces two layout passes -- perhaps
// this is why we need to call this TWICE
// here so that the box size doesn't
// temporarily flash bigger on every update.
if (TestDebug || DEBUG.LAYOUT) out("doLayout w/adjustSizeDynamically");
if (false)
out("skipping size fix-up");
else {
if (WrapText)
adjustSizeDynamically();
else {
setSize(getPreferredSize());
setSize(getPreferredSize());
}
}
} else {
if (!TestHarness)
new Throwable(this + " UNPARENTED doLayout").printStackTrace();
}
//super.layout();
}
private boolean mFirstAfterAddNotify = false;
private boolean mKeepTextWidth = false;
/** adjust size given new text that has been typed into this component while it's being display */
private void adjustSizeDynamically() {
adjustSize(mKeepTextWidth);
}
private void adjustSize(boolean keepTextWidth) {
if (TestDebug||DEBUG.TEXT) out("adjustSize, keepWidth=" + keepTextWidth);
Dimension preferred = getPreferredSize();
Dimension newSize;
if (keepTextWidth) {
newSize = new Dimension();
newSize.width = getWidth();
if (mFirstAfterAddNotify) {
if (TestDebug||DEBUG.TEXT) out("adding 1 to width");
// ensure there is room to draw cursor at end of line.
// Actually, it appears that once the addNotify is done, getPreferredSize
// returns a width increased by one: it must notice it's now editable
// and want to include room for the cursor.
newSize.width++;
mFirstAfterAddNotify = false;
}
newSize.height = preferred.height;
} else {
// First call to preferred size may be based somewhat on current size
// E.g., if current size doesn't fit all on line line, will give a preferred
// that includes two lines. However, if we then set our size to the new preferred
// size, it turns out it all DOES fit on one line, and then the height becomes
// reported as only one line's worth in the second getPreferredSize().
setSize(preferred);
newSize = getPreferredSize();
newSize.width++; // ensure enough room to draw cursor at end of line
}
if (TestDebug||DEBUG.TEXT) out("adjustTo", newSize, keepTextWidth ? "(keep width)" : "(expand width)");
setSize(newSize);
//new Throwable("adjustSizeDynamically").printStackTrace();
}
public void keyReleased(KeyEvent e) { e.consume(); }
public void keyTyped(KeyEvent e) {
// todo: would be nice if centered labels stayed center as you typed them
//setLocation((int)lwc.getLabelX(), (int)lwc.getLabelY());
// needs something else, plus can't work at zoom because
// width isn't updated till the end (because width at + zoom
// is overstated based on temporarily scaled font)
// Man, it would be REALLY nice if we could paint the
// real component in a scaled GC w/out the font tweaking --
// problems like this would go away.
}
private static boolean isFinishEditKeyPress(KeyEvent e) {
// if we hit return key either on numpad ("enter" key), or
// with any modifier down except a shift alone (in case of
// caps lock) complete the edit.
return e.getKeyCode() == KeyEvent.VK_ENTER &&
(
e.getKeyLocation() == KeyEvent.KEY_LOCATION_NUMPAD ||
(e.getModifiersEx() != 0 && !e.isShiftDown())
)
== true;
//== false; // reversed logic of below description
}
private Container removeAsEdit() {
Container parent = getParent();
if (parent != null)
parent.remove(this);
else
out("FAILED TO FIND PARENT ATTEMPTING TO REMOVE SELF");
return parent;
}
public void keyPressed(KeyEvent e)
{
if (DEBUG.KEYS) out(e.toString());
//System.out.println("TextBox: " + e);
//if (VueUtil.isAbortKey(e)) // check for ESCAPE for CTRL-Z or OPTION-Z if on mac
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
e.consume();
getParent().remove(this); // will trigger a save (via focusLost)
super.setText(mUnchangedText);
setSize(mUnchangedSize); // todo: won't be good enough if we ever resize the actual node as we type
} else if (isFinishEditKeyPress(e)) {
keyWasPressed = true;
e.consume();
getParent().remove(this); // will trigger a save (via focusLost)
} else if (e.getKeyCode() == KeyEvent.VK_U && e.isMetaDown()) {
e.consume();
String t = getText();
if (e.isShiftDown())
setText(t.toUpperCase()); // upper whole string
else
setText(Character.toTitleCase(t.charAt(0)) + t.substring(1)); // upper first char
} else
keyWasPressed = true;
// action keys will be ignored if we consume this here!
// (e.g., "enter" does nothing)
//e.consume();
}
/**
* This is what triggers the final save of the new text value to the LWComponent,
* and notify's the UndoManager that a user action was completed.
*/
public void focusLost(FocusEvent e)
{
if (TestDebug||DEBUG.FOCUS) outc("focusLost to " + e.getOppositeComponent());
if (TestHarness == false && getParent() != null)
getParent().remove(this);
if (keyWasPressed) { // TODO: as per VueTextField, need to handle drag & drop detect
// only do this if they typed something (so we don't wind up with "label"
// for the label on an accidental edit activation)
if (TestDebug||DEBUG.FOCUS) out("key was pressed; setting label to: [" + getText() + "]");
final String text = getText();
computeMinimumWidth(lwc.getFont(), text); // do before setLabel
lwc.setLabel0(text, false);
VUE.getUndoManager().mark();
}
lwc.notify(this, LWKey.Repaint);
}
public void focusGained(FocusEvent e)
{
if (TestDebug||DEBUG.FOCUS) outc("focusGained from " + e.getOppositeComponent());
}
/** do not use, or won't be able to get out actual text height */
@Override
public void setPreferredSize(Dimension preferredSize) {
if (true||TestDebug) out("setPreferred " + preferredSize);
super.setPreferredSize(preferredSize);
}
@Override
public Dimension getPreferredSize() {
Dimension s;
try {
// Is this failing due to this being accessed from an ImgLoader thread?
s = super.getPreferredSize();
} catch (Throwable t) {
Log.error("getPreferredSize", t);
s = new Dimension(200,100);
}
if (getParent() != null)
s.width += 1; // leave room for cursor, which at least on mac gets clipped if at EOL
if (TestDebug) out("getPrefer", s);
return s;
}
// @Override
// public Dimension getPreferredSize() {
// Dimension s = super.getPreferredSize();
// //getMinimumSize();//debug
// //s.width = (int) lwc.getWidth();
// //System.out.println("MTP lwcWidth " + lwc.getWidth());
// if (getParent() != null)
// s.width += 1; // leave room for cursor, which at least on mac gets clipped if at EOL
// //if (getParent() == null)
// // s.width += 10;//fudge factor for understated string widths (don't do this here -- need best accuracy here)
// if (TestDebug) out("getPrefer", s);
// //if (TestDebug && DEBUG.META) new Throwable("getPreferredSize").printStackTrace();
// return s;
// }
public void setSize(Size s) {
setSize(s.dim());
}
public void setSize(Dimension s) {
if (TestDebug||DEBUG.TEXT) out("setSize", s);
super.setSize(s);
mBounds.width = s.width;
mBounds.height = s.height;
// if (preZoomFont == null) {
// // preZoomFont only set if we had to zoom the font
// //this.mapWidth = s.width;
// //this.mapHeight = s.height;
// mBounds.width = s.width;
// mBounds.height = s.height;
// }
}
/*
public void setHeight(int h) {
// todo: may need to all the above setSize for font code
super.setSize(getWidth(), h);
}
*/
/**
* Set the size to the given size, increasing or decreasing height as
* needed to provide a fit around our text
*/
public void setSizeFlexHeight(Size newSize) {
//------------------------------------------------------------------
// Tell the JTextPane to take on the size requested. It will
// then set the preferred height to the minimum height able to
// contain the given text at that width.
//------------------------------------------------------------------
setSize(newSize);
//------------------------------------------------------------------
// Now adjust our height to the new preferred height, which should
// just contain our text.
//------------------------------------------------------------------
final Dimension s = getPreferredSize();
s.width = getWidth();
if (TestDebug||DEBUG.TEXT) out("flexHeigt", s);
super.setSize(s.width, s.height);
}
public void setSize(float w, float h) {
setSize(new Dimension((int)w, (int)h));
}
public void setPreferredSize(float w, float h) {
setPreferredSize(new Dimension((int)w, (int)h));
}
public Dimension getSize() {
Dimension s = super.getSize();
//s.width = (int) lwc.getWidth();
if (TestDebug) out("getSize", s);
//if (DEBUG.TEXT&&DEBUG.META) new Throwable("getSize").printStackTrace();
return s;
}
public void setMaximumSize(Dimension s) {
if (true||TestDebug) out("setMaximumSize", s);
super.setMaximumSize(s);
}
public Dimension getMaximumSize() {
Dimension s = super.getMaximumSize();
if (TestDebug||DEBUG.TEXT) out("getMaximumSize", s);
return s;
}
public void setMinimumSize(Dimension s) {
if (true||TestDebug) out("setMinimumSize", s);
super.setMinimumSize(s);
}
public Dimension getMinimumSize() {
Dimension s = super.getMinimumSize();
if (TestDebug||DEBUG.TEXT) out("getMinimumSize", s);
return s;
}
@Override
public void reshape(int x, int y, int w, int h) {
if (TestDebug||DEBUG.TEXT) {
boolean change = getX() != x || getY() != y || getWidth() != w || getHeight() != h;
if (change) {
out(" oldshape " + tufts.Util.out(getBounds()));
out(" reshape " + w + "x" + h + " " + x + "," + y);
}
//out(" reshape " + w + "x" + h + " " + x + "," + y + (change ? "" : " (no change)"));
if (DEBUG.META && change)
new Throwable("reshape").printStackTrace();
}
super.reshape(x, y, w, h);
if (TestDebug||DEBUG.TEXT) {
Rectangle b = getBounds();
if (b.x != x || b.y != y || b.width != w || b.height != h)
out("BADBOUNDS " + tufts.Util.out(b));
}
}
public Rectangle2D getBoxBounds() {
return mBounds;
}
public boolean boxContains(float x, float y)
{
return x >= mBounds.x
&& y >= mBounds.y
&& x <= mBounds.x + mBounds.width
&& y <= mBounds.y + mBounds.height;
}
public boolean boxIntersects(Rectangle2D rect)
{
return rect.intersects(mBounds);
}
public void setBoxLocation(float x, float y)
{
mBounds.x = x;
mBounds.y = y;
}
public void setBoxLocation(Point2D p)
{
setBoxLocation((float) p.getX(), (float) p.getY());
}
public void setBoxCenter(float x, float y) {
setBoxLocation(x - getBoxWidth() / 2,
y - getBoxHeight() / 2);
}
public Point2D.Float getBoxPoint()
{
return new Point2D.Float(mBounds.x, mBounds.y);
}
public float getBoxWidth() { return mBounds.width; };
public float getBoxHeight() { return mBounds.height; }
public float getBoxX() { return mBounds.x; };
public float getBoxY() { return mBounds.y; }
/*
void resizeToWidth(float w)
{
int width = (int) (w + 0.5f);
setSize(new Dimension(width, 999)); // can set height to 1 as we're ignore the set-size
// now the preferred height will be set to the real
// total text height at that width -- pull it back out and set actual size to same
Dimension ps = getPreferredSize();
setSize(new Dimension(width, (int)ps.getHeight()));
}
*/
public void Xpaint(Graphics g) {
super.paint(g);
g.setColor(Color.gray);
g.setClip(null);
g.drawRect(0,0, getWidth(), getHeight());
}
public void paintComponent(Graphics g)
{
if (TestDebug||DEBUG.TEXT) out("paintComponent @ " + getX() + "," + getY() + " parent=" + getParent());
final MapViewer viewer = (MapViewer) javax.swing.SwingUtilities.getAncestorOfClass(MapViewer.class, this);
if (viewer != null)
((Graphics2D)g).setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, viewer.AA_ON);
// turn on anti-aliasing -- the cursor repaint loop doesn't
// set anti-aliasing, so text goes jiggy around cursor/in selection if we don't do this
super.paintComponent(g);
if (true) {
// draw a border (we don't want to add one because that changes the preferred size at a bad time)
g.setColor(Color.gray);
g.setClip(null);
final int xpad = 1;
final int ypad = 1;
g.drawRect(-xpad,-ypad, getWidth()+xpad*2-1, getHeight()+ypad*2-1);
}
}
private static final BasicStroke MinStroke = new BasicStroke(1/8f);
private static final BasicStroke MinStroke2 = new BasicStroke(1/24f);
/** @return true if hue value of Color is black, ignoring any alpha */
private boolean isBlack(Color c) {
return c != null && (c.getRGB() & 0xFFFFFF) == 0;
}
public void draw(DrawContext dc)
{
if (TestDebug) out("draw");
if (getParent() != null)
System.err.println("Warning: 2nd draw of an AWT drawn component!");
//todo: could try saving current translation or coordinates here,
// so have EXACT last position painted at. Tho we really should
// be able to compute it... problem is may not be at integer
// boundry at current translation, but have to be when we add it
// to the map -- tho hey, LWNode could force integer boundry
// when setting the translation before painting us.
if (DEBUG.BOXES && DEBUG.META) {
if (lwc.getLabel().indexOf('\n') < 0) {
TextRow r = new TextRow(lwc.getLabel(), lwc.getFont(), dc.g.getFontRenderContext());
dc.g.setColor(Color.lightGray);
r.draw(dc, 0, 0);
}
}
boolean restoreTextColor = false;
// if (dc.isBlackWhiteReversed() &&
// (dc.isPresenting() || lwc.isTransparent() /*|| isBlack(lwc.getFillColor())*/) &&
// isBlack(lwc.getTextColor())) {
// //System.out.println("reversing color to white for " + this);
// setDocumentColor(Color.white);
// inverted = true;
// } else
// inverted = false;
if (dc.isPresenting() && lwc.isTransparent()) {
// if the text color equals the background color when in a presentation
// (e.g. the master slide has a black background), and the text box
// has to fill of it's own for contrast, then temporarily swap
// the text color to white or black so it can be seen.
if (lwc.mTextColor.equals(dc.getBackgroundFill())) {
restoreTextColor = true;
if (lwc.mTextColor.brightness() > 0.5) {
setDocumentColor(DEBUG.Enabled ? Color.blue : Color.black);
} else {
setDocumentColor(DEBUG.Enabled ? Color.green : Color.white);
}
}
}
//super.paintBorder(g);
// // As of least Mac OS X 10.4.10 w/JVM 1.5.0_07 on 2007-08-13,
// // it appears there's no way to NOT render anti-aliased text,
// // unless there's some other way to override it in JTextPane/JTextComponent
// // Not a big deal -- we'd only like the option for a slight speed up
// // during animations.
// dc.g.setRenderingHint(java.awt.RenderingHints.KEY_TEXT_ANTIALIASING,
// java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
// dc.g.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING,
// java.awt.RenderingHints.VALUE_ANTIALIAS_OFF);
// // Even this doesn't appear to help:
// putClientProperty(com.sun.java.swing.SwingUtilities2.AA_TEXT_PROPERTY_KEY, Boolean.FALSE);
super.paintComponent(dc.g);
//super.paint(g);
if (restoreTextColor) {
// return document color to black
setDocumentColor(lwc.mTextColor.get());
}
// draw a border for links -- why?
// and even if, better to handle in LWLink
/*
if (lwc instanceof LWLink) {
Dimension s = getSize();
if (lwc.isSelected())
g.setColor(COLOR_SELECTION);
else
g.setColor(Color.gray);
g.setStroke(MinStroke);
g.drawRect(0,0, s.width-1, s.height-2);
}
*/
Graphics2D g = dc.g;
if (DEBUG.BOXES) {
Dimension s = getPreferredSize();
g.setColor(Color.red);
dc.setAbsoluteStroke(0.5);
//g.setStroke(MinStroke);
g.drawRect(0,0, s.width, s.height);
//g.drawRect(0,0, s.width-1, s.height);
}
//s = getMinimumSize();
//g.setColor(Color.red);
//g.setStroke(new BasicStroke(1/8f));
//g.drawRect(0,0, s.width, s.height);
if (DEBUG.BOXES || getParent() != null) {
Dimension s = getSize();
g.setColor(Color.blue);
dc.setAbsoluteStroke(0.5);
//g.setStroke(MinStroke);
g.drawRect(0,0, s.width, s.height);
//g.drawRect(0,0, s.width-1, s.height);
}
}
private void handleChange() {
// appears to be happening too late for dynamic size adjust -- current character isnt include
}
public void removeUpdate(DocumentEvent de) {
if (TestDebug||DEBUG.TEXT) out("removeUpdate " + de);
handleChange();
}
public void changedUpdate(DocumentEvent de) {
if (TestDebug||DEBUG.TEXT) out("changeUpdate " + de.getType() + " len=" + de.getLength());
handleChange();
}
public void insertUpdate(DocumentEvent de) {
if (TestDebug||DEBUG.TEXT) out("insertUpdate " + de);
handleChange();
}
public String toString()
{
return "TextBox[" + lwc + "]";
}
private static class TestPanel extends javax.swing.JPanel {
final TextBox box;
TestPanel(TextBox box) {
this.box = box;
setBorder(new javax.swing.border.EmptyBorder(100,100,100,100));
setLayout(new java.awt.BorderLayout());
box.setBorder(javax.swing.BorderFactory.createLineBorder(Color.blue));
//add(box, java.awt.BorderLayout.CENTER);
add(box, java.awt.BorderLayout.NORTH);
//box.setEditable(false);
}
public void paint(Graphics g) {
super.paint(g);
Dimension d = box.getPreferredSize();
g.setColor(Color.red);
g.drawRect(box.getX(), box.getY(), d.width-1, d.height-1);
g.drawString(d.width + "x" + d.height + " preferred size", box.getX(), box.getY()-2);
g.setColor(Color.blue);
g.drawString(box.getWidth() + "x" + box.getHeight() + " size", box.getX(), box.getY() + box.getHeight() + 11);
}
}
private String id() {
return Integer.toHexString(System.identityHashCode(this));
}
private void out(String s) {
System.out.println("TextBox@" + id() + " [" + getText() + "] " + s);
//System.out.println("TextBox@" + id() + " " + s);
}
private void out(String s, Dimension d) {
out(VueUtil.pad(' ', 9, s, true) + " " + tufts.Util.out(d));
}
private void out(String s, Dimension d, String s2) {
out(VueUtil.pad(' ', 9, s, true) + " " + tufts.Util.out(d) + " " + s2);
}
private void outc(String s) {
System.out.println(this + " " + id() + " " + s);
}
public static void main(String args[]) {
VUE.parseArgs(args);
VUE.initUI();
TestDebug = true;
TestHarness = true;
DEBUG.BOXES = true;
LWComponent node = new LWNode("Foo");
TextBox box = new TextBox(node, "One Two Three Four Five Six Seven");
tufts.vue.gui.DockWindow w = GUI.createDockWindow("TextBox Resize Test", new TestPanel(box));
w.setVisible(true);
//tufts.Util.displayComponent(panel);
}
public void mouseClicked(MouseEvent e) {
// TODO Auto-generated method stub
}
public void mouseEntered(MouseEvent e) {
// TODO Auto-generated method stub
}
public void mouseExited(MouseEvent e) {
// TODO Auto-generated method stub
}
public void mousePressed(MouseEvent e)
{
if (GUI.isMenuPopup(e))
{
displayContextMenu(e);
return;
}
}
private void displayContextMenu(MouseEvent e) {
getPopup(e).show(e.getComponent(), e.getX(), e.getY());
}
private JPopupMenu m = null;
private final JMenuItem copyItem = new JMenuItem(VueResources.getString("richtextbox.menu.copy"));
private final JMenuItem pasteItem = new JMenuItem(VueResources.getString("richtextbox.menu.paste"));
private JPopupMenu getPopup(MouseEvent e)
{
if (m == null)
{
m = new JPopupMenu(VueResources.getString("richtextbox.menu.textboxmenu"));
//copyItem.addActionListener(this);
//pasteItem.addActionListener(this);
//If you let this be focusable you'll loose the text box when
//the menu gets raised.
m.setFocusable(false);
m.add(copyItem);
m.add(pasteItem);
copyItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
TextBox.this.copy();
}
});
pasteItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
TextBox.this.paste();
setSize(getPreferredSize());
}
});
}
return m;
}
public void mouseReleased(MouseEvent e) {
// TODO Auto-generated method stub
}
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
//if (e.getSource().equals(copyItem))
}
}