/*
Part of the GUI library for Processing
http://www.lagers.org.uk/g4p/index.html
http://sourceforge.net/projects/g4p/files/?source=navbar
Copyright (c) 2013 Peter Lager
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General
Public License along with this library; if not, write to the
Free Software Foundation, Inc., 59 Temple Place, Suite 330,
Boston, MA 02111-1307 USA
*/
package automenta.vivisect.gui;
import automenta.vivisect.gui.StyledString.TextLayoutHitInfo;
import automenta.vivisect.gui.StyledString.TextLayoutInfo;
import java.awt.Font;
import java.awt.font.TextAttribute;
import java.awt.font.TextHitInfo;
import java.awt.geom.GeneralPath;
import java.util.LinkedList;
import processing.core.PApplet;
import processing.event.KeyEvent;
/**
*
* This class is the basis for the GTextField and GTextArea classes.
*
* @author Peter Lager
*
*/
public abstract class GEditableTextControl extends GTextBase implements Focusable {
GTabManager tabManager = null;
protected StyledString promptText = null;
// The width to break a line
protected int wrapWidth = Integer.MAX_VALUE;
// The typing area
protected float tx,ty,th,tw;
// Offset to display area
protected float ptx, pty;
// Caret position
protected float caretX, caretY;
protected boolean keepCursorInView = false;
protected GeneralPath gpTextDisplayArea;
// Used for identifying selection and cursor position
protected TextLayoutHitInfo startTLHI = new TextLayoutHitInfo();
protected TextLayoutHitInfo endTLHI = new TextLayoutHitInfo();
// The scrollbars available
protected final int scrollbarPolicy;
protected boolean autoHide = false;
protected GScrollbar hsb, vsb;
protected GTimer caretFlasher;
protected boolean showCaret = false;
// Stuff to manage text selections
protected int endChar = -1, startChar = -1, pos = endChar, nbr = 0, adjust = 0;
protected boolean textChanged = false, selectionChanged = false;
/* Is the component enabled to generate mouse and keyboard events */
boolean textEditEnabled = true;
public GEditableTextControl(PApplet theApplet, float p0, float p1, float p2, float p3, int scrollbars) {
super(theApplet, p0, p1, p2, p3);
scrollbarPolicy = scrollbars;
autoHide = ((scrollbars & SCROLLBARS_AUTOHIDE) == SCROLLBARS_AUTOHIDE);
caretFlasher = new GTimer(theApplet, this, "flashCaret", 400);
caretFlasher.start();
opaque = true;
cursorOver = TEXT;
}
public void setTabManager(GTabManager tm){
tabManager = tm;
}
/**
* Give up focus but if the text is only made from spaces
* then set it to null text. <br>
* Fire focus events for the GTextField and GTextArea controls
*/
protected void loseFocus(GControl grabber){
// If this control has focus then Fire a lost focus event
if(focusIsWith == this)
fireEvent(this, GEvent.LOST_FOCUS);
// Process mouse-over cursor
if(cursorIsOver == this)
cursorIsOver = null;
focusIsWith = grabber;
// If only blank text clear it out allowing default text (if any) to be displayed
if(stext.length() > 0){
int tl = stext.getPlainText().trim().length();
if(tl == 0)
stext = new StyledString("", wrapWidth);
}
keepCursorInView = true;
bufferInvalid = true;
}
/**
* Give the focus to this component but only after allowing the
* current component with focus to release it gracefully. <br>
* Always cancel the keyFocusIsWith irrespective of the component
* type.
* Fire focus events for the GTextField and GTextArea controls
*/
protected void takeFocus(){
// If focus is not yet with this control fire a gets focus event
if(focusIsWith != this){
// If the focus is with another control then tell
// that control to lose focus
if(focusIsWith != null)
focusIsWith.loseFocus(this);
fireEvent(this, GEvent.GETS_FOCUS);
}
focusIsWith = this;
}
/**
* Determines whether this component is to have focus or not. <br>
*/
public void setFocus(boolean focus){
if(!focus){
loseFocus(null);
return;
}
// Make sure we have some text
if(focusIsWith != this){
dragging = false;
if(stext == null || stext.length() == 0)
stext = new StyledString(" ", wrapWidth);
LinkedList<TextLayoutInfo> lines = stext.getLines(buffer.g2);
startTLHI = new TextLayoutHitInfo(lines.getFirst(), null);
startTLHI.thi = startTLHI.tli.layout.getNextLeftHit(1);
endTLHI = new TextLayoutHitInfo(lines.getLast(), null);
int lastChar = endTLHI.tli.layout.getCharacterCount();
endTLHI.thi = startTLHI.tli.layout.getNextRightHit(lastChar-1);
calculateCaretPos(endTLHI);
bufferInvalid = true;
}
keepCursorInView = true;
takeFocus();
}
/**
* This method is deprecated use the setPromptText(String) method instead
* @deprecated
*/
@Deprecated
public void setDefaultText(String dtext){
setPromptText(dtext);
}
/**
* Set the prompt text for this control. When the text control is empty
* the prompt text (italic) is displayed instead.
* .
* @param ptext prompt text
*/
public void setPromptText(String ptext){
if(ptext == null || ptext.length() == 0)
promptText = null;
else {
promptText = new StyledString(ptext, wrapWidth);
promptText.addAttribute(GUI.POSTURE, GUI.POSTURE_OBLIQUE);
}
bufferInvalid = true;
}
/**
* @return the wrapWidth
*/
public int getWrapWidth() {
return wrapWidth;
}
/**
* @param wrapWidth the wrapWidth to set
*/
public void setWrapWidth(int wrapWidth) {
this.wrapWidth = wrapWidth;
}
/**
* This method has been deprecated and you should use the getPromptText() method instead.
* @deprecated
*/
@Deprecated
public String getDefaultText(){
return promptText.getPlainText();
}
/**
* Get the prompt text used in this control.
* @return the prompt text without styling
*/
public String getPromptText(){
return promptText.getPlainText();
}
/**
* Get the text in the control
* @return the text without styling
*/
public String getText(){
return stext.getPlainText();
}
/**
* Get the styled text in the control
* @return the text with styling
*/
public StyledString getStyledText(){
return stext;
}
/**
* Adds the text attribute to a range of characters on a particular line. If charEnd
* is past the EOL then the attribute will be applied to the end-of-line.
*
* @param attr the text attribute to add
* @param value value of the text attribute
* @param charStart the position of the first character to apply the attribute
* @param charEnd the position after the last character to apply the attribute
*/
public void addStyle(TextAttribute attr, Object value, int charStart, int charEnd){
if(stext != null){
stext.addAttribute(attr, value, charStart, charEnd);
bufferInvalid = true;
}
}
/**
* Adds the text attribute to a range of characters on a particular line. If charEnd
* is past the EOL then the attribute will be applied to the end-of-line.
*
* @param attr the text attribute to add
* @param value value of the text attribute
*/
public void addStyle(TextAttribute attr, Object value){
if(stext != null){
stext.addAttribute(attr, value);
bufferInvalid = true;
}
}
/**
* Clears all text attribute from a range of characters starting at position
* charStart and ending with the character preceding charEnd.
*
*
* @param charStart the position of the first character to apply the attribute
* @param charEnd the position after the last character to apply the attribute
*/
public void clearStyles(int charStart, int charEnd){
if(stext != null) {
stext.clearAttributes(charStart, charEnd);
bufferInvalid = true;
}
}
/**
* Clear all styles from the entire text.
*/
public void clearStyles(){
if(stext != null){
stext.clearAttributes();
bufferInvalid = true;
}
}
/**
* Set the font for this control.
* @param font
*/
public void setFont(Font font) {
if(font != null && font != localFont && buffer != null){
localFont = font;
buffer.g2.setFont(localFont);
stext.getLines(buffer.g2);
ptx = pty = 0;
setScrollbarValues(ptx, pty);
bufferInvalid = true;
}
}
// SELECTED / HIGHLIGHTED TEXT
/**
* Get the text that has been selected (highlighted) by the user. <br>
* @return the selected text without styling
*/
public String getSelectedText(){
if(!hasSelection())
return "";
TextLayoutHitInfo startSelTLHI;
TextLayoutHitInfo endSelTLHI;
if(endTLHI.compareTo(startTLHI) == -1){
startSelTLHI = endTLHI;
endSelTLHI = startTLHI;
}
else {
startSelTLHI = startTLHI;
endSelTLHI = endTLHI;
}
int ss = startSelTLHI.tli.startCharIndex + startSelTLHI.thi.getInsertionIndex();
int ee = endSelTLHI.tli.startCharIndex + endSelTLHI.thi.getInsertionIndex();
String s = stext.getPlainText().substring(ss, ee);
return s;
}
/**
* If some text has been selected then set the style. If there is no selection then
* the text is unchanged.
*
*
* @param style
*/
public void setSelectedTextStyle(TextAttribute style, Object value){
if(!hasSelection())
return;
TextLayoutHitInfo startSelTLHI;
TextLayoutHitInfo endSelTLHI;
if(endTLHI.compareTo(startTLHI) == -1){
startSelTLHI = endTLHI;
endSelTLHI = startTLHI;
}
else {
startSelTLHI = startTLHI;
endSelTLHI = endTLHI;
}
int ss = startSelTLHI.tli.startCharIndex + startSelTLHI.thi.getInsertionIndex();
int ee = endSelTLHI.tli.startCharIndex + endSelTLHI.thi.getInsertionIndex();
stext.addAttribute(style, value, ss, ee);
// We have modified the text style so the end of the selection may have
// moved, so it needs to be recalculated. The start will be unaffected.
stext.getLines(buffer.g2);
endSelTLHI.tli = stext.getTLIforCharNo(ee);
int cn = ee - endSelTLHI.tli.startCharIndex;
if(cn == 0) // start of line
endSelTLHI.thi = endSelTLHI.tli.layout.getNextLeftHit(1);
else
endSelTLHI.thi = endSelTLHI.tli.layout.getNextRightHit(cn-1);
bufferInvalid = true;
}
/**
* Clear any styles applied to the selected text.
*/
public void clearSelectionStyle(){
if(!hasSelection())
return;
TextLayoutHitInfo startSelTLHI;
TextLayoutHitInfo endSelTLHI;
if(endTLHI.compareTo(startTLHI) == -1){
startSelTLHI = endTLHI;
endSelTLHI = startTLHI;
}
else {
startSelTLHI = startTLHI;
endSelTLHI = endTLHI;
}
int ss = startSelTLHI.tli.startCharIndex + startSelTLHI.thi.getInsertionIndex();
int ee = endSelTLHI.tli.startCharIndex + endSelTLHI.thi.getInsertionIndex();
stext.clearAttributes(ss, ee);
// We have modified the text style so the end of the selection may have
// moved, so it needs to be recalculated. The start will be unaffected.
stext.getLines(buffer.g2);
endSelTLHI.tli = stext.getTLIforCharNo(ee);
int cn = ee - endSelTLHI.tli.startCharIndex;
if(cn == 0) // start of line
endSelTLHI.thi = endSelTLHI.tli.layout.getNextLeftHit(1);
else
endSelTLHI.thi = endSelTLHI.tli.layout.getNextRightHit(cn-1);
bufferInvalid = true;
}
/**
* Used internally to set the scrollbar values as the text changes.
*
* @param sx
* @param sy
*/
void setScrollbarValues(float sx, float sy){
if(vsb != null){
float sTextHeight = stext.getTextAreaHeight();
if(sTextHeight < th)
vsb.setValue(0.0f, 1.0f);
else
vsb.setValue(sy/sTextHeight, th/sTextHeight);
}
// If needed update the horizontal scrollbar
if(hsb != null){
float sTextWidth = stext.getMaxLineLength();
if(stext.getMaxLineLength() < tw)
hsb.setValue(0,1);
else
hsb.setValue(sx/sTextWidth, tw/sTextWidth);
}
}
/**
* Move caret to home position
* @param currPos the current position of the caret
* @return true if caret moved else false
*/
protected boolean moveCaretStartOfLine(TextLayoutHitInfo currPos){
if(currPos.thi.getCharIndex() == 0)
return false; // already at start of line
currPos.thi = currPos.tli.layout.getNextLeftHit(1);
return true;
}
/**
* Move caret to the end of the line that has the current caret position
* @param currPos the current position of the caret
* @return true if caret moved else false
*/
protected boolean moveCaretEndOfLine(TextLayoutHitInfo currPos){
if(currPos.thi.getCharIndex() == currPos.tli.nbrChars - 1)
return false; // already at end of line
currPos.thi = currPos.tli.layout.getNextRightHit(currPos.tli.nbrChars - 1);
return true;
}
/**
* Move caret left by one character.
* @param currPos the current position of the caret
* @return true if caret moved else false
*/
protected boolean moveCaretLeft(TextLayoutHitInfo currPos){
TextHitInfo nthi = currPos.tli.layout.getNextLeftHit(currPos.thi);
if(nthi == null){
return false;
}
else {
// Move the caret to the left of current position
currPos.thi = nthi;
}
return true;
}
/**
* Move caret right by one character.
* @param currPos the current position of the caret
* @return true if caret moved else false
*/
protected boolean moveCaretRight(TextLayoutHitInfo currPos){
TextHitInfo nthi = currPos.tli.layout.getNextRightHit(currPos.thi);
if(nthi == null){
return false;
}
else {
currPos.thi = nthi;
}
return true;
}
public void setJustify(boolean justify){
stext.setJustify(justify);
bufferInvalid = true;
}
/**
* Sets the local colour scheme for this control
*/
public void setLocalColorScheme(int cs){
super.setLocalColorScheme(cs);
if(hsb != null)
hsb.setLocalColorScheme(localColorScheme);
if(vsb != null)
vsb.setLocalColorScheme(localColorScheme);
}
/**
* Find out if some text is selected (highlighted)
* @return true if some text is selected else false
*/
public boolean hasSelection(){
return (startTLHI.tli != null && endTLHI.tli != null && startTLHI.compareTo(endTLHI) != 0);
}
/**
* Calculate the caret (text insertion point)
*
* @param tlhi
*/
protected void calculateCaretPos(TextLayoutHitInfo tlhi){
float temp[] = tlhi.tli.layout.getCaretInfo(tlhi.thi);
caretX = temp[0];
caretY = tlhi.tli.yPosInPara;
}
/**
* Determines whether the text can be edited using the keyboard or mouse. It
* still allows the text to be modified by the sketch code. <br>
* If text editing is being disabled and the control has focus then it is forced
* to give up that focus. <br>
* This might be useful if you want to use a GTextArea control to display large
* amounts of text that needs scrolling (so cannot use a GLabel) but must not
* change e.g. a user instruction guide.
*
* @param enableTextEdit false to disable keyboard input
*/
public void setTextEditEnabled(boolean enableTextEdit){
// If we are disabling this then make sure it does not have focus
if(enableTextEdit == false && focusIsWith == this){
loseFocus(null);
}
enabled = enableTextEdit;
textEditEnabled = enableTextEdit;
}
/**
* Is this control keyboard enabled
*/
public boolean isTextEditEnabled(){
return textEditEnabled;
}
public void keyEvent(KeyEvent e) {
if(!visible || !enabled || !textEditEnabled || !available) return;
if(focusIsWith == this && endTLHI != null){
char keyChar = e.getKey();
int keyCode = e.getKeyCode();
int keyID = e.getAction();
boolean shiftDown = e.isShiftDown();
boolean ctrlDown = e.isControlDown();
textChanged = false;
keepCursorInView = true;
int startPos = pos, startNbr = nbr;
// Get selection details
endChar = endTLHI.tli.startCharIndex + endTLHI.thi.getInsertionIndex();
startChar = (startTLHI != null) ? startTLHI.tli.startCharIndex + startTLHI.thi.getInsertionIndex() : endChar;
pos = endChar;
nbr = 0;
adjust = 0;
if(endChar != startChar){ // Have we some text selected?
if(startChar < endChar){ // Forward selection
pos = startChar; nbr = endChar - pos;
}
else if(startChar > endChar){ // Backward selection
pos = endChar; nbr = startChar - pos;
}
}
if(startPos >= 0){
if(startPos != pos || startNbr != nbr)
fireEvent(this, GEvent.SELECTION_CHANGED);
}
// Select either keyPressedProcess or keyTypeProcess. These two methods are overridden in child classes
if(keyID == KeyEvent.PRESS) {
keyPressedProcess(keyCode, keyChar, shiftDown, ctrlDown);
setScrollbarValues(ptx, pty);
}
else if(keyID == KeyEvent.TYPE ){ // && e.getKey() != KeyEvent.CHAR_UNDEFINED && !ctrlDown){
keyTypedProcess(keyCode, keyChar, shiftDown, ctrlDown);
setScrollbarValues(ptx, pty);
}
if(textChanged){
changeText();
fireEvent(this, GEvent.CHANGED);
}
}
}
// Enable polymorphism.
protected void keyPressedProcess(int keyCode, char keyChar, boolean shiftDown, boolean ctrlDown) { }
protected void keyTypedProcess(int keyCode, char keyChar, boolean shiftDown, boolean ctrlDown){ }
// Only executed if text has changed
protected boolean changeText(){
TextLayoutInfo tli;
TextHitInfo thi = null, thiRight = null;
pos += adjust;
// Force layouts to be updated
stext.getLines(buffer.g2);
// Try to get text layout info for the current position
tli = stext.getTLIforCharNo(pos);
if(tli == null){
// If unable to get a layout for pos then reset everything
endTLHI = null;
startTLHI = null;
ptx = pty = 0;
caretX = caretY = 0;
return false;
}
// We have a text layout so we can do something
// First find the position in line
int posInLine = pos - tli.startCharIndex;
// Get some hit info so we can see what is happening
try{
thiRight = tli.layout.getNextRightHit(posInLine);
}
catch(Exception excp){
thiRight = null;
}
if(posInLine <= 0){ // At start of line
thi = tli.layout.getNextLeftHit(thiRight);
}
else if(posInLine >= tli.nbrChars){ // End of line
thi = tli.layout.getNextRightHit(tli.nbrChars - 1);
}
else { // Character in line;
thi = tli.layout.getNextLeftHit(thiRight);
}
endTLHI.setInfo(tli, thi);
// Cursor at end of paragraph graphic
calculateCaretPos(endTLHI);
// // Is do we have to move cursor to start of next line
// if(newline) {
// if(pos >= stext.length()){
// stext.insertCharacters(pos, " ");
// stext.getLines(buffer.g2);
// }
// moveCaretRight(endTLHI);
// calculateCaretPos(endTLHI);
// }
// // Finish off by ensuring no selection, invalidate buffer etc.
// startTLHI.copyFrom(endTLHI);
// }
bufferInvalid = true;
return true;
}
/**
* Do not call this directly. A timer calls this method as and when required.
*/
public void flashCaret(GTimer timer){
showCaret = !showCaret;
}
/**
* Do not call this method directly, GUI uses it to handle input from
the horizontal scrollbar.
*/
public void hsbEventHandler(GScrollbar scrollbar, GEvent event){
keepCursorInView = false;
ptx = hsb.getValue() * (stext.getMaxLineLength() + 4);
bufferInvalid = true;
}
/**
* Do not call this method directly, GUI uses it to handle input from
the vertical scrollbar.
*/
public void vsbEventHandler(GScrollbar scrollbar, GEvent event){
keepCursorInView = false;
pty = vsb.getValue() * (stext.getTextAreaHeight() + 1.5f * stext.getMaxLineHeight());
bufferInvalid = true;
}
/**
* Permanently dispose of this control.
*/
public void markForDisposal(){
if(tabManager != null)
tabManager.removeControl(this);
super.markForDisposal();
}
/**
* Save the styled text used by this control to file. <br>
* It will also save the start and end position of any text selection.
*
* @param fname the name of the file to use
* @return true if saved successfully else false
*/
public boolean saveText(String fname){
if(stext == null)
return false;
if(hasSelection()){
stext.startIdx = startTLHI.tli.startCharIndex + startTLHI.thi.getInsertionIndex();
stext.endIdx = endTLHI.tli.startCharIndex + endTLHI.thi.getInsertionIndex();
}
else {
stext.startIdx = stext.endIdx = -1;
}
StyledString.save(winApp, stext, fname);
return true;
}
/**
* Load the styled string to be used by this control. <br>
* It will also restore any text selection saved with the text.
*
* @param fname the name of the file to use
* @return true if loaded successfully else false
*/
public boolean loadText(String fname){
StyledString ss = StyledString.load(winApp, fname);
if(ss == null)
return false;
setStyledText(ss);
// Now restore any text selection
if(stext.startIdx >=0){ // we have a selection
// Selection starts at ...
startTLHI = new TextLayoutHitInfo();
startTLHI.tli = stext.getTLIforCharNo(stext.startIdx);
int pInLayout = stext.startIdx - startTLHI.tli.startCharIndex;
if(pInLayout == 0)
startTLHI.thi = startTLHI.tli.layout.getNextLeftHit(1);
else
startTLHI.thi = startTLHI.tli.layout.getNextRightHit(pInLayout - 1);
// Selection ends at ...
endTLHI = new TextLayoutHitInfo();
endTLHI.tli = stext.getTLIforCharNo(stext.endIdx);
pInLayout = stext.endIdx - endTLHI.tli.startCharIndex;
if(pInLayout == 0)
endTLHI.thi = endTLHI.tli.layout.getNextLeftHit(1);
else
endTLHI.thi = endTLHI.tli.layout.getNextRightHit(pInLayout - 1);
calculateCaretPos(endTLHI);
}
bufferInvalid = true;
return true;
}
}