/* 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) 2014 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.HotSpot.HSrect; import automenta.vivisect.gui.StyledString.TextLayoutHitInfo; import automenta.vivisect.gui.StyledString.TextLayoutInfo; import java.awt.Font; import java.awt.Graphics2D; import java.awt.font.TextHitInfo; import java.awt.font.TextLayout; import java.awt.geom.GeneralPath; import java.util.LinkedList; import processing.core.PApplet; import processing.core.PGraphics; import processing.core.PGraphicsJava2D; import processing.event.KeyEvent; import processing.event.MouseEvent; /** * The password field component. <br> * * This control allows the user to secretly enter a password and supports an * optional horizontal scrollbar. <br> * * Each key typed will display a <b>#</b> character, but the user can specify another character. <br> * * Unlike a text field it does not support text selection or the copying and pasting of * text via the clipboard. <br> * * Fires CHANGED, ENTERED, LOST_FOCUS, GETS_FOCUS events.<br> * The focus events are only fired if the control is added to a GTabManager object. <br> * * @author Peter Lager * */ public final class GPassword extends GControl implements Focusable{ protected TextLayoutHitInfo cursorTLHI = new TextLayoutHitInfo(); private static char cover = '#'; private StyledString stext = new StyledString(" "); private StyledString hidden = new StyledString(" "); private int maxWordLength = 10; private int wordLength = 0; GTabManager tabManager = 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; protected Font localFont = GUI.globalFont; /** * Create a password field without a scrollbar. * * @param theApplet * @param p0 * @param p1 * @param p2 * @param p3 */ public GPassword(PApplet theApplet, float p0, float p1, float p2, float p3) { this(theApplet, p0, p1, p2, p3, SCROLLBARS_NONE); } /** * Create a password field with the given scrollbar policy. <br> * This policy can be one of these <br> * <ul> * <li>SCROLLBARS_NONE</li> * <li>SCROLLBARS_HORIZONTAL_ONLY</li> * </ul> * If you want the scrollbar to auto hide then perform a logical or with * <ul> * <li>SCROLLBARS_AUTOHIDE</li> * </ul> * e.g. SCROLLBARS_HORIZONTAL_ONLY | SCROLLBARS_AUTOHIDE * <br> * @param theApplet * @param p0 * @param p1 * @param p2 * @param p3 * @param sbPolicy */ public GPassword(PApplet theApplet, float p0, float p1, float p2, float p3, int sbPolicy) { super(theApplet, p0, p1, p2, p3); scrollbarPolicy = sbPolicy; autoHide = ((sbPolicy & SCROLLBARS_AUTOHIDE) == SCROLLBARS_AUTOHIDE); caretFlasher = new GTimer(theApplet, this, "flashCaret", 400); caretFlasher.start(); opaque = true; cursorOver = TEXT; setVisibleChar(cover); children = new LinkedList<GControl>(); tx = ty = 2; tw = width - 2 * 2; th = height - ((scrollbarPolicy & SCROLLBAR_HORIZONTAL) != 0 ? 11 : 0); gpTextDisplayArea = new GeneralPath(); gpTextDisplayArea.moveTo( 0, 0); gpTextDisplayArea.lineTo( 0, th); gpTextDisplayArea.lineTo(tw, th); gpTextDisplayArea.lineTo(tw, 0); gpTextDisplayArea.closePath(); // // The image buffer is just for the typing area // buffer = (PGraphicsJava2D) winApp.createGraphics((int)width, (int)height, PApplet.JAVA2D); // buffer.rectMode(PApplet.CORNER); // buffer.g2.setFont(localFont); hotspots = new HotSpot[]{ new HSrect(1, tx, ty, tw, th), // typing area new HSrect(9, 0, 0, width, height) // control surface }; GUI.pushStyle(); GUI.showMessages = false; z = Z_STICKY; GUI.control_mode = GControlMode.CORNER; if((scrollbarPolicy & SCROLLBAR_HORIZONTAL) != 0){ hsb = new GScrollbar(theApplet, 0, 0, tw, 10); add(hsb, tx, ty + th + 2, 0); hsb.addEventHandler(this, "hsbEventHandler"); hsb.setAutoHide(autoHide); } GUI.popStyle(); // z = Z_STICKY; createEventHandler(GUI.applet, "handlePasswordEvents", new Class<?>[]{ GPassword.class, GEvent.class }, new String[]{ "pwordControl", "event" } ); registeredMethods = PRE_METHOD | DRAW_METHOD | MOUSE_METHOD | KEY_METHOD; GUI.addControl(this); } /** * Set the character that will be displayed instead of the actual character * entered by the user. <br> * Default value is '#' */ public void setVisibleChar(char c){ int ascii = (int) c; if((ascii >= 33 && ascii <= 255 && ascii != 127) || ascii == 8364) cover = c; } /** * Get the current password (hidden) value of this field. * @return actual password text */ public String getPassword(){ String password = hidden.getPlainText(); return password.equals(" ") ? "" : password; } /** * Get the current length of the password entered. */ public int getWordLength(){ return wordLength; } /** * Sets the max length of the password. This method is ignored if the control * already holds some user input. <br> * The default value is 10. * @param ml the new max length (must be >= 1) */ public void setMaxWordLength(int ml){ if(wordLength == 0 && ml >= 1) maxWordLength = ml; } /** * Set the font to be used in this control * * @param font AWT font to use */ public void setFont(Font font) { if(font != null && font != localFont && buffer != null){ localFont = font; buffer.g2.setFont(localFont); bufferInvalid = true; } } public PGraphics getSnapshot(){ updateBuffer(); PGraphicsJava2D snap = (PGraphicsJava2D) winApp.createGraphics(buffer.width, buffer.height, PApplet.JAVA2D); snap.beginDraw(); snap.image(buffer,0,0); if(hsb != null){ snap.pushMatrix(); snap.translate(hsb.getX(), hsb.getY()); snap.image(hsb.getBuffer(), 0, 0); snap.popMatrix(); } snap.endDraw(); return snap; } public void pre(){ if(keepCursorInView){ boolean horzScroll = false; float max_ptx = caretX - tw + 2; if(endTLHI != null){ if(ptx > caretX){ // Scroll to the left (text moves right) ptx -= HORZ_SCROLL_RATE; if(ptx < 0) ptx = 0; horzScroll = true; } else if(ptx < max_ptx){ // Scroll to the right (text moves left)? ptx += HORZ_SCROLL_RATE; if(ptx > max_ptx) ptx = max_ptx; horzScroll = true; } // Ensure that we show as much text as possible keeping the caret in view // This is particularly important when deleting from the end of the text if(ptx > 0 && endTLHI.tli.layout.getAdvance() - ptx < tw - 2){ ptx = Math.max(0, endTLHI.tli.layout.getAdvance() - tw - 2); horzScroll = true; } if(horzScroll && hsb != null) hsb.setValue(ptx / (stext.getMaxLineLength() + 4)); } // If we have scrolled invalidate the buffer otherwise forget it if(horzScroll) bufferInvalid = true; else keepCursorInView = false; } } /** * Do not call this directly. A timer calls this method as and when required. */ public void flashCaret(GTimer timer){ showCaret = !showCaret; } public void mouseEvent(MouseEvent event){ if(!visible || !enabled || !available) return; calcTransformedOrigin(winApp.getCursorX(), winApp.getCursorY()); ox -= tx; oy -= ty; // Remove translation currSpot = whichHotSpot(ox, oy); if(currSpot == 1 || focusIsWith == this) cursorIsOver = this; else if(cursorIsOver == this) cursorIsOver = null; switch(event.getAction()){ case MouseEvent.PRESS: if(currSpot == 1){ if(focusIsWith != this && z >= focusObjectZ()){ keepCursorInView = true; takeFocus(); } dragging = false; if(stext == null || stext.length() == 0){ stext = new StyledString(" ", wrapWidth); stext.getLines(buffer.g2); } endTLHI = stext.calculateFromXY(buffer.g2, ox + ptx, oy + pty); startTLHI = new TextLayoutHitInfo(endTLHI); calculateCaretPos(endTLHI); bufferInvalid = true; } else { // Not over this control so if we have focus loose it if(focusIsWith == this) loseFocus(null); } break; case MouseEvent.RELEASE: dragging = false; bufferInvalid = true; break; } } public void keyEvent(KeyEvent e) { if(!visible || !enabled || !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); } } } protected void keyPressedProcess(int keyCode, char keyChar, boolean X, boolean ctrlDown){ boolean cursorMoved = true; switch(keyCode){ case LEFT: moveCaretLeft(endTLHI); break; case RIGHT: moveCaretRight(endTLHI); break; case GConstants.HOME: moveCaretStartOfLine(endTLHI); break; case GConstants.END: moveCaretEndOfLine(endTLHI); break; default: cursorMoved = false; } if(cursorMoved){ calculateCaretPos(endTLHI); startTLHI.copyFrom(endTLHI); } } protected void keyTypedProcess(int keyCode, char keyChar, boolean shiftDown, boolean ctrlDown){ int ascii = (int)keyChar; //if((wordLength < maxWordLength && ascii >= 32 && ascii <= 255 && ascii != 127) || ascii == 8364){ if(wordLength < maxWordLength && isDisplayable(ascii)){ stext.insertCharacters( "" + cover, pos); hidden.insertCharacters("" + keyChar, pos); wordLength++; adjust = 1; textChanged = true; } else if(keyChar == BACKSPACE){ if(stext.deleteCharacters(pos - 1, 1)){ hidden.deleteCharacters(pos - 1, 1); wordLength = --wordLength < 0 ? 0: wordLength; adjust = -1; textChanged = true; } } else if(keyChar == DELETE){ if(stext.deleteCharacters(pos, 1)){ hidden.deleteCharacters(pos, 1); wordLength--; adjust = 0; textChanged = true; } } else if(keyChar == ENTER || keyChar == RETURN) { fireEvent(this, GEvent.ENTERED); // If we have a tab manager and can tab forward then do so if(tabManager != null && tabManager.nextControl(this)){ startTLHI.copyFrom(endTLHI); return; } } else if(keyChar == TAB){ // If possible move to next text control if(tabManager != null){ boolean result = (shiftDown) ? tabManager.prevControl(this) : tabManager.nextControl(this); if(result){ startTLHI.copyFrom(endTLHI); return; } } } // If we have emptied the text then recreate a one character string (space) if(stext.length() == 0){ stext.insertCharacters(" ", 0); hidden.insertCharacters(" ", 0); adjust++; textChanged = true; } } 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); bufferInvalid = true; startTLHI.copyFrom(endTLHI); return 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); } } public void draw(){ if(!visible) return; updateBuffer(); winApp.pushStyle(); winApp.pushMatrix(); applyTransform(); winApp.pushMatrix(); // Move matrix to line up with top-left corner winApp.translate(-halfWidth, -halfHeight); // Draw buffer winApp.imageMode(PApplet.CORNER); if(alphaLevel < 255) winApp.tint(TINT_FOR_ALPHA, alphaLevel); winApp.image(buffer, 0, 0); // Draw caret if text display area if(focusIsWith == this && showCaret && endTLHI.tli != null){ float[] cinfo = endTLHI.tli.layout.getCaretInfo(endTLHI.thi); float x_left = - ptx + cinfo[0]; float y_top = - pty + endTLHI.tli.yPosInPara; float y_bot = y_top - cinfo[3] + cinfo[5]; if(x_left >= 0 && x_left <= tw && y_top >= 0 && y_bot <= th){ winApp.strokeWeight(1.9f); winApp.stroke(palette[15]); winApp.line(tx+x_left, ty+Math.max(0, y_top), tx+x_left, ty+Math.min(th, y_bot)); } } winApp.popMatrix(); if(children != null){ for(GControl c : children) c.draw(); } winApp.popMatrix(); winApp.popStyle(); } /** * If the buffer is invalid then redraw it. * @TODO need to use palette for colours */ protected void updateBuffer(){ if(bufferInvalid) { Graphics2D g2d = buffer.g2; // Get the latest lines of text LinkedList<TextLayoutInfo> lines = stext.getLines(g2d); bufferInvalid = false; buffer.beginDraw(); // Whole control surface if opaque if(opaque) buffer.background(palette[6]); else buffer.background(buffer.color(255,0)); // Now move to top left corner of text display area buffer.translate(tx,ty); // Typing area surface buffer.noStroke(); buffer.fill(palette[7]); buffer.rect(-1,-1,tw+2,th+2); g2d.setClip(gpTextDisplayArea); buffer.translate(-ptx, -pty); // Translate in preparation for display selection and text // Display selection and text for(TextLayoutInfo lineInfo : lines){ TextLayout layout = lineInfo.layout; buffer.translate(0, layout.getAscent()); // Draw text g2d.setColor(jpalette[2]); lineInfo.layout.draw(g2d, 0, 0); buffer.translate(0, layout.getDescent() + layout.getLeading()); } g2d.setClip(null); buffer.endDraw(); } } /** * 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); startTLHI.copyFrom(endTLHI); calculateCaretPos(endTLHI); bufferInvalid = true; } keepCursorInView = true; takeFocus(); } /** * 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; } /** * 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); } @Override public void setTabManager(GTabManager tm){ tabManager = tm; } }