package org.geogebra.web.html5.gui.inputfield; import java.util.ArrayList; import java.util.List; import org.geogebra.common.awt.GColor; import org.geogebra.common.awt.GFont; import org.geogebra.common.awt.GGraphics2D; import org.geogebra.common.euclidian.Drawable; import org.geogebra.common.euclidian.EuclidianConstants; import org.geogebra.common.euclidian.draw.DrawInputBox; import org.geogebra.common.euclidian.event.FocusListener; import org.geogebra.common.euclidian.event.KeyHandler; import org.geogebra.common.euclidian.event.PointerEventType; import org.geogebra.common.gui.VirtualKeyboardListener; import org.geogebra.common.gui.inputfield.AltKeys; import org.geogebra.common.gui.inputfield.AutoComplete; import org.geogebra.common.gui.inputfield.AutoCompleteTextField; import org.geogebra.common.gui.inputfield.InputHelper; import org.geogebra.common.gui.inputfield.MyTextField; import org.geogebra.common.javax.swing.GBox; import org.geogebra.common.kernel.Macro; import org.geogebra.common.kernel.geos.GeoElement; import org.geogebra.common.kernel.geos.GeoInputBox; import org.geogebra.common.main.App; import org.geogebra.common.main.Feature; import org.geogebra.common.main.GWTKeycodes; import org.geogebra.common.main.Localization; import org.geogebra.common.main.MyError; import org.geogebra.common.util.AutoCompleteDictionary; import org.geogebra.common.util.StringUtil; import org.geogebra.common.util.debug.Log; import org.geogebra.common.util.lang.Unicode; import org.geogebra.web.html5.Browser; import org.geogebra.web.html5.event.FocusListenerW; import org.geogebra.web.html5.event.KeyEventsHandler; import org.geogebra.web.html5.event.KeyListenerW; import org.geogebra.web.html5.gui.HasKeyboardTF; import org.geogebra.web.html5.gui.util.CancelEventTimer; import org.geogebra.web.html5.gui.util.ClickStartHandler; import org.geogebra.web.html5.gui.view.autocompletion.CompletionsPopup; import org.geogebra.web.html5.gui.view.autocompletion.GSuggestBox; import org.geogebra.web.html5.gui.view.autocompletion.ScrollableSuggestBox; import org.geogebra.web.html5.main.AppW; import org.geogebra.web.html5.main.GlobalKeyDispatcherW; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.RepeatingCommand; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.event.dom.client.BlurEvent; import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.FocusEvent; import com.google.gwt.event.dom.client.FocusHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyDownEvent; import com.google.gwt.event.dom.client.KeyDownHandler; import com.google.gwt.event.dom.client.KeyPressEvent; import com.google.gwt.event.dom.client.KeyPressHandler; import com.google.gwt.event.dom.client.KeyUpEvent; import com.google.gwt.event.dom.client.KeyUpHandler; import com.google.gwt.event.dom.client.MouseUpEvent; import com.google.gwt.event.dom.client.MouseUpHandler; import com.google.gwt.event.logical.shared.SelectionEvent; import com.google.gwt.event.logical.shared.SelectionHandler; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.regexp.shared.MatchResult; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.FocusWidget; import com.google.gwt.user.client.ui.SuggestOracle.Suggestion; import com.google.gwt.user.client.ui.ToggleButton; import com.google.gwt.user.client.ui.Widget; import com.himamis.retex.editor.web.MathFieldW; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; public class AutoCompleteTextFieldW extends FlowPanel implements AutoComplete, AutoCompleteW, AutoCompleteTextField, KeyDownHandler, KeyUpHandler, KeyPressHandler, ValueChangeHandler<String>, SelectionHandler<Suggestion>, VirtualKeyboardListener, HasSymbolPopup, HasKeyboardTF { public interface InsertHandler { void onInsert(String text); } private static final int BOX_ROUND = 8; protected AppW app; private Localization loc; private StringBuilder curWord; private int curWordStart; protected AutoCompleteDictionary dict; protected boolean isCASInput = false; protected boolean autoComplete; private int historyIndex; private ArrayList<String> history; private boolean handleEscapeKey = false; private List<String> completions; private String cmdPrefix; HistoryPopupW historyPopup; protected ScrollableSuggestBox textField = null; private DrawInputBox drawTextField = null; // symbol table popup fields ToggleButton showSymbolButton = null; private SymbolTablePopupW tablePopup; private boolean showSymbolTableIcon = false; private static boolean showSymbolButtonFocused = false; /** * Flag to determine if text must start with "=" to activate autoComplete; * used with spreadsheet cells */ private boolean isEqualsRequired = false; /** * Flag to determine if Tab key should behave like usual or disabled. */ private boolean tabEnabled = true; private int columns = 0; private boolean forCAS; private InsertHandler insertHandler = null; /** * Pattern to find an argument description as found in the syntax * information of a command. */ // private static Pattern syntaxArgPattern = // Pattern.compile("[,\\[] *(?:<[\\(\\) \\-\\p{L}]*>|\\.\\.\\.) // *(?=[,\\]])"); // Simplified to this as there are too many non-alphabetic character in // parameter descriptions: private static com.google.gwt.regexp.shared.RegExp syntaxArgPattern = com.google.gwt.regexp.shared.RegExp .compile("[,\\[\\(] *(<.*?>|\"<.*?>\"|\\.\\.\\.) *(?=[,\\]\\)])"); private int actualFontSize = 14; private boolean deferredFocus = false; private boolean dummyCursor = false; /** * Constructs a new AutoCompleteTextField that uses the dictionary of the * given Application for autocomplete look up. A default model is created * and the number of columns is 0. * */ public AutoCompleteTextFieldW(int columns, App app) { this(columns, (AppW) app, true, null, false); } public AutoCompleteTextFieldW(int columns, App app, Drawable drawTextField) { this(columns, app); this.drawTextField = (DrawInputBox) drawTextField; addStyleName("FromDrawTextFieldNew"); } public AutoCompleteTextFieldW(int columns, final AppW app, boolean handleEscapeKey, KeyEventsHandler keyHandler, boolean forCAS) { this.forCAS = forCAS; // AG not MathTextField and Mytextfield exists yet super(app); // allow dynamic width with columns = -1 CompletionsPopup completionsPopup = new CompletionsPopup(); textField = new ScrollableSuggestBox(completionsPopup, this, app.getPanel()) { @Override public void setText(String s) { String oldText = super.getText(); int pos = getValueBox().getCursorPos(); StringBuilder sb = new StringBuilder(); int wp = InputHelper.updateCurrentWord(false, new StringBuilder(), oldText, pos, true); /* * if(wp <= 0){ wp = pos; } */ sb.append(oldText.substring(0, wp)); sb.append(s); sb.append(oldText.substring(pos)); super.setText(sb.toString()); // AutoCompleteTextFieldW.this.moveToNextArgument(false); } @Override public void onBrowserEvent(Event event) { int etype = event.getTypeInt(); if ((etype == Event.ONMOUSEDOWN || etype == Event.ONTOUCHSTART) && app.has(Feature.KEYBOARD_BEHAVIOUR)) { app.getGuiManager().setOnScreenKeyboardTextField( AutoCompleteTextFieldW.this); } if (etype == Event.ONMOUSEDOWN || etype == Event.ONMOUSEMOVE || etype == Event.ONMOUSEUP || etype == Event.ONTOUCHMOVE || etype == Event.ONTOUCHSTART || etype == Event.ONTOUCHEND) { event.stopPropagation(); return; } // TODO required for mobile devices // if (showOnScreenKeyBoard // && DOM.eventGetType(event) == FOCUS) { // requestFocus(); // // if (keyboardUsed && !keyBoardModeText) { // setFocus(false); // } // } else if (showOnScreenKeyBoard && keyboardUsed) { // super.onBrowserEvent(event); // super.setFocus(false); // } else { super.onBrowserEvent(event); // } // react on enter from system on screen keyboard or hardware // keyboard if ((event.getTypeInt() == Event.ONKEYUP || event.getTypeInt() == Event.ONKEYPRESS) && event.getKeyCode() == KeyCodes.KEY_ENTER) { // app.hideKeyboard(); // prevent handling in AutoCompleteTextField event.stopPropagation(); } } }; textField.sinkEvents( Event.ONMOUSEMOVE | Event.ONMOUSEUP | Event.TOUCHEVENTS); if (columns > 0) { setColumns(columns); } // setVerticalAlignment(ALIGN_MIDDLE); addStyleName("AutoCompleteTextFieldW"); String id = DOM.createUniqueId(); // Log.debug(id); // id = id.substring(7); textField.addStyleName("TextField"); textField.getElement().setId(id); showSymbolButton = new ToggleButton() { @Override public void onBrowserEvent(Event event) { if (event.getTypeInt() == Event.ONMOUSEDOWN) { // set it as focused anyway, because it is needed // before the real focus and blur events take place showSymbolButton.addStyleName("ShowSymbolButtonFocused"); setShowSymbolButtonFocused(true); } super.onBrowserEvent(event); // this insight has been learnt in InputTreeItem // i.e. do not do it for some event types, e.g. // at least in the following three cases: if (event.getTypeInt() == Event.ONMOUSEMOVE || event.getTypeInt() == Event.ONMOUSEOVER || event.getTypeInt() == Event.ONMOUSEOUT) { return; } // autoCompleteTextField should not loose focus AutoCompleteTextFieldW.this.setFocus(true); } }; textField.setShowSymbolElement(this.showSymbolButton.getElement()); showSymbolButton.getElement().setId(id + "_SymbolButton"); showSymbolButton.getElement().setAttribute("data-visible", "false"); // showSymbolButton.getElement().setAttribute("style", "display: none"); showSymbolButton.setText(Unicode.alpha + ""); showSymbolButton.addStyleName("SymbolToggleButton"); showSymbolButton.addBlurHandler(new BlurHandler() { @Override public void onBlur(BlurEvent event) { showSymbolButton.removeStyleName("ShowSymbolButtonFocused"); setShowSymbolButtonFocused(false); // TODO: make it disappear when blurred // to a place else than the textfield? } }); showSymbolButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { CancelEventTimer.disableBlurEvent(); if (showSymbolButton.isDown()) { // when it is still down, it will be changed to up // when it is still up, it will be changed to down showTablePopupRelativeTo(showSymbolButton); } else { hideTablePopup(); } // autoCompleteTextField should not loose focus AutoCompleteTextFieldW.this.setFocus(true); } }); showSymbolButton.setFocus(false); add(textField); add(showSymbolButton); this.app = app; this.loc = app.getLocalization(); setAutoComplete(true); this.handleEscapeKey = handleEscapeKey; curWord = new StringBuilder(); historyIndex = 0; history = new ArrayList<String>(50); completions = null; // CommandCompletionListCellRenderer cellRenderer = new // CommandCompletionListCellRenderer(); completionsPopup.addTextField(this); // addKeyListener(this); now in MathTextField <==AG not mathtexfield // exist yet if (keyHandler == null) { textField.getValueBox().addKeyDownHandler(this); textField.getValueBox().addKeyUpHandler(this); textField.getValueBox().addKeyPressHandler(this); } else { // This is currently used for // MyCellEditorW.SpreadsheetCellEditorKeyListener textField.getValueBox().addKeyDownHandler(keyHandler); textField.getValueBox().addKeyPressHandler(keyHandler); textField.getValueBox().addKeyUpHandler(keyHandler); } textField.addValueChangeHandler(this); textField.addSelectionHandler(this); ClickStartHandler.init(textField, new ClickStartHandler() { @Override public void onClickStart(int x, int y, PointerEventType type) { // set this text field to be edited by the keyboard app.updateKeyBoardField(AutoCompleteTextFieldW.this); // make sure the keyboard is not closed CancelEventTimer.keyboardSetVisible(); } }); init(); } /** * @param app * creates new AutoCompleteTextField with app. */ public AutoCompleteTextFieldW(App app) { this(0, app); } public void setEnabled(boolean b) { this.textField.setEnabled(b); } public boolean isEnabled() { return this.textField.isEnabled(); } private void init() { textField.getValueBox().addMouseUpHandler(new MouseUpHandler() { @Override public void onMouseUp(MouseUpEvent event) { // AG I dont understand thisAutoCompleteTextField tf = // ((AutoCompleteTextField)event.getSource()); // AG tf.setFocus(true); // textField.setFocus(true); requestFocus(); if (app != null) { app.getGlobalKeyDispatcher() .setFocused(true); } } }); } @Override public DrawInputBox getDrawTextField() { return drawTextField; } @Override public ArrayList<String> getHistory() { return history; } /** * Add a history popup list and an embedded popup button. See * AlgebraInputBar */ public void addHistoryPopup(boolean isDownPopup) { if (historyPopup == null) { historyPopup = new HistoryPopupW(this, app.getPanel()); } historyPopup.setDownPopup(isDownPopup); } @Override public void geoElementSelected(GeoElement geo, boolean addToSelection) { // } @Override public void showPopupSymbolButton(boolean b) { this.showSymbolTableIcon = b; if (showSymbolButton == null) { return; } // temp // TODO: don't fix the popup button here, but it should appear if mouse // clicked into the textfield. if ((showSymbolTableIcon) && app.isAllowedSymbolTables() && this.columns > EuclidianConstants.SHOW_SYMBOLBUTTON_MINLENGTH) { showSymbolButton.getElement().addClassName("shown"); showSymbolButton.getElement().setAttribute("data-persist", "true"); } else { showSymbolButton.getElement().removeClassName("shown"); showSymbolButton.getElement().removeAttribute("data-persist"); } } /** * Sets whether the component is currently performing autocomplete lookups * as keystrokes are performed. * * @param val * True or false. */ @Override public void setAutoComplete(boolean val) { autoComplete = val && loc.isAutoCompletePossible(); /* * if (autoComplete) app.initTranslatedCommands(); */ } @Override public List<String> resetCompletions() { String text = getText(); updateCurrentWord(false); completions = null; if (isEqualsRequired && !text.startsWith("=")) { return null; } boolean korean = false; // AG // app.getLocale().getLanguage().equals("ko"); // start autocompletion only for words with at least two characters if (!InputHelper.needsAutocomplete(curWord, app.getKernel())) { completions = null; return null; } cmdPrefix = curWord.toString(); if (korean) { completions = getDictionary().getCompletionsKorean(cmdPrefix); } else { completions = getDictionary().getCompletions(cmdPrefix); } List<String> commandCompletions = getSyntaxes(completions); // Start with the built-in function completions completions = app.getParserFunctions().getCompletions(cmdPrefix); // Then add the command completions if (completions.isEmpty()) { completions = commandCompletions; } else if (commandCompletions != null) { completions.addAll(commandCompletions); } return completions; } /* * Take a list of commands and return all possible syntaxes for these * commands */ private List<String> getSyntaxes(List<String> commands) { if (commands == null) { return null; } ArrayList<String> syntaxes = new ArrayList<String>(); for (String cmd : commands) { String cmdInt = app.getInternalCommand(cmd); String syntaxString; if (isCASInput) { syntaxString = loc.getCommandSyntaxCAS(cmdInt); } else { syntaxString = app.getExam() == null ? loc.getCommandSyntax(cmdInt) : app.getExam().getSyntax(cmdInt, loc, app.getSettings()); } if (syntaxString == null) { continue; } if (syntaxString.endsWith(isCASInput ? Localization.syntaxCAS : Localization.syntaxStr)) { // command not found, check for macros Macro macro = isCASInput ? null : app.getKernel().getMacro(cmd); if (macro != null) { syntaxes.add(macro.toString()); } else { // syntaxes.add(cmdInt + "[]"); Log.debug("Can't find syntax for: " + cmd); } continue; } for (String syntax : syntaxString.split("\\n")) { syntaxes.add(syntax); } } return syntaxes; } public void cancelAutoCompletion() { completions = null; } @Override public void enableColoring(boolean b) { // } @Override public void setOpaque(boolean b) { // } @Override public void setFont(GFont font) { actualFontSize = font.getSize(); textField.getElement().getStyle().setFontSize(font.getSize(), Unit.PX); if (showSymbolButton != null) { showSymbolButton.getElement().getStyle().setFontSize(font.getSize(), Unit.PX); showSymbolButton.getElement().getStyle() .setLineHeight(font.getSize(), Unit.PX); } if (columns > 0) { setColumns(this.columns); } } @Override public void setForeground(GColor color) { textField.getElement().getStyle() .setColor(GColor.getColorString(color)); } @Override public void setBackground(GColor color) { textField.getElement().getStyle() .setBackgroundColor(GColor.getColorString(color)); } @Override public void setFocusable(boolean b) { // } @Override public void setEditable(boolean b) { textField.setEnabled(b); } @Override public void setColumns(int columns) { this.columns = columns; if (showSymbolButton != null && (this.columns > EuclidianConstants.SHOW_SYMBOLBUTTON_MINLENGTH || this.columns == -1)) { prepareShowSymbolButton(true); } if (this.drawTextField != null) { // only use the correct code for members of the EuclidianView int columnWidth = 11; switch (actualFontSize) { case 7: columnWidth = 6; break; case 9: columnWidth = 8; break; case 14: columnWidth = 11; break; case 18: columnWidth = 15; break;// 18:15:educated guess case 19: columnWidth = 16; break; case 28: columnWidth = 24; break; case 56: columnWidth = 47; break; case 112: columnWidth = 95; break; // more precise for FitLine+FitLineX, but unreal // default: columnWidth = (int) Math.floor(0.83265 * actualFontSize // + 0.4615); break; // default: columnWidth = (int) Math.round(0.832 * actualFontSize); // break; // 20 length * 18 fontSize * 0.002 difference gives just less than 1 // pixel anyway default: columnWidth = (int) Math.round(0.83 * actualFontSize); break; } // this is a way to emulate how Java does it in Desktop version, // but columnWidth is not always exact (+-1) getTextBox().setWidth((columns * columnWidth + 5) + "px"); // the number 5 comes from experimental testing for small textfields // (e.g. columns=1) // of course, this is not the most perfect, but at least works... // due to Greek letters popup, length should be lessened somewhere // else } else { // GeoGebra GUI (non-GGB GUI) can still use the old code, // for compatibility reasons, e.g. Spreadsheet View // as the following solution was wrong, since em means vertical // height: getTextBox().setWidth(columns + "em"); } } public String getCurrentWord() { return curWord.toString(); } @Override public List<String> getCompletions() { return completions; } public int getCurrentWordStart() { return curWordStart; } @Override public void addFocusListener(FocusListener listener) { if (listener instanceof FocusListenerW) { textField.getValueBox().addFocusHandler((FocusListenerW) listener); textField.getValueBox().addBlurHandler((FocusListenerW) listener); } } @Override public void wrapSetText(String s) { // } @Override public int getCaretPosition() { return textField.getValueBox().getCursorPos(); } @Override public void setCaretPosition(int caretPos) { setCaretPosition(caretPos, true); } /** * Sets the position of caret. * * @param caretPos * new position * @param moveDummyCursor * true, if needed to change the dummy cursor position too */ public void setCaretPosition(int caretPos, boolean moveDummyCursor) { if (dummyCursor && moveDummyCursor) { if (caretPos == textField.getText().length()) { return; } removeDummyCursor(); addDummyCursor(caretPos); } else { textField.getValueBox().setCursorPos(caretPos); } } public void addDummyCursor(int caretPos) { if (!app.has(Feature.KEYBOARD_BEHAVIOUR)) { return; } if (dummyCursor || !Browser.isAndroid()) { return; } String text = textField.getText(); text = text.substring(0, caretPos) + '|' + text.substring(caretPos); textField.setValue(text); textField.getValueBox().setCursorPos(caretPos); dummyCursor = true; } @Override public void addDummyCursor() { if (!app.has(Feature.KEYBOARD_BEHAVIOUR)) { return; } if (dummyCursor || !Browser.isAndroid()) { return; } Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() { @Override public void execute() { int caretPos = getCaretPosition(); addDummyCursor(caretPos); } }); } @Override public int removeDummyCursor() { if (!dummyCursor) { return -1; } String text = textField.getText(); int cpos = getCaretPosition(); text = text.substring(0, cpos) + text.substring(cpos + 1); textField.setValue(text); dummyCursor = false; return cpos; } public boolean hasDummyCursor() { return dummyCursor; } @Override public void setDictionary(boolean forCAS) { this.forCAS = forCAS; this.dict = null; } @Override public AutoCompleteDictionary getDictionary() { if (this.dict == null) { this.dict = this.forCAS ? app.getCommandDictionaryCAS() : app.getCommandDictionary(); } return dict; } // returns the word at position pos in text public static String getWordAtPos(String text, int pos) { // search to the left int wordStart = pos - 1; while (wordStart >= 0 && StringUtil .isLetterOrDigitOrUnderscore(text.charAt(wordStart))) { --wordStart; } wordStart++; // search to the right int wordEnd = pos; int length = text.length(); while (wordEnd < length && StringUtil.isLetterOrDigitOrUnderscore(text.charAt(wordEnd))) { ++wordEnd; } if (wordStart >= 0 && wordEnd <= length) { return text.substring(wordStart, wordEnd); } return null; } /** * shows dialog with syntax info * * @param cmd * is the internal command name */ private void showCommandHelp(String cmd) { // show help for current command (current word) String help = loc.getCommandSyntax(cmd); // show help if available if (help != null) { app.showError(new MyError(loc, loc.getPlain("Syntax") + ":\n" + help, cmd, null)); } else if (app.getGuiManager() != null) { app.getGuiManager().openCommandHelp(null); } } private void clearSelection() { int start = textField.getText() .indexOf(textField.getValueBox().getSelectedText()); int end = start + textField.getValueBox().getSelectionLength(); // clear selection if there is one if (start != end) { int pos = getCaretPosition(); String oldText = getText(); StringBuilder sb = new StringBuilder(); sb.append(oldText.substring(0, start)); sb.append(oldText.substring(end)); setText(sb.toString()); if (pos < sb.length()) { setCaretPosition(pos); } } } /** * Updates curWord to word at current caret position. curWordStart, * curWordEnd are set to this word's start and end position */ public void updateCurrentWord(boolean searchRight) { int next = InputHelper.updateCurrentWord(searchRight, this.curWord, getText(), getCaretPosition(), true); if (next > -1) { this.curWordStart = next; } } /* * just show syntax error (already correctly formulated by * CommandProcessor.argErr()) */ public void showError(MyError e) { app.showError(e); } @Override public boolean getAutoComplete() { return autoComplete && loc.isAutoCompletePossible(); } /** * @return previous input from input textfield's history */ private String getPreviousInput() { if (history.size() == 0) { return null; } if (historyIndex > 0) { --historyIndex; } return history.get(historyIndex); } /** * @return next input from input textfield's history */ private String getNextInput() { if (historyIndex < history.size()) { ++historyIndex; } if (historyIndex == history.size()) { return null; } return history.get(historyIndex); } public void mergeKoreanDoubles() { // avoid shift on Korean keyboards /* * AG dont do that yet if (app.getLocale().getLanguage().equals("ko")) { * String text = getText(); int caretPos = getCaretPosition(); String * mergeText = Korean.mergeDoubleCharacters(text); int decrease = * text.length() - mergeText.length(); if (decrease > 0) { * setText(mergeText); setCaretPosition(caretPos - decrease); } } */ Log.debug("KoreanDoubles may be needed in AutocompleteTextField"); } private boolean moveToNextArgument(boolean find) { String text = getText(); int caretPos = getCaretPosition(); // make sure it works if caret is just after [ if (caretPos > 0 && text.length() < caretPos && text.charAt(caretPos) != '[') { caretPos--; } String suffix = text.substring(caretPos); int index = -1; // AGMatcher argMatcher = syntaxArgPattern.matcher(text); MatchResult argMatcher = syntaxArgPattern.exec(suffix); // boolean hasNextArgument = argMatcher.find(caretPos); boolean hasNextArgument = syntaxArgPattern.test(suffix); if (hasNextArgument) { index = argMatcher.getIndex() + caretPos; } if (find && !hasNextArgument) { // hasNextArgument = argMatcher.find(); hasNextArgument = syntaxArgPattern.test(text); argMatcher = syntaxArgPattern.exec(text); if (hasNextArgument) { index = argMatcher.getIndex(); } } // if (hasNextArgument && (find || argMatcher.start() == caretPos)) { if (hasNextArgument && argMatcher.getGroup(1) != null && (find || index == caretPos)) { // setCaretPosition(argMatcher.end(); // moveCaretPosition(argMatcher.start() + 1); String groupStr = argMatcher.getGroup(1); textField.getValueBox().setSelectionRange(index + 2, groupStr.length()); return true; } return false; } // ---------------------------------------------------------------------------- // Protected methods ..why? :-) // ---------------------------------------------------------------------------- boolean ctrlC = false; private boolean rightAltDown; private boolean leftAltDown; @Override public void onKeyPress(KeyPressEvent e) { if (GlobalKeyDispatcherW.isBadKeyEvent(e)) { e.preventDefault(); e.stopPropagation(); return; } // only handle parentheses char ch = e.getCharCode(); int caretPos = getCaretPosition(); String text = getText(); // checking for isAltDown() because Alt+, prints another character on // the PC // TODO make this more robust - perhaps it could go in a document change // listener if (ch == ',' && !e.isAltKeyDown()) { if (caretPos < text.length() && text.charAt(caretPos) == ',') { // User typed ',' just in ahead of an existing ',': // We may be in the position of filling in the next argument of // an autocompleted command // Look for a pattern of the form ", < Argument Description > ," // or ", < Argument Description > ]" // If found, select the argument description so that it can // easily be typed over with the value // of the argument. if (moveToNextArgument(false)) { e.stopPropagation(); e.preventDefault(); } return; } } if (MathFieldW.checkCode(e.getNativeEvent(), "NumpadDecimal")) { e.preventDefault(); insertString("."); return; } if (!(ch == '(' || ch == '{' || ch == '[' || ch == '}' || ch == ')' || ch == ']')) { // super.keyTyped(e); Log.debug("super.keyTyped needed in AutocompleteTextField"); return; } clearSelection(); caretPos = getCaretPosition(); if (ch == '}' || ch == ')' || ch == ']') { // simple check if brackets match if (text.length() > caretPos && text.charAt(caretPos) == ch) { int count = 0; for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); if (c == '{') { count++; } else if (c == '}') { count--; } else if (c == '(') { count += 1E3; } else if (c == ')') { count -= 1E3; } else if (c == '[') { count += 1E6; } else if (c == ']') { count -= 1E6; } } if (count == 0) { // if brackets match, just move the cursor forwards one e.preventDefault(); caretPos++; } } } // auto-close parentheses if (caretPos == text.length() || MyTextField .isCloseBracketOrWhitespace(text.charAt(caretPos))) { switch (ch) { default: // do nothing break; case '(': // opening parentheses: insert closing parenthesis automatically insertString(")"); break; case '{': // opening braces: insert closing parenthesis automatically insertString("}"); break; case '[': // opening bracket: insert closing parenthesis automatically insertString("]"); break; } } // make sure we keep the previous caret position setCaretPosition(Math.min(text.length(), caretPos)); } // private boolean isBadKeyEvent(KeyEvent e) { // return e.isAltKeyDown() && !e.isControlKeyDown() // && e.getNativeEvent().getCharCode() > 128; // } @Override public void onKeyDown(KeyDownEvent e) { if (!isTabEnabled()) { return; } if (MathFieldW.isRightAlt(e.getNativeEvent())) { rightAltDown = true; } if (MathFieldW.isLeftAlt(e.getNativeEvent())) { leftAltDown = true; } if (leftAltDown) { Log.debug("TODO: preventDefault"); } int keyCode = e.getNativeKeyCode(); app.getGlobalKeyDispatcher(); if (keyCode == GWTKeycodes.KEY_TAB || keyCode == GWTKeycodes.KEY_F1 || GlobalKeyDispatcherW.isBadKeyEvent(e)) { e.preventDefault(); if (keyCode == GWTKeycodes.KEY_TAB && usedForInputBox()) { AutoCompleteTextField tf = app.getActiveEuclidianView() .getTextField(); if (tf != null) { geoUsedForInputBox.setText(tf.getText()); tf.setVisible(false); } app.getGlobalKeyDispatcher().handleTab(e.isControlKeyDown(), e.isShiftKeyDown(), true); GeoElement next = app.getSelectionManager().getSelectedGeos() .get(0); if (next instanceof GeoInputBox) { app.getActiveEuclidianView() .focusTextField((GeoInputBox) next); } else { app.getActiveEuclidianView().requestFocus(); } } } } @Override @SuppressFBWarnings({ "SF_SWITCH_FALLTHROUGH", "missing break is deliberate" }) public void onKeyUp(KeyUpEvent e) { int keyCode = e.getNativeKeyCode(); // we don't want to trap AltGr // as it is used eg for entering {[}] is some locales // NB e.isAltGraphDown() doesn't work if (e.isAltKeyDown() && e.isControlKeyDown()) { return; } // swallow eg ctrl-a ctrl-b ctrl-p on Mac /* * AG if (Application.MAC_OS && e.isControlKeyDown()) { e.consume(); } */ ctrlC = false; switch (keyCode) { case GWTKeycodes.KEY_Z: case GWTKeycodes.KEY_Y: if (e.isControlKeyDown()) { app.getGlobalKeyDispatcher().handleGeneralKeys(e); e.stopPropagation(); } break; case GWTKeycodes.KEY_C: if (e.isControlKeyDown()) // workaround for MAC_OS { ctrlC = true; } break; // process input case GWTKeycodes.KEY_ESCAPE: if (!handleEscapeKey) { break; } /* * AG do this if we will have windows Component comp = * SwingUtilities.getRoot(this); if (comp instanceof JDialog) { * ((JDialog) comp).setVisible(false); return; } */ if (textField.isSuggestionListVisible()) { textField.hideSuggestions(); } else { textField.setFocus(false); app.getActiveEuclidianView().requestFocus(); } break; case GWTKeycodes.KEY_UP: if (!isSuggesting()) { if (!handleEscapeKey) { break; } if (historyPopup == null) { String text = getPreviousInput(); if (text != null) { setText(text); } } else if (!historyPopup.isDownPopup()) { historyPopup.showPopup(); } } e.stopPropagation(); break; case GWTKeycodes.KEY_DOWN: if (!handleEscapeKey) { break; } if (historyPopup != null && historyPopup.isDownPopup()) { historyPopup.showPopup(); } else { // Fix for Ticket #463 if (getNextInput() != null) { setText(getNextInput()); } } e.stopPropagation(); // prevent GlobalKeyDispatcherW to move the // euclidian view break; case GWTKeycodes.KEY_F9: // needed for applets if (app.isApplet()) { app.getGlobalKeyDispatcher().handleGeneralKeys(e); } break; case GWTKeycodes.KEY_LEFT: textField.hideSuggestions(); e.stopPropagation(); break; case GWTKeycodes.KEY_RIGHT: if (moveToNextArgument(false)) { e.stopPropagation(); textField.hideSuggestions(); } e.stopPropagation(); break; case GWTKeycodes.KEY_TAB: e.preventDefault(); if (moveToNextArgument(true)) { e.stopPropagation(); } break; case GWTKeycodes.KEY_F1: if (autoComplete) { if (!"".equals(getText())) { int pos = getCaretPosition(); while (pos > 0 && getText().charAt(pos - 1) == '[') { pos--; } String word = getWordAtPos(getText(), pos); String lowerCurWord = word.toLowerCase(); String closest = getDictionary().lookup(lowerCurWord); if (closest != null) { // lowerCurWord.equals(closest.toLowerCase())) showCommandHelp(app.getInternalCommand(closest)); } else if (app.getGuiManager() != null) { app.getGuiManager().openHelp(App.WIKI_MANUAL); } } } else if (app.getGuiManager() != null) { app.getGuiManager().openHelp(App.WIKI_MANUAL); } e.stopPropagation(); break; case GWTKeycodes.KEY_ZERO: case GWTKeycodes.KEY_ONE: case GWTKeycodes.KEY_TWO: case GWTKeycodes.KEY_THREE: case GWTKeycodes.KEY_FOUR: case GWTKeycodes.KEY_FIVE: case GWTKeycodes.KEY_SIX: case GWTKeycodes.KEY_SEVEN: case GWTKeycodes.KEY_EIGHT: case GWTKeycodes.KEY_NINE: if (e.isControlKeyDown() && e.isShiftKeyDown()) { app.getGlobalKeyDispatcher().handleGeneralKeys(e); } // fall through eg Alt-2 for squared default: if (MathFieldW.isRightAlt(e.getNativeEvent())) { rightAltDown = true; } if (MathFieldW.isLeftAlt(e.getNativeEvent())) { leftAltDown = true; } // check for eg alt-a for alpha // check for eg alt-shift-a for upper case alpha if (e.isAltKeyDown() && !rightAltDown) { String s = AltKeys.getAltSymbols(keyCode, e.isShiftKeyDown(), true); if (s != null) { insertString(s); break; } } /* * Try handling here that is originaly in keyup */ boolean modifierKeyPressed = e.isControlKeyDown() || e.isAltKeyDown(); // we don't want to act when AltGr is down // as it is used eg for entering {[}] is some locales // NB e.isAltGraphDown() doesn't work if (e.isAltKeyDown() && e.isControlKeyDown()) { modifierKeyPressed = false; } char charPressed = Character.valueOf((char) e.getNativeKeyCode()); if ((StringUtil.isLetterOrDigitOrUnderscore(charPressed) || modifierKeyPressed) && !(ctrlC) && (e.getNativeKeyCode() != GWTKeycodes.KEY_A)) { clearSelection(); } // handle alt-p etc // super.keyReleased(e); mergeKoreanDoubles(); if (getAutoComplete()) { updateCurrentWord(false); } } } public void addToHistory(String str) { // exit if the new string is the same as the last entered string if (!history.isEmpty() && str.equals(history.get(history.size() - 1))) { return; } history.add(str); historyIndex = history.size(); } @Override public boolean isSuggesting() { return textField.isSuggestionListVisible(); } private boolean isSuggestionJustHappened = false; private GeoInputBox geoUsedForInputBox; /** * @return that suggestion is just happened (click or enter, so we don't * need to run the enter code again */ public boolean isSuggestionJustHappened() { return isSuggestionJustHappened;// && !isSuggestionClickJustHappened; } public void setIsSuggestionJustHappened(boolean b) { isSuggestionJustHappened = b; } /* Hopefully happens only on click */ @Override public void onValueChange(ValueChangeEvent<String> event) { // textField.getValueBox().getElement().focus(); } @Override public void onSelection(SelectionEvent<Suggestion> event) { isSuggestionJustHappened = true; int index = completions .indexOf(event.getSelectedItem().getReplacementString()); validateAutoCompletion(index, getCompletions()); } /** * Inserts a string into the text at the current caret position */ @Override public void insertString(String text) { int start = getSelectionStart(); int end = getSelectionEnd(); setText(start, end, text); if (insertHandler != null) { insertHandler.onInsert(text); } } public void onBackSpace() { int start = getSelectionStart(); int end = getSelectionEnd(); if (end - start < 1) { end = getCaretPosition(); start = end - 1; } if (start >= 0) { setText(start, end, ""); } } private void setText(int start, int end, String text) { // clear selection if there is one if (start != end) { String oldText = getText(true); StringBuilder sb = new StringBuilder(); sb.append(oldText.substring(0, start)); sb.append(oldText.substring(end)); setText(sb.toString()); setCaretPosition(start, false); } int pos = getCaretPosition(); String oldText = getText(true); StringBuilder sb = new StringBuilder(); sb.append(oldText.substring(0, pos)); sb.append(text); sb.append(oldText.substring(pos)); setText(sb.toString()); // setCaretPosition(pos + text.length()); final int newPos = pos + text.length(); this.updateCurrentWord(false); setCaretPosition(newPos, false); // TODO: tried to keep the Mac OS from auto-selecting the field by // resetting the // caret, but not working yet // setCaret(new DefaultCaret()); // setCaretPosition(newPos); } private int getSelectionEnd() { return getSelectionStart() + textField.getValueBox().getSelectionLength(); } private int getSelectionStart() { return getText().indexOf(textField.getValueBox().getSelectedText()); } @Override public void removeSymbolTable() { if (showSymbolButton == null) { return; } this.showSymbolButton.removeFromParent(); this.showSymbolButton = null; } void showTablePopupRelativeTo(Widget w) { if (tablePopup == null && this.showSymbolButton != null) { tablePopup = new SymbolTablePopupW(app, this, showSymbolButton); } if (this.tablePopup != null) { tablePopup.showRelativeTo(w); } } public void hideTablePopup() { if (this.tablePopup != null) { this.tablePopup.hide(); } } @Override public String getText() { String text = textField.getText(); if(dummyCursor){ int cpos = getCaretPosition(); text = text.substring(0, cpos) + text.substring(cpos + 1); } return text; } public String getText(boolean withDummyCursor) { if (withDummyCursor) { return textField.getText(); } return getText(); } @Override public void setText(String s) { textField.getValueBox().setText(s); } public FocusWidget getTextBox() { return textField.getValueBox(); } @Override public void toggleSymbolButton(boolean toggled) { if (showSymbolButton == null) { return; } showSymbolButton.setDown(toggled); } public GSuggestBox getTextField() { return textField; } /** * Ticket #1167 Auto-completes input; <br> * * @param index * index of the chosen command in the completions list * @param completions * @return false if completions list is null or index < 0 or index > * completions.size() * @author Arnaud */ public boolean validateAutoCompletion(int index, List<String> completions) { if (completions == null || index < 0 || index >= completions.size()) { return false; } String command = completions.get(index); // String text = getText(); // StringBuilder sb = new StringBuilder(); // sb.append(text.substring(0, curWordStart)); // sb.append(command); // sb.append(text.substring(curWordStart + curWord.length())); // setText(sb.toString()); int bracketIndex = command.indexOf('[');// + 1; // Special case if the completion is a built-in function if (bracketIndex == -1) { bracketIndex = command.indexOf('('); bracketIndex++; /* * setCaretPosition(curWordStart + bracketIndex + 1); return true; */ } setCaretPosition(curWordStart + bracketIndex); moveToNextArgument(false); return true; } /** * This method inspects its first parameter - an Object, if there is a * symbol button associated with that. If there is one, this method sets the * visibility of the symbol button according to the second parameter. * * @param source * the scanned object * @param show * true, if the source's symbol button must be visible, false * otherwise. */ public static void showSymbolButtonIfExists(Object source, boolean show) { if (source instanceof HasSymbolPopup) { ((HasSymbolPopup) source).showPopup(show); } } @Override public void setUsedForInputBox(GeoInputBox geo) { geoUsedForInputBox = geo; } @Override public boolean usedForInputBox() { return geoUsedForInputBox != null; } Scheduler.ScheduledCommand cmdDeferredFocus = new Scheduler.ScheduledCommand() { @Override public void execute() { textField.setFocus(true); } }; @Override public void requestFocus() { // if (app.isPrerelease()) { // app.showKeyboard(this); // // // TODO needs to be removed for mobile devices // textField.setFocus(true); // } else { // #5371 if (hasDeferredFocus()) { Scheduler.get().scheduleDeferred(cmdDeferredFocus); } textField.setFocus(true); // } if (geoUsedForInputBox != null && !geoUsedForInputBox.isSelected()) { app.getSelectionManager().clearSelectedGeos(false); app.getSelectionManager().addSelectedGeo(geoUsedForInputBox); } } @Override public void setFocusTraversalKeysEnabled(boolean b) { // Dummy method } @Override public boolean hasFocus() { Log.debug("Unimplemented"); return false; } /** returns if text must start with "=" to activate autocomplete */ public boolean isEqualsRequired() { return isEqualsRequired; } /** sets flag to require text starts with "=" to activate autocomplete */ public void setEqualsRequired(boolean isEqualsRequired) { this.isEqualsRequired = isEqualsRequired; } public void setCASInput(boolean b) { this.isCASInput = b; } @Override public void addKeyHandler(KeyHandler handler) { textField.getValueBox().addKeyPressHandler(new KeyListenerW(handler)); } /** * @param handler * Handler to key up events * @return the handler */ public HandlerRegistration addKeyUpHandler(KeyUpHandler handler) { return textField.getValueBox().addKeyUpHandler(handler); } public void setWidth(int width) { if (width > 0) { textField.setWidth(width + "px"); super.setWidth(width + "px"); } } @Override public String getCommand() { this.updateCurrentWord(true); return this.getCurrentWord(); } /** * @param handler * Adds a focus handler to the wrapped textfield. */ public HandlerRegistration addFocusHandler(FocusHandler handler) { return textField.getValueBox().addFocusHandler(handler); } public void enableGGBKeyboard() { if (!app.has(Feature.KEYBOARD_BEHAVIOUR)) { return; } if (Browser.isTabletBrowser()) { // avoid native keyboard opening getTextField().getValueBox().setReadOnly(true); } addFocusHandler(new FocusHandler() { @Override public void onFocus(FocusEvent event) { fieldFocus(); } }); addBlurHandler(new BlurHandler() { @Override public void onBlur(BlurEvent event) { fieldBlur(); } }); } protected void fieldFocus() { FieldHandler.focusGained(this, app); } protected void fieldBlur() { FieldHandler.focusLost(this, app); } /** * Selects all text. */ public void selectAll() { textField.getValueBox().selectAll(); } /** * @param handler * Keypresshandler * * Added to tetxtfield as handler */ public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) { return textField.getValueBox().addKeyPressHandler(handler); } /** * @param handler * Blurhandler attached to texbox */ public void addBlurHandler(BlurHandler handler) { getTextBox().addBlurHandler(handler); } public boolean isTabEnabled() { return tabEnabled; } public void setTabEnabled(boolean tabEnabled) { this.tabEnabled = tabEnabled; } /** * use this, if no explicit length set, but symbolbutton must be shown. */ public void requestToShowSymbolButton() { if (showSymbolButton == null) { return; } this.columns = EuclidianConstants.SHOW_SYMBOLBUTTON_MINLENGTH + 1; prepareShowSymbolButton(true); } @Override public void prepareShowSymbolButton(boolean b) { if (showSymbolButton == null) { return; } if (b) { showSymbolButton.getElement().setAttribute("data-visible", "true"); addStyleName("SymbolCanBeShown"); } else { showSymbolButton.getElement().setAttribute("data-visible", "false"); removeStyleName("SymbolCanBeShown"); } } @Override public void setFocus(boolean b, boolean sv) { setFocus(b); } @Override public void setFocus(boolean b) { textField.setFocus(b); } @Override public void showPopup(boolean show) { if (this.showSymbolButton == null) { return; } Element showSymbolElement = this.showSymbolButton.getElement(); // Log.debug("AF focused" + show); if (showSymbolElement != null && "true" .equals(showSymbolElement.getAttribute("data-visible"))) { if (show) { // Log.debug("AF focused2" + show); showSymbolElement.addClassName("shown"); } else { // Log.debug("AF focused2" + show); if (!"true".equals( showSymbolElement.getAttribute("data-persist"))) { showSymbolElement.removeClassName("shown"); } } } } public void addInsertHandler(InsertHandler insertHandler) { this.insertHandler = insertHandler; } @Override public Widget toWidget() { return this; } @Override public void ensureEditing() { // TODO Auto-generated method stub } @Override public void startOnscreenKeyboardEditing() { if (Browser.isAndroid()) { addDummyCursor(); } } @Override public void endOnscreenKeyboardEditing() { if (Browser.isAndroid()) { removeDummyCursor(); } } public boolean hasDeferredFocus() { return deferredFocus; } public void setDeferredFocus(boolean b) { deferredFocus = b; } @Override public void drawBounds(GGraphics2D g2, GColor bgColor, int left, int top, int width, int height) { g2.setPaint(bgColor); g2.fillRoundRect(left, top, width, height, BOX_ROUND, BOX_ROUND); // TF Rectangle g2.setPaint(GColor.LIGHT_GRAY); g2.drawRoundRect(left, top, width, height, BOX_ROUND, BOX_ROUND); } @Override public void hideDeferred(final GBox box) { CancelEventTimer.blurEventOccured(); Scheduler.get().scheduleEntry(new RepeatingCommand() { @Override public boolean execute() { if (CancelEventTimer.cancelBlurEvent()) { return true; } setVisible(false); box.setVisible(false); // app.getActiveEuclidianView().remove(box); return false; } }); } @Override public void autocomplete(String s) { getTextField().setText(s); ArrayList<String> arr = new ArrayList<String>(); arr.add(s); validateAutoCompletion(0, arr); } @Override public void onEnter(boolean b) { // TODO Auto-generated method stub } @Override public void updatePosition(AbstractSuggestionDisplay sug) { sug.setPositionRelativeTo(textField); } @Override public boolean isForCAS() { return false; } @Override public boolean needsAutofocus() { return false; } @Override public void setDrawTextField(DrawInputBox df) { drawTextField = df; } @Override public GeoInputBox getInputBox() { return geoUsedForInputBox; } /** * @return app */ public AppW getApp() { return app; } public static boolean isShowSymbolButtonFocused() { return showSymbolButtonFocused; } public static void setShowSymbolButtonFocused(boolean showSymbolButtonFocused) { AutoCompleteTextFieldW.showSymbolButtonFocused = showSymbolButtonFocused; } }