/********************************************************************************* * TotalCross Software Development Kit * * Copyright (C) 2001 Jean Rissoto * * Copyright (C) 2001-2012 SuperWaba Ltda. * * All Rights Reserved * * * * This library and virtual machine 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. * * * * This file is covered by the GNU LESSER GENERAL PUBLIC LICENSE VERSION 3.0 * * A copy of this license is located in file license.txt at the root of this * * SDK or can be downloaded here: * * http://www.gnu.org/licenses/lgpl-3.0.txt * * * *********************************************************************************/ package totalcross.ui; import totalcross.sys.*; import totalcross.ui.dialog.*; import totalcross.ui.event.*; import totalcross.ui.gfx.*; import totalcross.ui.image.*; import totalcross.util.*; /** * MultiEdit is an Edit with support for multiple lines. A static scrollbar is added, but disabled/enabled as needed. * <p> * Here is an example showing an Edit control being used: * * <pre> * import totalcross.ui.*; * * public class MyProgram extends MainWindow * { * MultiEdit mEdit; * * public void initUI() * { * // the constructor method is called with the mask, the number of lines * // and the vertical interval in pixel between two lines * mEdit = new MultiEdit("",3,1); * add(mEdit,LEFT,TOP); * // add/setRect must precede setText * mEdit.setText("What text you want"); // eventually * } * } * </pre> * If the MultiEdit is not editable, the user can scroll the edit a page at a time * just by clicking in the middle upper or middle lower. * @author Jean Rissoto (in memoriam) * @author Guilherme Campos Hazan (guich) */ public class MultiEdit extends Container implements Scrollable { private static final char ENTER = '\n'; private static final char LINEFEED = '\r'; private int lastZ1y = -9999; private TimerEvent blinkTimer; // only valid while the edit has focus -- original private boolean hasFocus; private boolean cursorShowing; boolean firstPenDown; protected boolean editable = true; /** Set to false if you don't want the cursor to blink when the edit is not editable */ public static boolean hasCursorWhenNotEditable = true; // guich@340_23 /** Set to true if you want the control to decide whether to gain/lose focus automatically, without having to press ACTION. */ protected boolean improvedGeographicalFocus; /** The last insert position before this control looses focus. */ public int lastInsertPos; protected IntVector first = new IntVector(5); //JR@0.4. indices of first character of each line. the value of last is len(text)+1 private int firstToDraw; //JR@0.4 private int numberTextLines; // JR@0.4 private int hLine; // JR@0.4. height of a line private Coord z1 = new Coord(), z2 = new Coord(); private Coord z3 = new Coord(); private Rect boardRect; private Rect textRect; protected StringBuffer chars = new StringBuffer(100); private int insertPos; private int startSelectPos; private int newInsertPos; private int pushedInsertPos; //protected in Edit private int pushedStartSelectPos; //protected in Edit protected ScrollBar sb; private int spaceBetweenLines;// private int tempGap, tempRowCount, tempRowCount0; // used on popup keyboard private int fColor,back0,back1; //JR@0.8 private int fourColors[] = new int[4]; //JR@0.8 private byte kbdType=Edit.KBD_KEYBOARD; private byte lastKbdType=Edit.KBD_KEYBOARD; //vik@421_24 private String validChars; private int maxLength; // guich@200b4 private int dragDistance; private boolean isScrolling; private boolean popupVKbd; /** While using geographical focus, editMode is toggled using the Action Key to allow navigation * and editing of the text inside the MultiEdit **/ private boolean editMode = true; private boolean editModeValue = true; // guich@tc115_6 private String mapFrom,mapTo; // guich@tc110_56 private int oldTabIndex=-1; private boolean ignoreNextFocusIn; private Image npback; private int rowCount0=-1; private boolean scScrolled; int lastPenDown=-1; private static KeyEvent backspaceEvent = new KeyEvent(KeyEvent.SPECIAL_KEY_PRESS,SpecialKeys.BACKSPACE,0); private boolean scrollBarsAlwaysVisible; /** The mask used to infer the preferred width. Unlike the Edit class, the MultiEdit does not support real masking. */ public String mask; /** Used to set the number of rows of this MultiEdit; used as parameter to compute the preferred height. * You must call setRect after changing this to resize the control in height. */ public int rowCount; // guich@320_28 /** Sets the capitalise settings for this MultiEdit. Text entered will made as is, uppercase or * lowercase * @see totalcross.ui.Edit#ALL_NORMAL * @see totalcross.ui.Edit#ALL_UPPER * @see totalcross.ui.Edit#ALL_LOWER */ public byte capitalise; /** If set to true, the text will be auto-selected when the focus enters. * True by default on penless devices. */ public boolean autoSelect = Settings.keyboardFocusTraversable; // guic@tc130 /** If true, a dotted line appears under each row of text (on by default) */ public boolean drawDots = true; /** The gap between the rows. */ public int gap=1; // guich@320_28: made public to remove 2 almost unused methods /** Set to true to justify the text when the MultiEdit is NOT editable. * Note that this makes the text drawing a bit slower. */ public boolean justify; /** The Flick object listens and performs flick animations on PenUp events when appropriate. */ protected Flick flick; /** Constructs a MultiEdit with 1 pixel as space between lines and with no lines. * You must set the bounds using FILL or FIT. */ public MultiEdit() { this(0, 1); } /** Constructor for a text Edit with a vertical scroll Bar, gap is 1 by default and control's bounds must be specified with a setRect. Space between lines may be 0. */ public MultiEdit(int rowCount, int spaceBetweenLines) // vertical space between 2 lines in pixel { ignoreOnAddAgain = ignoreOnRemove = true; this.started = true; this.rowCount = rowCount; this.hLine = fmH + spaceBetweenLines; this.spaceBetweenLines = spaceBetweenLines; this.clearPosState(); add(this.sb = Settings.fingerTouch ? new ScrollPosition(ScrollBar.VERTICAL) : new ScrollBar(ScrollBar.VERTICAL)); if (!Settings.fingerTouch) { sb.setLiveScrolling(true); // don't let the scrollbar steal focus from us sb.setEnabled(false); // gao - leave this disabled for visual effect until we know we need it sb.setFocusLess(true); sb.focusTraversable = false; sb.setVisible(false); } this.focusTraversable = true; // kmeehl@tc100 if (Settings.fingerTouch) flick = new Flick(this); } /** Constructor for a text Edit with a vertical scroll Bar, gap is 1 * by default and control's bounds must be given with a setRect. Space between lines may be 0. * The mask is used to compute the PREFERRED width of the control. Note that the mask does not <i>masks</i> * the input. If mask is "", the FILL width is choosen. */ public MultiEdit(String mask, int rowCount, int spaceBetweenLines) { this(rowCount, spaceBetweenLines); this.mask = mask; } public boolean flickStarted() { dragDistance = 0; return isScrolling; } public void flickEnded(boolean atPenDown) { } public boolean canScrollContent(int direction, Object target) { if (Settings.fingerTouch) switch (direction) { case DragEvent.UP: return firstToDraw > 0; case DragEvent.DOWN: return (firstToDraw + rowCount) < numberTextLines; } return false; } public boolean scrollContent(int xDelta, int yDelta, boolean fromFlick) { if (Math.abs(xDelta) > Math.abs(yDelta)) // MultiEdit has only vertical scrolling return false; int lastFirstToDrawLine = numberTextLines - rowCount; if (lastFirstToDrawLine <= 0 || (yDelta < 0 && firstToDraw == 0) || (yDelta > 0 && firstToDraw >= lastFirstToDrawLine)) // already at the top/bottom of the view window return false; dragDistance += yDelta; if ((dragDistance < 0 && dragDistance > -hLine) || (dragDistance >= 0 && dragDistance < hLine)) // not enough to move one single line, store accumulated increment and return return true; int lineDelta = dragDistance / hLine; dragDistance %= hLine; firstToDraw += lineDelta; if (firstToDraw < 0) firstToDraw = 0; else if (firstToDraw > lastFirstToDrawLine) firstToDraw = lastFirstToDrawLine; sb.setValue(firstToDraw); if (!fromFlick) sb.tempShow(); newInsertPos = zToCharPos(z1); Window.needsPaint = true; return true; } public int getScrollPosition(int direction) { if (direction == DragEvent.LEFT || direction == DragEvent.RIGHT) return 0; return dragDistance; } /** Maps the keys in the from char array into the keys in the to char array. For example enable a 'numeric pad' * on devices that has the 1 in the u character, you can use this: * <pre> * ed.mapKeys("uiojklnm!.","1234567890"); * </pre> * To make sure that lowercase characters are also handled, you should also change the capitalise mode: * <pre> * ed.capitalise = Edit.ALL_LOWER; * </pre> * If you want to disable a set of keys, use the setValidChars method. Note that mapKeys have precendence over setValidChars. * @param from The source keys. Must have the same length of <code>to</code>. Set to null to disable mapping. * @param to The destination keys. Must have the same length of <code>from</code> * @since TotalCross 1.01 * @see #setValidChars(String) */ public void mapKeys(String from, String to) { if (from == null || to == null) from = to = null; else if (from.length() != to.length()) throw new IllegalArgumentException("from.length must match to.length"); this.mapFrom = from; this.mapTo = to; } public int getPreferredHeight() { if (rowCount0 == -1) rowCount0 = rowCount; return (hLine*rowCount0+ (uiFlat?2:4) + 2*gap) + insets.top+insets.bottom; //+2= minimal space between 2 lines } public int getPreferredWidth() { return (mask==null?(totalcross.sys.Settings.screenWidth>>2):(mask.length()==0)?FILL:(fm.stringWidth(mask) + 10)) + insets.left+insets.right; // guich@200b4_202: from 2 -> 4 is PalmOS style - guic@300_52: empty mask means FILL } /** Sets the desired maximum length for text entered in the Edit. * @since SuperWaba 2.0 beta 4 */ public void setMaxLength(int length) { maxLength = length; if (length != 0 && maxLength < chars.length()) // jescoto@421_15: resize text if maxLength < len chars.setLength(length); } /** Used to change the default keyboard to be used with this Edit control. * Use the constants Edit.KBD_NONE and Edit.KBD_KEYBOARD. */ public void setKeyboard(byte kbd) { this.lastKbdType=this.kbdType = kbd == Edit.KBD_NONE ? Edit.KBD_NONE : Edit.KBD_KEYBOARD; //vik@421_24 } /** sets the valid chars that can be entered in this edit. if null is passed, any char can be entered. (case insensitive). */ public void setValidChars(String validCharsString) { if (validCharsString != null) validChars = validCharsString.toUpperCase(); else validChars = null; } /** * Returns the text displayed in the edit control. */ public String getText() { return chars.toString(); } /** Returns the text's buffer. Do NOT change the buffer contents, since changing it * will not affect the char widths array, thus, leading to a wrong display. * @since TotalCross 1.0 */ public StringBuffer getTextBuffer() { return chars; } /** * Sets the text displayed in the edit control. */ public void setText(String s) { setText(s,Settings.sendPressEventOnChange); } /** * Sets the text displayed in the edit control. */ public void setText(String s, boolean postPressed) { chars = new StringBuffer(Convert.replace(s, Convert.CRLF,"\n")); newInsertPos = numberTextLines = 0; if (textRect != null) calculateFirst(); clearPosState(); if (postPressed) postPressedEvent(); } /** Sets if the control accepts input from the user. Note: If set to uneditable, keyboard is disabled. */ public void setEditable(boolean on) { editable = on; kbdType = on ? this.lastKbdType:Edit.KBD_NONE; //vik@421_24 } /** Gets if the control accepts input from the user */ public boolean isEditable() { return editable; } /** Gets total number of lines in the text */ public int getNumberOfTextLines() { return numberTextLines; } /** Set to true to hide the vertical scrollbar when it isn't needed (instead of disabling it). * This must be done right after the constructor. * @since TotalCross 1.0 */ public void setScrollbarsAlwaysVisible(boolean asNeeded) { scrollBarsAlwaysVisible = asNeeded; if (!Settings.fingerTouch) sb.setVisible(asNeeded); } /** user method to popup the keyboard/calendar/calculator for this edit. */ public void popupKCC() { if (kbdType == Edit.KBD_NONE || !editable || !isEnabled()) return; if (Settings.virtualKeyboard) _onEvent(new Event(ControlEvent.FOCUS_IN,this,0)); // simulate a focus in event. else { if (Edit.keyboard == null) Edit.keyboard = new KeyboardBox(); showInputWindow(Edit.keyboard); } } private void showInputWindow(Window w) { oldTabIndex = parent.tabOrder.indexOf(this); requestFocus(); // guich@200b4: bring focus back pushPosState(); if (removeTimer(blinkTimer)) // guich@200b4_167 blinkTimer = null; // guich@320: modify and restore later our state, bc the Keyboard needs a shrinked control tempGap = gap; tempRowCount = rowCount; tempRowCount0 = rowCount0; gap = 0; rowCount0 = rowCount = 2; w.popupNonBlocking(); popPosState(); requestFocus(); } private int zToCharPos(Coord z) { z.y = Math.max(0, Math.min(this.height-1, z.y)); int line = firstToDraw + (z.y - textRect.y) / hLine; // what's the line? if (line >= numberTextLines) line = Math.max(numberTextLines-1,0); z.x = Math.max(0, Math.min(fm.sbWidth(chars,first.items[line],first.items[line+1]-first.items[line]), z.x)); return Convert.getBreakPos(fm, chars, first.items[line], z.x, false); } private void charPosToZ(int n, Coord z) { z.x = textRect.x; z.y = textRect.y; int len = chars.length(); if (len == 0 || n == 0) // no string or pos 0? return; if (n > len) n = len; int i,mid,end = first.size(); for (i = 0;end-i > 1 ;) // kmeehl@tc100: compute the new line using binary search { mid = i + (end-i)/2; if (n > first.items[mid]) i = mid; else end = mid; } //if (n == first.items[i+1]) i++; else - if char is at last space, put it in the next line - note that this doesn't work bc when pressing down key it skips 2 lines z.x += fm.sbWidth(chars,first.items[i],n-first.items[i]); z.y += (i-firstToDraw) * hLine; } protected void onBoundsChanged(boolean screenChanged) { int zOffset = uiFlat?0:2; // size of borders boardRect = new Rect(zOffset,zOffset,this.width-2*zOffset-(Settings.fingerTouch?0:sb.getPreferredWidth()),this.height-2*zOffset); //JR @0.5 textRect = boardRect.modifiedBy(gap,gap,-2*gap,-2*gap); rowCount = textRect.height / this.hLine; // kambiz@350_5: update rowCount according to the new size of the text area sb.setRect(RIGHT-(Settings.fingerTouch ? 1 : 0),TOP,PREFERRED,FILL, null, screenChanged); sb.setValues(0, rowCount, 0, rowCount); numberTextLines = 0; firstToDraw = 0; if (chars.length() > 0) calculateFirst(); npback = null; } /** Compute the index of the first character of each line */ private void calculateFirst() // guich@320_28: completely redesigned - guich@581_6: highly optimized { StringBuffer chars = this.chars; // cache int i=0, originalLineCount = numberTextLines; first.removeAllElements(); first.addElement(0); // in line 0, the first char is always 0 int tw = textRect.width; int n = chars.length(); int pos = 0; for (; pos < n; pos++) { int pos0 = pos == 0 || chars.charAt(pos-1) < ' ' ? pos : pos-1; // guich@tc113_37: when parsing "Update of /pcvsroot/LitebaseSDK/src/native/parser", it was breaking in the first /, but in the next loop iteration, it was skipping the first /, and, thus, computing a character less first.addElement(pos = Convert.getBreakPos(fm, chars, pos0, tw, true)); // guich@tc166: we'll take care of the initial space/ENTER during drawing } first.addElement(n); numberTextLines = first.size()-1; //try {for (i =0; i <= numberTextLines; i++) Vm.debug("first["+i+"]: "+first.items[i]+" '"+chars.charAt(first.items[i])+"'");} catch (Exception e) {Vm.debug("first["+i+"]: "+first.items[i]);} // has the number of lines changed? enable/disable scrollbar if (numberTextLines != originalLineCount) { boolean needScroll = numberTextLines > rowCount; if (!Settings.fingerTouch) { sb.setEnabled(needScroll); // gao always visually enable / disable based on needScroll if (scrollBarsAlwaysVisible) sb.setVisible(true); else // gao make sure its enabled and visible only when needed sb.setVisible(needScroll); } sb.setMaximum(needScroll ? numberTextLines : 0); } // compute the new line of the cursor - kmeehl@tc100 changed a bit int end = first.size(); int mid; for(i = 0; end-i > 1 ;) // kmeehl - compute the new line using binary search { mid = i + (end-i)/2; if (newInsertPos > first.items[mid]) i = mid; else end = mid; } // change position only if the cursor is out of viewable area if (i < firstToDraw) firstToDraw = i; else if (i >= firstToDraw + rowCount) //bruno@tc114_47: fixed scrolling - when typing, the last line was being omitted { firstToDraw = i - rowCount + 1; if (firstToDraw < 0) firstToDraw = 0; } // need to change scrollbar position? if (sb.getValue() != firstToDraw) sb.setValue(firstToDraw+1); } private void focusOut() { if (Settings.virtualKeyboard && Settings.isWindowsDevice() && editable && kbdType != Edit.KBD_NONE) // if running on a PocketPC device, set the bounds of Sip in a way to not cover the edit { Window.isSipShown = false; Window.setSIP(Window.SIP_HIDE,null,false); } hasFocus = false; // see what to do when popup if (removeTimer(blinkTimer)) blinkTimer = null; if (cursorShowing) // kambisDarabi@310_7 : remove the cursor, if it is currently shown Window.needsPaint = true; hasFocus = false; Window w = getParentWindow(); if ((Settings.keyboardFocusTraversable || Settings.geographicalFocus) && w != null && w == Window.getTopMost()) // guich@tc110_81: remove highlight from us. - guich@tc120_39: only if we're in the topmost window { //parent.requestFocus(); - guich@tc115_91 if (w.getFocus() == this) // parent didn't get focus w.removeFocus(); w.setHighlighted(this); } } /** Called by the system to pass events to the edit control. */ public void onEvent(Event event) { if (event.type == PenEvent.PEN_DOWN) scScrolled = false; if (event.target == this && textRect != null) { boolean redraw = false; boolean extendSelect = false; boolean clearSelect = false; newInsertPos = insertPos; switch (event.type) { case TimerEvent.TRIGGERED: if (blinkTimer != null && !isTopMost()) // must check here and not in the onPaint method, otherwise it results in a problem: show an edit field, then popup a window and move it: the edit field of the other window is no longer being drawn { focusOut(); event.consumed = true; return; } if (parent != null && (editMode || Settings.fingerTouch)) Window.needsPaint = true; // guich@tc130: show the copy/paste menu if (editable && isEnabled() && lastPenDown != -1 && Edit.clipboardDelay != -1 && (Vm.getTimeStamp() - lastPenDown) >= Edit.clipboardDelay) if (showClipboardMenu()) { event.consumed = true; // astein@230_5: prevent blinking cursor event from propagating break; } event.consumed = true; // astein@230_5: prevent blinking cursor event from propagating return; case ControlEvent.FOCUS_IN: firstPenDown = true; if (Settings.geographicalFocus) editMode = editModeValue || improvedGeographicalFocus; // kmeehl@tc100 // guich@300_43: this is needed bc when popupKCC is called, the focus comes back to here; also, when the // popped up window is closed, the focus comes back again, so we could enter in an infinite loop if (ignoreNextFocusIn) // guich@tc126_21 ignoreNextFocusIn = false; else if (!Settings.fingerTouch) showSip(); // guich@tc126_21 hasFocus = true; if (blinkTimer == null && (editable || hasCursorWhenNotEditable)) blinkTimer = addTimer(350); break; case ControlEvent.FOCUS_OUT: focusOut(); break; case KeyEvent.KEY_PRESS: case KeyEvent.SPECIAL_KEY_PRESS: if (editable && isEnabled()) { KeyEvent ke = (KeyEvent) event; if (event.type == KeyEvent.SPECIAL_KEY_PRESS && ke.key == SpecialKeys.ESCAPE) event.consumed = true; // don't let the back key be passed to the parent if (ke.key == SpecialKeys.ACTION && (Settings.isWindowsDevice() || Settings.platform.equals(Settings.WIN32))) // guich@tc122_22: in WM, the ACTION key is mapped to the ENTER. so we revert it here ke.key = SpecialKeys.ENTER; if ((ke.key == SpecialKeys.ACTION || ke.key == SpecialKeys.ESCAPE) && !improvedGeographicalFocus) { //isHighlighting = true; // kmeehl@tc100: set isHighlighting first, so that Window.removeFocus() wont trample Window.highlighted - guich@tc110_81: commented out. this will be done in focusOut(). focusOut(); // remove the cursor return; } if (!editMode) break; // kmeehl@tc100 int len = chars.length(); if (editable) { if (ke.key == 0) return; // guich@402_41: sometimes, the left key causes a zero key being entered, crashing everything if (ke.key == LINEFEED) // guich@tc100: ignore \r\n, so we don't have to keep checking for both. break; if ((ke.key == SpecialKeys.KEYBOARD_ABC || ke.key == SpecialKeys.KEYBOARD_123) && (Edit.keyboard == null || !Edit.keyboard.isVisible())) { popupKCC(); return; } boolean moveFocus = !Settings.geographicalFocus && ke.key == SpecialKeys.TAB; if (event.target == this && moveFocus) // guich@tc125_26 { if (parent != null && parent.moveFocusToNextEditable(this, ke.modifiers == 0) != null) return; } // if ((Settings.keyboardFocusTraversable || Settings.geographicalFocus) && (ke.key == SpecialKeys.ESCAPE || ke.key == SpecialKeys.MENU)) boolean isControl = (ke.modifiers & SpecialKeys.CONTROL) != 0; // guich@320_46 - guich@tc100b4_25: also check for the type of event, otherwise the arrow keys won't work boolean isPrintable = ke.key > 0 && (ke.modifiers & SpecialKeys.ALT) == 0 && (ke.modifiers & SpecialKeys.CONTROL) == 0 && event.type == KeyEvent.KEY_PRESS; boolean isDelete = (ke.key == SpecialKeys.DELETE); boolean isBackspace = (ke.key == SpecialKeys.BACKSPACE); boolean isEnter = (ke.key == SpecialKeys.ENTER); int del1 = -1; int del2 = -1; int sel1 = startSelectPos; int sel2 = insertPos; if (sel1 > sel2) { int temp = sel1; sel1 = sel2; sel2 = temp; } // clipboard -- original if (isControl) { if (isControl) { if (0 < ke.key && ke.key < 32) ke.key += 64; ke.modifiers &= ~SpecialKeys.CONTROL; // remove control } char key = Convert.toUpperCase((char) ke.key); switch (key) { case 'X': clipboardCut(); return; case 'C': clipboardCopy(); return; case ' ': setText(""); return; case 'P': case 'V': clipboardPaste(); break; } clearSelect = true; // break; } if (mapFrom != null) // guich@tc110_56 { int idx = mapFrom.indexOf(Convert.toLowerCase((char)ke.key)); if (idx != -1) ke.key = mapTo.charAt(idx); } if (isPrintable) { if (capitalise == Edit.ALL_NORMAL) ; else if (capitalise == Edit.ALL_UPPER) ke.key = Convert.toUpperCase((char) ke.key); else if (capitalise == Edit.ALL_LOWER) ke.key = Convert.toLowerCase((char) ke.key); if (!isCharValid((char) ke.key)) // guich@101: tests if the key is in the valid char set - moved to here because a valid clipboard char can be an invalid edit char break; } if (sel1 != -1 && (isPrintable || isDelete || isBackspace)) { del1 = sel1; del2 = sel2 - 1; } else if (isDelete) { del1 = insertPos; del2 = insertPos; } else if (isBackspace) { del1 = insertPos - 1; del2 = insertPos - 1; } if (isEnter) { ke.key = ENTER; isPrintable = true; } if (del1 >= 0 && del2 < len) { if (len > del2 - 1) chars.delete(del1, del2 + 1); // Vm.arrayCopy(chars, del2 + 1, chars, del1, numOnRight); newInsertPos = del1; clearSelect = true; } if (isPrintable && (maxLength == 0 || len < maxLength)) { // grow the array if required (grows by charsStep) -- original Convert.insertAt(chars, newInsertPos, (char) ke.key); newInsertPos++; redraw = true; clearSelect = true; } } boolean isMove = true; try { switch (ke.key) { case SpecialKeys.HOME: newInsertPos = 0; if (firstToDraw != 0) { firstToDraw = 0; sb.setValue(0); } break; case SpecialKeys.END: newInsertPos = len; if (numberTextLines > rowCount) { firstToDraw = numberTextLines - rowCount; sb.setValue(firstToDraw); } break; case SpecialKeys.UP: case SpecialKeys.PAGE_UP: if (!editable && !hasCursorWhenNotEditable) // guich@tc114_62 sb.onEvent(event); else if (newInsertPos >= first.items[1]) { charPosToZ(newInsertPos, z1); z1.x = z3.x; // kmeehl@tc100: remember the previous horizontal position if (z1.y <= textRect.y && firstToDraw > 0) // guich@550_22: check firstToDraw, otherwise it will insert blanks at the top { int ii = sb.getValue(); if (ii > sb.getMinimum()) firstToDraw--; sb.setValue(--ii); } else z1.y -= hLine; int line = firstToDraw + (z1.y - textRect.y) / hLine; if (line >= 0) { if (chars.charAt(first.items[line]) == ENTER && first.items[line + 1] - first.items[line] == 1) newInsertPos = first.items[line + 1]; else newInsertPos = zToCharPos(z1); charPosToZ(newInsertPos, z1); } } break; case SpecialKeys.LEFT: newInsertPos--; if (newInsertPos < 0) newInsertPos = 0; while (newInsertPos < first.items[firstToDraw]) { firstToDraw--; sb.setValue(sb.getValue() - 1); } charPosToZ(newInsertPos, z3); // kmeehl@tc100: remember the previous horizontal position break; case SpecialKeys.RIGHT: newInsertPos++; if (newInsertPos > len) newInsertPos = len; if (numberTextLines > rowCount) while ((firstToDraw + rowCount) <= numberTextLines && newInsertPos > first.items[firstToDraw + rowCount]) { firstToDraw++; sb.setValue(sb.getValue() + 1); } charPosToZ(newInsertPos, z3); // kmeehl@tc100: remember the previous horizontal position break; case SpecialKeys.DOWN: case SpecialKeys.PAGE_DOWN: if (!editable && !hasCursorWhenNotEditable) // guich@tc114_62 sb.onEvent(event); else if (numberTextLines > 0 && newInsertPos <= first.items[numberTextLines - 1]) // -1 guich@573_44: check if > 0 { charPosToZ(newInsertPos, z1); z1.x = z3.x; // kmeehl@tc100: remember the previous horizontal position if (z1.y >= textRect.height - hLine) { int ii = sb.getValue(); if (ii < sb.getMaximum()) firstToDraw++; sb.setValue(++ii); } else z1.y += hLine; int line = firstToDraw + (z1.y - textRect.y) / hLine; if (newInsertPos < len && chars.charAt(newInsertPos) == ENTER && first.items[line + 1] - first.items[line] == 1) newInsertPos++; else newInsertPos = zToCharPos(z1); if (line > firstToDraw + (z1.y - textRect.y) / hLine) // zToCharPos failed... newInsertPos++; charPosToZ(newInsertPos, z1); } break; default: isMove = false; } } catch (Exception e) { if (Settings.onJavaSE) e.printStackTrace(); } if (isMove && newInsertPos != insertPos) { if ((ke.modifiers & SpecialKeys.SHIFT) > 0) extendSelect = true; else clearSelect = true; } if (!isMove) calculateFirst(); } break; case PenEvent.PEN_UP: // kmeehl@tc100 lastPenDown = -1; firstPenDown = false; if (!editable && !Settings.fingerTouch) // guich@tc100: allow the user to scroll by just clicking in the ME { event.target = sb; ((PenEvent) event).y = ((PenEvent) event).y < height / 2 ? 0 : height; sb.onEvent(event); break; } else if (popupVKbd) { showSip(); popupVKbd = false; } charPosToZ(newInsertPos, z3); // kmeehl@tc100: remember the previous horizontal position isScrolling = false; break; case PenEvent.PEN_DOWN: { lastPenDown = event.timeStamp; if (!editable && !Settings.fingerTouch) // guich@tc100: allow the user to scroll by just clicking in the ME { event.target = sb; ((PenEvent) event).y = ((PenEvent) event).y < height / 2 ? 0 : height; sb.onEvent(event); break; } if (Settings.geographicalFocus) editMode = true; // kmeehl@tc100 popupVKbd = true; // kmeehl@tc100 PenEvent pe = (PenEvent) event; z1.x = pe.x; z1.y = pe.y; newInsertPos = firstPenDown && Settings.moveCursorToEndOnFocus ? chars.length() : zToCharPos(z1); if ((pe.modifiers & SpecialKeys.SHIFT) > 0) extendSelect = true; // shift else if (firstPenDown && autoSelect) { startSelectPos = 0; newInsertPos = chars.length(); } else clearSelect = true; break; } case PenEvent.PEN_DRAG: { lastPenDown = -1; DragEvent de = (DragEvent) event; if (Settings.fingerTouch) { if (isScrolling) { scrollContent(-de.xDelta, -de.yDelta, true); event.consumed = true; } else { int direction = DragEvent.getInverseDirection(de.direction); event.consumed = true; if (canScrollContent(direction, de.target) && scrollContent(-de.xDelta, -de.yDelta, true)) { isScrolling = scScrolled = true; dragDistance = 0; /* with this, dragging in a MultiEdit with keyboard open, closes the keyboard but the screen is kept shifted if (Settings.fingerTouch && editable && Window.isSipShown) // guich@tc122_39: only when fingerTouch is enabled { Window.isSipShown = false; Window.setSIP(Window.SIP_HIDE, null, false); } */ popupVKbd = false; } } } else if (editable) { PenEvent pe = (PenEvent) event; z1.x = pe.x; z1.y = pe.y; newInsertPos = zToCharPos(z1); if (newInsertPos != insertPos && isEnabled()) extendSelect = true; else return; // guich@320_28: avoid unnecessary repaints } break; } case KeyEvent.ACTION_KEY_PRESS: try { KeyEvent ke = (KeyEvent) event; // allow ENTER to be handled as a normal key event if (Settings.geographicalFocus && ke.key == SpecialKeys.ENTER && editMode) { event.type = KeyEvent.KEY_PRESS; _onEvent(event); break; } } catch (ClassCastException cce) { } if (Settings.geographicalFocus && !improvedGeographicalFocus) editMode = !editMode; if (editMode) { showSip(); if (blinkTimer == null) blinkTimer = addTimer(350); } else if (editable) { if (Window.isSipShown) { Window.isSipShown = false; Window.setSIP(Window.SIP_HIDE, null, false); } if (removeTimer(blinkTimer)) blinkTimer = null; } break; case KeyboardBox.KEYBOARD_ON_UNPOP: gap = tempGap; rowCount = tempRowCount; rowCount0 = tempRowCount0; return; case KeyboardBox.KEYBOARD_POST_UNPOP: if (oldTabIndex != -1) // reinsert this control in the previous position { parent.tabOrder.removeElement(this); parent.tabOrder.insertElementAt(this, oldTabIndex); oldTabIndex = -1; } requestFocus(); return; default: return; } if (extendSelect) { if (startSelectPos == -1) startSelectPos = insertPos; else if (newInsertPos == startSelectPos) startSelectPos = -1; redraw = true; } if (clearSelect && (startSelectPos != -1)) { startSelectPos = -1; redraw = true; } newInsertPos = Math.min(chars.length(), newInsertPos); if (newInsertPos < 0) newInsertPos = 0; boolean insertChanged = (newInsertPos != startSelectPos); if (insertChanged && cursorShowing) Window.needsPaint = true; // erase cursor at old insert position lastInsertPos = insertPos = newInsertPos; if (redraw || insertChanged) Window.needsPaint = true; } else if (event.target == sb && event.type == ControlEvent.PRESSED) { firstToDraw = sb.getValue(); Window.needsPaint = true; // alexgross@340_17 } } private boolean showClipboardMenu() { lastPenDown = -1; firstPenDown = false; int idx = Edit.showClipboardMenu(this); if (0 <= idx && idx <= 3) { if (idx != 3 && startSelectPos == -1) // if nothing was selected, select everything { startSelectPos = 0; insertPos = chars.length(); } if (idx == 0) clipboardCut(); else if (idx == 1) clipboardCopy(); else { if (idx == 2) chars.setLength(0); clipboardPaste(); startSelectPos = -1; return true; // break instead of return on the caller } } return false; } private void clipboardCut() { int sel1 = startSelectPos; int sel2 = insertPos; if (sel1 > sel2) { int temp = sel1; sel1 = sel2; sel2 = temp; } if (sel1 != -1) { Vm.clipboardCopy(chars.toString().substring(sel1, sel2)); // brunosoares@tc100: BlackBerry does not support StringBuffer.substring() showTip(this, Edit.cutStr, 500, -1); backspaceEvent.target = this; _onEvent(backspaceEvent); } } /** Cuts the text on the given range. */ public void cutText(int sel1, int sel2) { if (sel1 > sel2) { int temp = sel1; sel1 = sel2; sel2 = temp; } startSelectPos = sel1; insertPos = sel2; if (sel1 != -1) { backspaceEvent.target = this; _onEvent(backspaceEvent); } } private void clipboardCopy() { int sel1 = startSelectPos; int sel2 = insertPos; if (sel1 > sel2) { int temp = sel1; sel1 = sel2; sel2 = temp; } if (sel1 != -1) { Vm.clipboardCopy(chars.toString().substring(sel1, sel2)); // brunosoares@tc100: BlackBerry does not support StringBuffer.substring() showTip(this, Edit.copyStr, 500, -1); } } private void clipboardPaste() { String pasted = Convert.replace(Vm.clipboardPaste(), Convert.CRLF, "\n"); if (pasted == null || pasted.length() == 0) ; else { showTip(this, Edit.pasteStr, 500, -1); int n = pasted.length(); if (maxLength > 0) { pasted = pasted.substring(0,Math.min(pasted.length(),Math.max(0,maxLength - chars.length()))); n = pasted.length(); } if (validChars != null) // check against the valid chars { StringBuffer sb = new StringBuffer(n); for (int i = 0; i < n; i++) { char c = pasted.charAt(i); if (isCharValid(c)) sb.append(c); } pasted = sb.toString(); } if (chars.length() == 0) { chars.append(pasted); newInsertPos = n; } else for (int i = 0; i < n; i++) Convert.insertAt(chars, newInsertPos++, pasted.charAt(i)); calculateFirst(); } } private void showSip() // guich@tc126_21 { if (kbdType != Edit.KBD_NONE && Settings.virtualKeyboard && editMode && editable && !hadParentScrolled() && !Window.isScreenShifted()) // if running on a PocketPC device, set the bounds of Sip in a way to not cover the edit - kmeehl@tc100: added check for editMode and !dragScroll { int sbl = Settings.SIPBottomLimit; if (sbl == -1) sbl = Settings.screenHeight / 2; boolean onBottom = getAbsoluteRect().y < sbl || Settings.unmovableSIP; if (!Window.isSipShown || Settings.isWindowsDevice()) { Window.isSipShown = true; Window.setSIP(onBottom ? Window.SIP_BOTTOM : Window.SIP_TOP, this, false); } if (Settings.unmovableSIP) // guich@tc126_21 { Window w = getParentWindow(); if (w == null) w = Window.topMost; w.shiftScreen(this,0); } lastZ1y = -9999; } } protected void draw(Graphics g) { if (g == null || !isDisplayed() || boardRect == null) return; // guich@tc114_65: check if its displayed if (!transparentBackground) { g.backColor = uiAndroid ? parent.backColor : back0; g.clearClip(); int x2 = this.width - (Settings.fingerTouch ? 0 : sb.getPreferredWidth()); g.fillRect(0, 0, x2, this.height); if (uiAndroid) { if (npback == null) try { npback = NinePatch.getInstance().getNormalInstance(NinePatch.MULTIEDIT, width, height, isEnabled() ? back0 : Color.interpolate(back0 == parent.backColor ? Color.BRIGHT : back0,parent.backColor), false); } catch (ImageException e) {} NinePatch.tryDrawImage(g,npback,0,0); } else g.draw3dRect(0, 0, x2, this.height, Graphics.R3D_CHECK, false, false, fourColors); } g.setClip(boardRect); // draw the text and/or the selection --original if (startSelectPos != -1 && editable) // guich@tc113_38: only if editable { // character regions are: -- original // 0 to (sel1-1) .. sel1 to (sel2-1) .. sel2 to last_char -- original int sel1 = Math.min(startSelectPos, insertPos); int sel2 = Math.max(startSelectPos, insertPos); charPosToZ(sel1, z1); charPosToZ(sel2, z2); g.backColor = back1; if (z1.y == z2.y) g.fillRect(z1.x, z1.y, z2.x - z1.x, fmH); else { g.fillRect(z1.x, z1.y, textRect.x2() - z1.x + 1, hLine); if (z2.y > z1.y) g.fillRect(textRect.x, z1.y + hLine, textRect.width, z2.y - z1.y - hLine); g.fillRect(textRect.x, z2.y, z2.x - textRect.x, fmH); } } int i = firstToDraw; int h = textRect.y; int dh = textRect.y + fm.ascent; int maxh = h + textRect.height; g.foreColor = fColor; g.backColor = back0; int last = numberTextLines - 1; int len = chars.length(); for (; i <= last && h < maxh; i++, h += hLine, dh += hLine) { //if (!forceDrawAll) g.fillRect(boardRect.x + 1, h, boardRect.width - 2, hLine); // erase drawing area int k = first.items[i]; int k2 = first.items[i + 1]; if (len > 0 && k < len && k != k2) { if (chars.charAt(k) <= ' ') // guich@tc166: ignore space/ENTER at line start k++; if (k2 > k) g.drawText(chars, k, k2 - k, textRect.x, h, (!editable && justify && i < last && k2 < len && chars.charAt(k2) >= ' ') ? textRect.width : 0, textShadowColor != -1, textShadowColor); // don't justify if the line ends with <enter> } if (drawDots) { g.drawDots(textRect.x, dh, textRect.x2(), dh); // guich@320_28: draw the dotted line g.backColor = back0; } } // guich@320_28: draw the dotted lines if (drawDots) for (; i < firstToDraw + rowCount; i++, h += hLine, dh += hLine) { if (drawDots) g.drawDots(textRect.x, dh, textRect.x2(), dh); g.backColor = back0; } if (hasFocus && (editable || hasCursorWhenNotEditable)) { // draw cursor if (cursorShowing) { charPosToZ(insertPos, z1); g.foreColor = Color.interpolate(backColor,foreColor); g.drawRect(z1.x, z1.y - (spaceBetweenLines >> 1), 1, hLine); } cursorShowing = !cursorShowing; if (Window.isScreenShifted() && lastZ1y != z1.y) getParentWindow().shiftScreen(this, lastZ1y = z1.y); } else cursorShowing = false; } protected void onWindowPaintFinished() { if (editable && !hasFocus) _onEvent(new Event(ControlEvent.FOCUS_IN,this,0)); // this event is called on the focused control of the parent window. so, if we are not in FOCUS state, set it now. --original - guich@350_7: added the editable check } public void onPaint(Graphics g) { draw(g); } private void clearPosState() { insertPos = 0; startSelectPos = -1; } protected void pushPosState() { pushedInsertPos = insertPos; pushedStartSelectPos = startSelectPos; } protected void popPosState() { if (cursorShowing) Window.needsPaint = true; insertPos = pushedInsertPos; startSelectPos = pushedStartSelectPos; } /** Return true if the given char exists in the set of valid characters for this Edit */ private boolean isCharValid(char c) { if (validChars == null) return true; c = Convert.toUpperCase(c); return validChars.indexOf(c) != -1; } protected void onColorsChanged(boolean colorsChanged) { fColor = getForeColor(); back0 = Color.brighter(getBackColor()); back1 = back0 != Color.WHITE?backColor:Color.getCursorColor(back0);//guich@300_20: use backColor instead of: back0.getCursorColor(); if (!uiAndroid) Graphics.compute3dColors(isEnabled(),backColor,foreColor,fourColors); sb.setBackForeColors(backColor, foreColor); npback = null; } /** Sets the rect for this MultiEdit. Note that height is recomputed based * in the value for rowCount given in the constructor if the given height is PREFERRED */ public void setRect(int x, int y, int width, int height, Control relative, boolean screenChanged) { if ((PREFERRED-RANGE) <= height && height <= (PREFERRED+RANGE)) // kambiz@330_24: use preferred height only if user wants height += getPreferredHeight() - PREFERRED; super.setRect(x,y,width,height,relative,screenChanged); } protected void onFontChanged() // guich@320_28 { hLine = fmH + spaceBetweenLines; } /** Clears the text of this control. */ public void clear() // guich@572_19 { setText(clearValueStr); } public void getFocusableControls(Vector v) { if (visible && isEnabled()) v.addElement(this); } /** Scrolls the text to the given line. */ public void scrollToLine(int line) { if (line < 0) line = 0; else if (line >= numberTextLines) line = Math.max(numberTextLines-1,0); if (line < sb.minimum) sb.setValue(sb.minimum); else if (line > sb.maximum) sb.setValue(sb.maximum); else sb.setValue(line); line = sb.getValue(); insertPos = first.items[line]; firstToDraw = line; Window.needsPaint = true; } public Control handleGeographicalFocusChangeKeys(KeyEvent ke) { boolean processEvent = editable && (!improvedGeographicalFocus || (!ke.isPrevKey() && !ke.isNextKey()) || (ke.key == SpecialKeys.LEFT && insertPos > 0) || (ke.key == SpecialKeys.RIGHT && insertPos < chars.length()) || ((ke.key == SpecialKeys.UP || ke.key == SpecialKeys.PAGE_UP) && numberTextLines > 1 && insertPos >= first.items[1]) || ((ke.key == SpecialKeys.DOWN || ke.key == SpecialKeys.PAGE_DOWN) && numberTextLines > 1 && insertPos < first.items[numberTextLines - 1])); if (editMode && processEvent) { Object o = ke.target; // guich@tc115_6: otherwise, we will not process arrow events ke.consumed = false; // guich@tc115_20 ke.target = this; _onEvent(ke); ke.target = o; return this; } else if (ke.isUpKey() || ke.isDownKey()) { int val = sb.getValue(); int inc = ke.isUpKey()?-1:1; if (inc == -1 && firstToDraw == 0) return null; int line = numberTextLines - textRect.height/hLine; if (line < 0 || (inc == 1 && firstToDraw == line)) return null; sb.setValue(val + inc); firstToDraw += inc; Window.needsPaint = true; return this; } else return null; } /** Scrolls the text to bottom. */ public void scrollToBottom() { int len = chars.length(); newInsertPos = len; if (numberTextLines>rowCount) { firstToDraw=numberTextLines-rowCount; sb.setValue(firstToDraw); } } /** Scrolls the tex to the top. */ public void scrollToTop() { newInsertPos = 0; if (firstToDraw!=0) { firstToDraw=0; sb.setValue(0); } } /** Returns the length of the text. * @since TotalCross 1.01 */ public int getLength() { return chars.length(); } public void requestFocus() // guich@tc115_6: user requested focus directly, so enable editMode by default { editModeValue = true; super.requestFocus(); editModeValue = false; } /** Returns the keyboard type of this Edit control. * @see Edit#KBD_NONE * @see Edit#KBD_DEFAULT * @see Edit#KBD_KEYBOARD * @see Edit#KBD_CALCULATOR * @see Edit#KBD_CALCULATOR * @since SuperWaba 5.67 */ public byte getKeyboardType() // guich@567_6 { return kbdType; } /** Returns a copy of this Edit with almost all features. Used by Keyboard and SIPBox classes. * @since TotalCross 1.27 */ public MultiEdit getCopy() { MultiEdit ed = mask == null ? new MultiEdit(rowCount, spaceBetweenLines) : new MultiEdit(mask, rowCount, spaceBetweenLines); ed.startSelectPos = startSelectPos; ed.insertPos = insertPos; ed.setBackForeColors(backColor,foreColor); if (validChars != null) ed.setValidChars(validChars); ed.capitalise = capitalise; ed.maxLength = maxLength; ed.kbdType = kbdType; ed.editable = editable; ed.scrollBarsAlwaysVisible = scrollBarsAlwaysVisible; ed.rowCount = rowCount; ed.drawDots = drawDots; ed.justify = justify; ed.autoSelect = autoSelect; return ed; } public Flick getFlick() { return flick; } public boolean wasScrolled() { return scScrolled; } protected boolean willOpenKeyboard() { return editable && (kbdType == Edit.KBD_DEFAULT || kbdType == Edit.KBD_KEYBOARD); } /** Sets the cursor position, ranging from 0 to the text' length. * You can use this with <code>lastInsertPos</code> to recover cursor position. */ public void setCursorPos(int pos) { if (0 <= pos && pos < chars.length()) insertPos = pos; } }