/* * Copyright (C) 2009, 2010, 2011, 2012 Research In Motion Limited. All rights reserved. * * 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 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "config.h" #include "InputHandler.h" #include "BackingStore.h" #include "BackingStoreClient.h" #include "CSSStyleDeclaration.h" #include "Chrome.h" #include "ColorPickerClient.h" #include "DOMSupport.h" #include "DatePickerClient.h" #include "Document.h" #include "DocumentLoader.h" #include "DocumentMarkerController.h" #include "FocusController.h" #include "Frame.h" #include "FrameView.h" #include "HTMLFormElement.h" #include "HTMLInputElement.h" #include "HTMLNames.h" #include "HTMLOptGroupElement.h" #include "HTMLOptionElement.h" #include "HTMLSelectElement.h" #include "HTMLTextAreaElement.h" #include "NotImplemented.h" #include "Page.h" #include "PlatformKeyboardEvent.h" #include "PluginView.h" #include "Range.h" #include "RenderLayer.h" #include "RenderMenuList.h" #include "RenderPart.h" #include "RenderText.h" #include "RenderTextControl.h" #include "RenderWidget.h" #include "RenderedDocumentMarker.h" #include "ScopePointer.h" #include "SelectPopupClient.h" #include "SelectionHandler.h" #include "SpellChecker.h" #include "TextCheckerClient.h" #include "TextIterator.h" #include "VisiblePosition.h" #include "WebPageClient.h" #include "WebPage_p.h" #include "WebSettings.h" #include "htmlediting.h" #include "visible_units.h" #include #include #include #include #include #include #include #define ENABLE_INPUT_LOG 0 #define ENABLE_FOCUS_LOG 0 #define ENABLE_SPELLING_LOG 0 static const unsigned MaxLearnTextDataSize = 500; static const unsigned MaxSpellCheckingStringLength = 250; using namespace BlackBerry::Platform; using namespace WebCore; #if ENABLE_INPUT_LOG #define InputLog(severity, format, ...) logAlways(severity, format, ## __VA_ARGS__) #else #define InputLog(severity, format, ...) #endif // ENABLE_INPUT_LOG #if ENABLE_FOCUS_LOG #define FocusLog(severity, format, ...) logAlways(severity, format, ## __VA_ARGS__) #else #define FocusLog(severity, format, ...) #endif // ENABLE_FOCUS_LOG #if ENABLE_SPELLING_LOG #define SpellingLog(severity, format, ...) logAlways(severity, format, ## __VA_ARGS__) #else #define SpellingLog(severity, format, ...) #endif // ENABLE_SPELLING_LOG namespace BlackBerry { namespace WebKit { class ProcessingChangeGuard { public: ProcessingChangeGuard(InputHandler* inputHandler) : m_inputHandler(inputHandler) , m_savedProcessingChange(false) { ASSERT(m_inputHandler); m_savedProcessingChange = m_inputHandler->processingChange(); m_inputHandler->setProcessingChange(true); } ~ProcessingChangeGuard() { m_inputHandler->setProcessingChange(m_savedProcessingChange); } private: InputHandler* m_inputHandler; bool m_savedProcessingChange; }; InputHandler::InputHandler(WebPagePrivate* page) : m_webPage(page) , m_currentFocusElement(0) , m_inputModeEnabled(false) , m_processingChange(false) , m_changingFocus(false) , m_currentFocusElementType(TextEdit) , m_currentFocusElementTextEditMask(DEFAULT_STYLE) , m_composingTextStart(0) , m_composingTextEnd(0) , m_pendingKeyboardVisibilityChange(NoChange) , m_delayKeyboardVisibilityChange(false) , m_request(0) , m_processingTransactionId(-1) , m_focusZoomScale(0.0) { } InputHandler::~InputHandler() { } static BlackBerryInputType convertInputType(const HTMLInputElement* inputElement) { if (inputElement->isPasswordField()) return InputTypePassword; if (inputElement->isSearchField()) return InputTypeSearch; if (inputElement->isEmailField()) return InputTypeEmail; if (inputElement->isMonthControl()) return InputTypeMonth; if (inputElement->isNumberField()) return InputTypeNumber; if (inputElement->isTelephoneField()) return InputTypeTelephone; if (inputElement->isURLField()) return InputTypeURL; #if ENABLE(INPUT_TYPE_COLOR) if (inputElement->isColorControl()) return InputTypeColor; #endif if (inputElement->isDateControl()) return InputTypeDate; if (inputElement->isDateTimeControl()) return InputTypeDateTime; if (inputElement->isDateTimeLocalControl()) return InputTypeDateTimeLocal; if (inputElement->isTimeControl()) return InputTypeTime; // FIXME: missing WEEK popup selector if (DOMSupport::elementIdOrNameIndicatesEmail(inputElement)) return InputTypeEmail; if (DOMSupport::elementIdOrNameIndicatesUrl(inputElement)) return InputTypeURL; if (DOMSupport::elementPatternIndicatesNumber(inputElement)) return InputTypeNumber; if (DOMSupport::elementPatternIndicatesHexadecimal(inputElement)) return InputTypeHexadecimal; return InputTypeText; } static int inputStyle(BlackBerryInputType type, const Element* element) { switch (type) { case InputTypeEmail: case InputTypeURL: case InputTypeSearch: case InputTypeText: case InputTypeTextArea: { DOMSupport::AttributeState autoCompleteState = DOMSupport::elementSupportsAutocomplete(element); DOMSupport::AttributeState autoCorrectState = DOMSupport::elementSupportsAutocorrect(element); // Autocomplete disabled explicitly. if (autoCompleteState == DOMSupport::Off) { if (autoCorrectState == DOMSupport::On) return NO_PREDICTION; return NO_AUTO_TEXT | NO_PREDICTION | NO_AUTO_CORRECTION; } // Autocomplete enabled explicitly. if (autoCompleteState == DOMSupport::On) { if (autoCorrectState == DOMSupport::Off) return NO_AUTO_TEXT | NO_AUTO_CORRECTION; return DEFAULT_STYLE; } // Check for reserved strings and known types. if (type == InputTypeEmail || type == InputTypeURL || DOMSupport::elementIdOrNameIndicatesNoAutocomplete(element)) { if (autoCorrectState == DOMSupport::On) return NO_PREDICTION; return NO_AUTO_TEXT | NO_PREDICTION | NO_AUTO_CORRECTION; } // Autocomplete state wasn't provided if it is a text area or autocorrect is on, apply support. if (autoCorrectState == DOMSupport::On || (type == InputTypeTextArea && autoCorrectState != DOMSupport::Off)) return DEFAULT_STYLE; // Single line text input, without special features explicitly turned on or a textarea // with autocorrect disabled explicitly. Only enable predictions. return NO_AUTO_TEXT | NO_AUTO_CORRECTION; } case InputTypeIsIndex: case InputTypePassword: case InputTypeNumber: case InputTypeTelephone: case InputTypeHexadecimal: // Disable special handling. return NO_AUTO_TEXT | NO_PREDICTION | NO_AUTO_CORRECTION; default: break; } return DEFAULT_STYLE; } static VirtualKeyboardType convertStringToKeyboardType(const AtomicString& string) { DEFINE_STATIC_LOCAL(AtomicString, Default, ("default")); DEFINE_STATIC_LOCAL(AtomicString, Url, ("url")); DEFINE_STATIC_LOCAL(AtomicString, Email, ("email")); DEFINE_STATIC_LOCAL(AtomicString, Password, ("password")); DEFINE_STATIC_LOCAL(AtomicString, Web, ("web")); DEFINE_STATIC_LOCAL(AtomicString, Number, ("number")); DEFINE_STATIC_LOCAL(AtomicString, Symbol, ("symbol")); DEFINE_STATIC_LOCAL(AtomicString, Phone, ("phone")); DEFINE_STATIC_LOCAL(AtomicString, Pin, ("pin")); DEFINE_STATIC_LOCAL(AtomicString, Hex, ("hexadecimal")); if (string.isEmpty()) return VKBTypeNotSet; if (equalIgnoringCase(string, Default)) return VKBTypeDefault; if (equalIgnoringCase(string, Url)) return VKBTypeUrl; if (equalIgnoringCase(string, Email)) return VKBTypeEmail; if (equalIgnoringCase(string, Password)) return VKBTypePassword; if (equalIgnoringCase(string, Web)) return VKBTypeWeb; if (equalIgnoringCase(string, Number)) return VKBTypeNumPunc; if (equalIgnoringCase(string, Symbol)) return VKBTypeSymbol; if (equalIgnoringCase(string, Phone)) return VKBTypePhone; if (equalIgnoringCase(string, Pin) || equalIgnoringCase(string, Hex)) return VKBTypePin; return VKBTypeNotSet; } static VirtualKeyboardType keyboardTypeAttribute(const WebCore::Element* element) { DEFINE_STATIC_LOCAL(QualifiedName, keyboardTypeAttr, (nullAtom, "data-blackberry-virtual-keyboard-type", nullAtom)); if (element->fastHasAttribute(keyboardTypeAttr)) { AtomicString attributeString = element->fastGetAttribute(keyboardTypeAttr); return convertStringToKeyboardType(attributeString); } if (element->isFormControlElement()) { const HTMLFormControlElement* formElement = static_cast(element); if (formElement->form() && formElement->form()->fastHasAttribute(keyboardTypeAttr)) { AtomicString attributeString = formElement->form()->fastGetAttribute(keyboardTypeAttr); return convertStringToKeyboardType(attributeString); } } return VKBTypeNotSet; } static VirtualKeyboardEnterKeyType convertStringToKeyboardEnterKeyType(const AtomicString& string) { DEFINE_STATIC_LOCAL(AtomicString, Default, ("default")); DEFINE_STATIC_LOCAL(AtomicString, Connect, ("connect")); DEFINE_STATIC_LOCAL(AtomicString, Done, ("done")); DEFINE_STATIC_LOCAL(AtomicString, Go, ("go")); DEFINE_STATIC_LOCAL(AtomicString, Join, ("join")); DEFINE_STATIC_LOCAL(AtomicString, Next, ("next")); DEFINE_STATIC_LOCAL(AtomicString, Search, ("search")); DEFINE_STATIC_LOCAL(AtomicString, Send, ("send")); DEFINE_STATIC_LOCAL(AtomicString, Submit, ("submit")); if (string.isEmpty()) return VKBEnterKeyNotSet; if (equalIgnoringCase(string, Default)) return VKBEnterKeyDefault; if (equalIgnoringCase(string, Connect)) return VKBEnterKeyConnect; if (equalIgnoringCase(string, Done)) return VKBEnterKeyDone; if (equalIgnoringCase(string, Go)) return VKBEnterKeyGo; if (equalIgnoringCase(string, Join)) return VKBEnterKeyJoin; if (equalIgnoringCase(string, Next)) return VKBEnterKeyNext; if (equalIgnoringCase(string, Search)) return VKBEnterKeySearch; if (equalIgnoringCase(string, Send)) return VKBEnterKeySend; if (equalIgnoringCase(string, Submit)) return VKBEnterKeySubmit; return VKBEnterKeyNotSet; } static VirtualKeyboardEnterKeyType keyboardEnterKeyTypeAttribute(const WebCore::Element* element) { DEFINE_STATIC_LOCAL(QualifiedName, keyboardEnterKeyTypeAttr, (nullAtom, "data-blackberry-virtual-keyboard-enter-key", nullAtom)); if (element->fastHasAttribute(keyboardEnterKeyTypeAttr)) { AtomicString attributeString = element->fastGetAttribute(keyboardEnterKeyTypeAttr); return convertStringToKeyboardEnterKeyType(attributeString); } if (element->isFormControlElement()) { const HTMLFormControlElement* formElement = static_cast(element); if (formElement->form() && formElement->form()->fastHasAttribute(keyboardEnterKeyTypeAttr)) { AtomicString attributeString = formElement->form()->fastGetAttribute(keyboardEnterKeyTypeAttr); return convertStringToKeyboardEnterKeyType(attributeString); } } return VKBEnterKeyNotSet; } void InputHandler::setProcessingChange(bool processingChange) { if (processingChange == m_processingChange) return; m_processingChange = processingChange; if (!m_processingChange) m_webPage->m_selectionHandler->inputHandlerDidFinishProcessingChange(); } WTF::String InputHandler::elementText() { if (!isActiveTextEdit()) return WTF::String(); return DOMSupport::inputElementText(m_currentFocusElement.get()); } BlackBerryInputType InputHandler::elementType(Element* element) const { // is bundled with input so we need to check the formControlName // first to differentiate it from input which is essentially the same as // isIndex has been deprecated. if (element->formControlName() == HTMLNames::isindexTag) return InputTypeIsIndex; if (const HTMLInputElement* inputElement = static_cast(element->toInputElement())) return convertInputType(inputElement); if (element->hasTagName(HTMLNames::textareaTag)) return InputTypeTextArea; // Default to InputTypeTextArea for content editable fields. return InputTypeTextArea; } void InputHandler::focusedNodeChanged() { ASSERT(m_webPage->m_page->focusController()); Frame* frame = m_webPage->m_page->focusController()->focusedOrMainFrame(); if (!frame || !frame->document()) return; Node* node = frame->document()->focusedNode(); if (isActiveTextEdit() && m_currentFocusElement == node) { notifyClientOfKeyboardVisibilityChange(true); return; } if (node && node->isElementNode()) { Element* element = static_cast(node); if (DOMSupport::isElementTypePlugin(element)) { setPluginFocused(element); return; } if (DOMSupport::isTextBasedContentEditableElement(element)) { // Focused node is a text based input field, textarea or content editable field. setElementFocused(element); return; } } if (isActiveTextEdit() && m_currentFocusElement->isContentEditable()) { // This is a special handler for content editable fields. The focus node is the top most // field that is content editable, however, by enabling / disabling designmode and // content editable, it is possible through javascript or selection to trigger the focus node to // change even while maintaining editing on the same selection point. If the focus element // isn't contentEditable, but the current selection is, don't send a change notification. // When processing changes selection changes occur that may reset the focus element // and could potentially cause a crash as m_currentFocusElement should not be // changed during processing of an EditorCommand. if (processingChange()) return; Frame* frame = m_currentFocusElement->document()->frame(); ASSERT(frame); // Test the current selection to make sure that the content in focus is still content // editable. This may mean Javascript triggered a focus change by modifying the // top level parent of this object's content editable state without actually modifying // this particular object. // Example site: html5demos.com/contentEditable - blur event triggers focus change. if (frame == m_webPage->focusedOrMainFrame() && frame->selection()->start().anchorNode() && frame->selection()->start().anchorNode()->isContentEditable()) return; } // No valid focus element found for handling. setElementUnfocused(); } void InputHandler::setPluginFocused(Element* element) { ASSERT(DOMSupport::isElementTypePlugin(element)); if (isActiveTextEdit()) setElementUnfocused(); m_currentFocusElementType = Plugin; m_currentFocusElement = element; } static bool convertStringToWchar(const String& string, wchar_t* dest, int destCapacity, int* destLength) { ASSERT(dest); if (!string.length()) { destLength = 0; return true; } UErrorCode ec = U_ZERO_ERROR; // wchar_t strings sent to IMF are 32 bit so casting to UChar32 is safe. u_strToUTF32(reinterpret_cast(dest), destCapacity, destLength, string.characters(), string.length(), &ec); if (ec) { logAlways(LogLevelCritical, "InputHandler::convertStringToWchar Error converting string ec (%d).", ec); destLength = 0; return false; } return true; } static bool convertStringToWcharVector(const String& string, WTF::Vector& wcharString) { ASSERT(wcharString.isEmpty()); int length = string.length(); if (!length) return true; if (!wcharString.tryReserveCapacity(length + 1)) { logAlways(LogLevelCritical, "InputHandler::convertStringToWcharVector Cannot allocate memory for string."); return false; } int destLength = 0; if (!convertStringToWchar(string, wcharString.data(), length + 1, &destLength)) return false; wcharString.resize(destLength); return true; } static String convertSpannableStringToString(spannable_string_t* src) { if (!src || !src->str || !src->length) return String(); WTF::Vector dest; int destCapacity = (src->length * 2) + 1; if (!dest.tryReserveCapacity(destCapacity)) { logAlways(LogLevelCritical, "InputHandler::convertSpannableStringToString Cannot allocate memory for string."); return String(); } int destLength = 0; UErrorCode ec = U_ZERO_ERROR; // wchar_t strings sent from IMF are 32 bit so casting to UChar32 is safe. u_strFromUTF32(dest.data(), destCapacity, &destLength, reinterpret_cast(src->str), src->length, &ec); if (ec) { logAlways(LogLevelCritical, "InputHandler::convertSpannableStringToString Error converting string ec (%d).", ec); return String(); } dest.resize(destLength); return String(dest.data(), destLength); } void InputHandler::sendLearnTextDetails(const WTF::String& string) { Vector wcharString; if (!convertStringToWcharVector(string, wcharString) || wcharString.isEmpty()) return; m_webPage->m_client->inputLearnText(wcharString.data(), wcharString.size()); } void InputHandler::learnText() { if (!isActiveTextEdit()) return; // Do not send (or calculate) the text when the field is NO_PREDICTION or NO_AUTO_TEXT. if (m_currentFocusElementTextEditMask & NO_PREDICTION || m_currentFocusElementTextEditMask & NO_AUTO_TEXT) return; String textInField(elementText()); textInField = textInField.substring(std::max(0, static_cast(textInField.length() - MaxLearnTextDataSize)), textInField.length()); textInField.remove(0, textInField.find(" ")); // Build up the 500 character strings in word chunks. // Spec says 1000, but memory corruption has been observed. ASSERT(textInField.length() <= MaxLearnTextDataSize); if (textInField.isEmpty()) return; InputLog(LogLevelInfo, "InputHandler::learnText '%s'", textInField.latin1().data()); sendLearnTextDetails(textInField); } void InputHandler::requestCheckingOfString(PassRefPtr textCheckingRequest) { m_request = textCheckingRequest; if (!m_request) { SpellingLog(LogLevelWarn, "InputHandler::requestCheckingOfString did not receive a valid request."); return; } unsigned requestLength = m_request->text().length(); // Check if the field should be spellchecked. if (!isActiveTextEdit() || !shouldSpellCheckElement(m_currentFocusElement.get()) || requestLength < 2) { m_request->didCancel(); return; } if (requestLength > MaxSpellCheckingStringLength) { // Cancel this request and send it off in newly created chunks. m_request->didCancel(); if (m_currentFocusElement->document() && m_currentFocusElement->document()->frame() && m_currentFocusElement->document()->frame()->selection()) { // Convert from position back to selection so we can expand the range to include the previous line. This should handle cases when the user hits // enter to finish composing a word and create a new line. VisiblePosition caretPosition = m_currentFocusElement->document()->frame()->selection()->start(); VisibleSelection visibleSelection = VisibleSelection(previousLinePosition(caretPosition, caretPosition.lineDirectionPointForBlockDirectionNavigation()), caretPosition); spellCheckBlock(visibleSelection, TextCheckingProcessIncremental); } return; } wchar_t* checkingString = (wchar_t*)malloc(sizeof(wchar_t) * (requestLength + 1)); if (!checkingString) { logAlways(LogLevelCritical, "InputHandler::requestCheckingOfString Cannot allocate memory for string."); m_request->didCancel(); return; } int paragraphLength = 0; if (!convertStringToWchar(m_request->text(), checkingString, requestLength + 1, ¶graphLength)) { logAlways(LogLevelCritical, "InputHandler::requestCheckingOfString Failed to convert String to wchar type."); free(checkingString); m_request->didCancel(); return; } m_processingTransactionId = m_webPage->m_client->checkSpellingOfStringAsync(checkingString, paragraphLength); free(checkingString); // If the call to the input service did not go through, then cancel the request so we don't block endlessly. // This should still take transactionId as a parameter to maintain the same behavior as if InputMethodSupport // were to cancel a request during processing. if (m_processingTransactionId == -1) { // Error before sending request to input service. m_request->didCancel(); return; } } void InputHandler::spellCheckingRequestCancelled(int32_t transactionId) { SpellingLog(LogLevelWarn, "InputHandler::spellCheckingRequestCancelled Expected transaction id %d, received %d. %s" , transactionId , m_processingTransactionId , transactionId == m_processingTransactionId ? "" : "We are out of sync with input service."); m_request->didCancel(); m_processingTransactionId = -1; } void InputHandler::spellCheckingRequestProcessed(int32_t transactionId, spannable_string_t* spannableString) { SpellingLog(LogLevelWarn, "InputHandler::spellCheckingRequestProcessed Expected transaction id %d, received %d. %s" , transactionId , m_processingTransactionId , transactionId == m_processingTransactionId ? "" : "We are out of sync with input service."); if (!spannableString || !isActiveTextEdit()) { SpellingLog(LogLevelWarn, "InputHandler::spellCheckingRequestProcessed Cancelling request with transactionId %d.", transactionId); m_request->didCancel(); m_processingTransactionId = -1; return; } Vector results; // Convert the spannableString to TextCheckingResult then append to results vector. String replacement; TextCheckingResult textCheckingResult; textCheckingResult.type = TextCheckingTypeSpelling; textCheckingResult.replacement = replacement; textCheckingResult.location = 0; textCheckingResult.length = 0; span_t* span = spannableString->spans; for (unsigned int i = 0; i < spannableString->spans_count; i++) { if (!span) break; if (span->end < span->start) { m_request->didCancel(); return; } if (span->attributes_mask & MISSPELLED_WORD_ATTRIB) { textCheckingResult.location = span->start; // The end point includes the character that it is before. Ie, 0, 0 // applies to the first character as the end point includes the character // at the position. This means the endPosition is always +1. textCheckingResult.length = span->end - span->start + 1; results.append(textCheckingResult); } span++; } m_request->didSucceed(results); } SpellChecker* InputHandler::getSpellChecker() { if (!m_currentFocusElement || !m_currentFocusElement->document()) return 0; if (Frame* frame = m_currentFocusElement->document()->frame()) if (Editor* editor = frame->editor()) return editor->spellChecker(); return 0; } bool InputHandler::shouldRequestSpellCheckingOptionsForPoint(Platform::IntPoint& point, const Element* touchedElement, imf_sp_text_t& spellCheckingOptionRequest) { if (!isActiveTextEdit() || touchedElement != m_currentFocusElement) return false; LayoutPoint contentPos(m_webPage->mapFromViewportToContents(point)); contentPos = DOMSupport::convertPointToFrame(m_webPage->mainFrame(), m_webPage->focusedOrMainFrame(), roundedIntPoint(contentPos)); Document* document = m_currentFocusElement->document(); ASSERT(document); RenderedDocumentMarker* marker = document->markers()->renderedMarkerContainingPoint(contentPos, DocumentMarker::Spelling); if (!marker) return false; // Populate the marker details in preparation for the request as the marker is // not guaranteed to be valid after the cursor is placed. spellCheckingOptionRequest.startTextPosition = marker->startOffset(); spellCheckingOptionRequest.endTextPosition = marker->endOffset(); SpellingLog(LogLevelInfo, "InputHandler::shouldRequestSpellCheckingOptionsForPoint Found spelling marker at point %d, %d\nMarker start %d end %d", point.x(), point.y(), spellCheckingOptionRequest.startTextPosition, spellCheckingOptionRequest.endTextPosition); return true; } void InputHandler::requestSpellingCheckingOptions(imf_sp_text_t& spellCheckingOptionRequest) { // If the caret is no longer active, no message should be sent. if (m_webPage->focusedOrMainFrame()->selection()->selectionType() != VisibleSelection::CaretSelection) return; // imf_sp_text_t should be generated in pixel viewport coordinates. WebCore::IntRect caretLocation = m_webPage->focusedOrMainFrame()->selection()->selection().visibleStart().absoluteCaretBounds(); caretLocation = m_webPage->mapToTransformed(m_webPage->focusedOrMainFrame()->view()->contentsToWindow(enclosingIntRect(caretLocation))); m_webPage->clipToTransformedContentsRect(caretLocation); spellCheckingOptionRequest.caret_rect.caret_top_x = caretLocation.x(); spellCheckingOptionRequest.caret_rect.caret_top_y = caretLocation.y(); spellCheckingOptionRequest.caret_rect.caret_bottom_x = caretLocation.x(); spellCheckingOptionRequest.caret_rect.caret_bottom_y = caretLocation.y() + caretLocation.height(); SpellingLog(LogLevelInfo, "InputHandler::requestSpellingCheckingOptions Sending request:\ncaret_rect.caret_top_x = %d\ncaret_rect.caret_top_y = %d" \ "\ncaret_rect.caret_bottom_x = %d\ncaret_rect.caret_bottom_y = %d\nstartTextPosition = %d\nendTextPosition = %d", spellCheckingOptionRequest.caret_rect.caret_top_x, spellCheckingOptionRequest.caret_rect.caret_top_y, spellCheckingOptionRequest.caret_rect.caret_bottom_x, spellCheckingOptionRequest.caret_rect.caret_bottom_y, spellCheckingOptionRequest.startTextPosition, spellCheckingOptionRequest.endTextPosition); m_webPage->m_client->requestSpellingCheckingOptions(spellCheckingOptionRequest); } void InputHandler::setElementUnfocused(bool refocusOccuring) { if (isActiveTextEdit()) { FocusLog(LogLevelInfo, "InputHandler::setElementUnfocused"); // Pass any text into the field to IMF to learn. learnText(); // End any composition that is in progress. finishComposition(); // Only hide the keyboard if we aren't refocusing on a new input field. if (!refocusOccuring) notifyClientOfKeyboardVisibilityChange(false, true /* triggeredByFocusChange */); m_webPage->m_client->inputFocusLost(); // If the frame selection isn't focused, focus it. if (!m_currentFocusElement->document()->frame()->selection()->isFocused()) m_currentFocusElement->document()->frame()->selection()->setFocused(true); } // Clear the node details. m_currentFocusElement = 0; m_currentFocusElementType = TextEdit; } bool InputHandler::isInputModeEnabled() const { // Input mode is enabled when set, or when dump render tree or always show keyboard setting is enabled. return m_inputModeEnabled || m_webPage->m_dumpRenderTree || Platform::Settings::instance()->alwaysShowKeyboardOnFocus(); } void InputHandler::setInputModeEnabled(bool active) { FocusLog(LogLevelInfo, "InputHandler::setInputModeEnabled '%s', override is '%s'" , active ? "true" : "false" , m_webPage->m_dumpRenderTree || Platform::Settings::instance()->alwaysShowKeyboardOnFocus() ? "true" : "false"); m_inputModeEnabled = active; // If the frame selection isn't focused, focus it. if (isInputModeEnabled() && isActiveTextEdit() && !m_currentFocusElement->document()->frame()->selection()->isFocused()) m_currentFocusElement->document()->frame()->selection()->setFocused(true); } void InputHandler::setElementFocused(Element* element) { ASSERT(DOMSupport::isTextBasedContentEditableElement(element)); ASSERT(element && element->document() && element->document()->frame()); #ifdef ENABLE_SPELLING_LOG BlackBerry::Platform::StopWatch timer; timer.start(); #endif if (!element || !(element->document())) return; Frame* frame = element->document()->frame(); if (!frame) return; if (frame->selection()->isFocused() != isInputModeEnabled()) frame->selection()->setFocused(isInputModeEnabled()); // Clear the existing focus node details. setElementUnfocused(true /*refocusOccuring*/); // Mark this element as active and add to frame set. m_currentFocusElement = element; m_currentFocusElementType = TextEdit; // Send details to the client about this element. BlackBerryInputType type = elementType(element); m_currentFocusElementTextEditMask = inputStyle(type, element); VirtualKeyboardType keyboardType = keyboardTypeAttribute(element); VirtualKeyboardEnterKeyType enterKeyType = keyboardEnterKeyTypeAttribute(element); if (enterKeyType == VKBEnterKeyNotSet && type != InputTypeTextArea) { if (element->isFormControlElement()) { const HTMLFormControlElement* formElement = static_cast(element); if (formElement->form() && formElement->form()->defaultButton()) enterKeyType = VKBEnterKeySubmit; } } FocusLog(LogLevelInfo, "InputHandler::setElementFocused, Type=%d, Style=%d, Keyboard Type=%d, Enter Key=%d", type, m_currentFocusElementTextEditMask, keyboardType, enterKeyType); m_webPage->m_client->inputFocusGained(type, m_currentFocusElementTextEditMask, keyboardType, enterKeyType); handleInputLocaleChanged(m_webPage->m_webSettings->isWritingDirectionRTL()); if (!m_delayKeyboardVisibilityChange) notifyClientOfKeyboardVisibilityChange(true, true /* triggeredByFocusChange */); #ifdef ENABLE_SPELLING_LOG SpellingLog(LogLevelInfo, "InputHandler::setElementFocused Focusing the field took %f seconds.", timer.elapsed()); #endif // Check if the field should be spellchecked. if (!shouldSpellCheckElement(element)) return; // Spellcheck the field in its entirety. VisibleSelection focusedBlock = DOMSupport::visibleSelectionForInputElement(element); spellCheckBlock(focusedBlock, TextCheckingProcessBatch); #ifdef ENABLE_SPELLING_LOG SpellingLog(LogLevelInfo, "InputHandler::setElementFocused Spellchecking the field increased the total time to focus to %f seconds.", timer.elapsed()); #endif } bool InputHandler::shouldSpellCheckElement(const Element* element) const { DOMSupport::AttributeState spellCheckAttr = DOMSupport::elementSupportsSpellCheck(element); // Explicitly set to off. if (spellCheckAttr == DOMSupport::Off) return false; // Undefined and part of a set of cases which we do not wish to check. This includes user names and email addresses, so we are piggybacking on NoAutocomplete cases. if (spellCheckAttr == DOMSupport::Default && (m_currentFocusElementTextEditMask & NO_AUTO_TEXT)) return false; return true; } void InputHandler::spellCheckBlock(VisibleSelection& visibleSelection, TextCheckingProcessType textCheckingProcessType) { if (!isActiveTextEdit()) return; RefPtr rangeForSpellChecking = visibleSelection.toNormalizedRange(); if (!rangeForSpellChecking || !rangeForSpellChecking->text() || !rangeForSpellChecking->text().length()) return; SpellChecker* spellChecker = getSpellChecker(); if (!spellChecker) { SpellingLog(LogLevelInfo, "InputHandler::spellCheckBlock Failed to spellcheck the current focused element."); return; } // If we have a batch request, try to send off the entire block. if (textCheckingProcessType == TextCheckingProcessBatch) { // If total block text is under the limited amount, send the entire chunk. if (rangeForSpellChecking->text().length() < MaxSpellCheckingStringLength) { spellChecker->requestCheckingFor(SpellCheckRequest::create(TextCheckingTypeSpelling, TextCheckingProcessBatch, rangeForSpellChecking, rangeForSpellChecking)); return; } } // Since we couldn't check the entire block at once, set up starting and ending markers to fire incrementally. VisiblePosition startPos = visibleSelection.visibleStart(); VisiblePosition startOfCurrentLine = startOfLine(startPos); VisiblePosition endOfCurrentLine = endOfLine(startOfCurrentLine); while (!isEndOfBlock(startOfCurrentLine)) { // Create a selection with the start and end points of the line, and convert to Range to create a SpellCheckRequest. rangeForSpellChecking = VisibleSelection(startOfCurrentLine, endOfCurrentLine).toNormalizedRange(); if (rangeForSpellChecking->text().length() < MaxSpellCheckingStringLength) { startOfCurrentLine = nextLinePosition(startOfCurrentLine, startOfCurrentLine.lineDirectionPointForBlockDirectionNavigation()); endOfCurrentLine = endOfLine(startOfCurrentLine); } else { // Iterate through words from the start of the line to the end. rangeForSpellChecking = getRangeForSpellCheckWithFineGranularity(startOfCurrentLine, endOfCurrentLine); if (!rangeForSpellChecking) { SpellingLog(LogLevelWarn, "InputHandler::spellCheckBlock Failed to set text range for spellchecking"); return; } startOfCurrentLine = VisiblePosition(rangeForSpellChecking->endPosition()); endOfCurrentLine = endOfLine(startOfCurrentLine); rangeForSpellChecking = DOMSupport::trimWhitespaceFromRange(VisiblePosition(rangeForSpellChecking->startPosition()), VisiblePosition(rangeForSpellChecking->endPosition())); } SpellingLog(LogLevelInfo, "InputHandler::spellCheckBlock Substring text is '%s', of size %d", rangeForSpellChecking->text().latin1().data(), rangeForSpellChecking->text().length()); // Call spellcheck with substring. spellChecker->requestCheckingFor(SpellCheckRequest::create(TextCheckingTypeSpelling, TextCheckingProcessBatch, rangeForSpellChecking, rangeForSpellChecking)); } } PassRefPtr InputHandler::getRangeForSpellCheckWithFineGranularity(VisiblePosition startPosition, VisiblePosition endPosition) { VisiblePosition endOfCurrentWord = endOfWord(startPosition); // Keep iterating until one of our cases is hit, or we've incremented the starting position right to the end. while (startPosition != endPosition) { // Check the text length within this range. if (VisibleSelection(startPosition, endOfCurrentWord).toNormalizedRange()->text().length() >= MaxSpellCheckingStringLength) { // If this is not the first word, return a Range with end boundary set to the previous word. if (startOfWord(endOfCurrentWord, LeftWordIfOnBoundary) != startPosition && !DOMSupport::isEmptyRangeOrAllSpaces(startPosition, endOfCurrentWord)) return VisibleSelection(startPosition, endOfWord(previousWordPosition(endOfCurrentWord), LeftWordIfOnBoundary)).toNormalizedRange(); // Our first word has gone over the character limit. Increment the starting position past an uncheckable word. startPosition = endOfCurrentWord; endOfCurrentWord = endOfWord(nextWordPosition(endOfCurrentWord)); } else if (endOfCurrentWord == endPosition) { // Return the last segment if the end of our word lies at the end of the range. return VisibleSelection(startPosition, endPosition).toNormalizedRange(); } else { // Increment the current word. endOfCurrentWord = endOfWord(nextWordPosition(endOfCurrentWord)); } } return 0; } bool InputHandler::openDatePopup(HTMLInputElement* element, BlackBerryInputType type) { if (!element || element->disabled() || !DOMSupport::isDateTimeInputField(element)) return false; if (isActiveTextEdit()) clearCurrentFocusElement(); switch (type) { case BlackBerry::Platform::InputTypeDate: case BlackBerry::Platform::InputTypeTime: case BlackBerry::Platform::InputTypeDateTime: case BlackBerry::Platform::InputTypeDateTimeLocal: case BlackBerry::Platform::InputTypeMonth: { // Check if popup already exists, close it if does. m_webPage->m_page->chrome()->client()->closePagePopup(0); String value = element->value(); String min = element->getAttribute(HTMLNames::minAttr).string(); String max = element->getAttribute(HTMLNames::maxAttr).string(); double step = element->getAttribute(HTMLNames::stepAttr).toDouble(); DatePickerClient* client = new DatePickerClient(type, value, min, max, step, m_webPage, element); return m_webPage->m_page->chrome()->client()->openPagePopup(client, WebCore::IntRect()); } default: // Other types not supported return false; } } bool InputHandler::openColorPopup(HTMLInputElement* element) { if (!element || element->disabled() || !DOMSupport::isColorInputField(element)) return false; if (isActiveTextEdit()) clearCurrentFocusElement(); m_currentFocusElement = element; m_currentFocusElementType = TextPopup; // Check if popup already exists, close it if does. m_webPage->m_page->chrome()->client()->closePagePopup(0); ColorPickerClient* client = new ColorPickerClient(element->value(), m_webPage, element); return m_webPage->m_page->chrome()->client()->openPagePopup(client, WebCore::IntRect()); } void InputHandler::setInputValue(const WTF::String& value) { if (!isActiveTextPopup()) return; HTMLInputElement* inputElement = static_cast(m_currentFocusElement.get()); inputElement->setValue(value); clearCurrentFocusElement(); } void InputHandler::nodeTextChanged(const Node* node) { if (processingChange() || !node) return; if (node != m_currentFocusElement) return; InputLog(LogLevelInfo, "InputHandler::nodeTextChanged"); m_webPage->m_client->inputTextChanged(); // Remove the attributed text markers as the previous call triggered an end to // the composition. removeAttributedTextMarker(); } WebCore::IntRect InputHandler::boundingBoxForInputField() { if (!isActiveTextEdit()) return WebCore::IntRect(); if (!m_currentFocusElement->renderer()) return WebCore::IntRect(); return m_currentFocusElement->renderer()->absoluteBoundingBoxRect(); } void InputHandler::ensureFocusTextElementVisible(CaretScrollType scrollType) { if (!isActiveTextEdit() || !isInputModeEnabled() || !m_currentFocusElement->document()) return; if (!(Platform::Settings::instance()->allowedScrollAdjustmentForInputFields() & scrollType)) return; Frame* elementFrame = m_currentFocusElement->document()->frame(); if (!elementFrame) return; Frame* mainFrame = m_webPage->mainFrame(); if (!mainFrame) return; FrameView* mainFrameView = mainFrame->view(); if (!mainFrameView) return; WebCore::IntRect selectionFocusRect; switch (elementFrame->selection()->selectionType()) { case VisibleSelection::CaretSelection: selectionFocusRect = elementFrame->selection()->absoluteCaretBounds(); break; case VisibleSelection::RangeSelection: { Position selectionPosition; if (m_webPage->m_selectionHandler->lastUpdatedEndPointIsValid()) selectionPosition = elementFrame->selection()->end(); else selectionPosition = elementFrame->selection()->start(); selectionFocusRect = VisiblePosition(selectionPosition).absoluteCaretBounds(); break; } case VisibleSelection::NoSelection: if (m_focusZoomScale) { m_webPage->zoomAboutPoint(m_focusZoomScale, m_focusZoomLocation); m_focusZoomScale = 0.0; m_focusZoomLocation = WebCore::IntPoint(); } return; } int fontHeight = selectionFocusRect.height(); m_webPage->suspendBackingStore(); // If the text is too small, zoom in to make it a minimum size. // The minimum size being defined as 3 mm is a good value based on my observations. static const int s_minimumTextHeightInPixels = Graphics::Screen::primaryScreen()->heightInMMToPixels(3); if (fontHeight && fontHeight * m_webPage->currentScale() < s_minimumTextHeightInPixels) { if (!m_focusZoomScale) { m_focusZoomScale = m_webPage->currentScale(); m_focusZoomLocation = selectionFocusRect.location(); } m_webPage->zoomAboutPoint(s_minimumTextHeightInPixels / fontHeight, m_focusZoomLocation); } else { m_focusZoomScale = 0.0; m_focusZoomLocation = WebCore::IntPoint(); } if (elementFrame != mainFrame) { // Element is in a subframe. // Remove any scroll offset within the subframe to get the point relative to the main frame. selectionFocusRect.move(-elementFrame->view()->scrollPosition().x(), -elementFrame->view()->scrollPosition().y()); // Adjust the selection rect based on the frame offset in relation to the main frame if it's a subframe. if (elementFrame->ownerRenderer()) { WebCore::IntPoint frameOffset = elementFrame->ownerRenderer()->absoluteContentBox().location(); selectionFocusRect.move(frameOffset.x(), frameOffset.y()); } } Position start = elementFrame->selection()->start(); if (start.anchorNode() && start.anchorNode()->renderer()) { if (RenderLayer* layer = start.anchorNode()->renderer()->enclosingLayer()) { WebCore::IntRect actualScreenRect = WebCore::IntRect(mainFrameView->scrollPosition(), m_webPage->actualVisibleSize()); ScrollAlignment horizontalScrollAlignment = ScrollAlignment::alignToEdgeIfNeeded; ScrollAlignment verticalScrollAlignment = ScrollAlignment::alignToEdgeIfNeeded; if (scrollType != EdgeIfNeeded) { // Align the selection rect if possible so that we show the field's // outline if the caret is at the edge of the field. if (RenderObject* focusedRenderer = m_currentFocusElement->renderer()) { WebCore::IntRect nodeOutlineBounds = focusedRenderer->absoluteOutlineBounds(); WebCore::IntRect caretAtEdgeRect = rectForCaret(0); int paddingX = abs(caretAtEdgeRect.x() - nodeOutlineBounds.x()); int paddingY = abs(caretAtEdgeRect.y() - nodeOutlineBounds.y()); if (selectionFocusRect.x() - paddingX == nodeOutlineBounds.x()) selectionFocusRect.setX(nodeOutlineBounds.x()); else if (selectionFocusRect.maxX() + paddingX == nodeOutlineBounds.maxX()) selectionFocusRect.setX(nodeOutlineBounds.maxX() - selectionFocusRect.width()); if (selectionFocusRect.y() - paddingY == nodeOutlineBounds.y()) selectionFocusRect.setY(nodeOutlineBounds.y() - selectionFocusRect.height()); else if (selectionFocusRect.maxY() + paddingY == nodeOutlineBounds.maxY()) selectionFocusRect.setY(nodeOutlineBounds.maxY() - selectionFocusRect.height()); // If the editing point is on the left hand side of the screen when the node's // rect is edge aligned, edge align the node rect. if (selectionFocusRect.x() - caretAtEdgeRect.x() < actualScreenRect.width() / 2) selectionFocusRect.setX(nodeOutlineBounds.x()); else horizontalScrollAlignment = ScrollAlignment::alignCenterIfNeeded; } verticalScrollAlignment = (scrollType == CenterAlways) ? ScrollAlignment::alignCenterAlways : ScrollAlignment::alignCenterIfNeeded; } // Pad the rect to improve the visual appearance. // Convert the padding back from transformed to ensure a consistent padding regardless of // zoom level as controls do not zoom. static const int s_focusRectPaddingSize = Graphics::Screen::primaryScreen()->heightInMMToPixels(3); selectionFocusRect.inflate(m_webPage->mapFromTransformed(WebCore::IntSize(0, s_focusRectPaddingSize)).height()); WebCore::IntRect revealRect(layer->getRectToExpose(actualScreenRect, selectionFocusRect, horizontalScrollAlignment, verticalScrollAlignment)); mainFrameView->setConstrainsScrollingToContentEdge(false); // In order to adjust the scroll position to ensure the focused input field is visible, // we allow overscrolling. However this overscroll has to be strictly allowed towards the // bottom of the page on the y axis only, where the virtual keyboard pops up from. WebCore::IntPoint scrollLocation = revealRect.location(); scrollLocation.clampNegativeToZero(); WebCore::IntPoint maximumScrollPosition = WebCore::IntPoint(mainFrameView->contentsWidth() - actualScreenRect.width(), mainFrameView->contentsHeight() - actualScreenRect.height()); scrollLocation = scrollLocation.shrunkTo(maximumScrollPosition); mainFrameView->setScrollPosition(scrollLocation); mainFrameView->setConstrainsScrollingToContentEdge(true); } } m_webPage->resumeBackingStore(); } void InputHandler::ensureFocusPluginElementVisible() { if (!isActivePlugin() || !m_currentFocusElement->document()) return; Frame* elementFrame = m_currentFocusElement->document()->frame(); if (!elementFrame) return; Frame* mainFrame = m_webPage->mainFrame(); if (!mainFrame) return; FrameView* mainFrameView = mainFrame->view(); if (!mainFrameView) return; WebCore::IntRect selectionFocusRect; RenderWidget* renderWidget = static_cast(m_currentFocusElement->renderer()); if (renderWidget) { PluginView* pluginView = static_cast(renderWidget->widget()); if (pluginView) selectionFocusRect = pluginView->ensureVisibleRect(); } if (selectionFocusRect.isEmpty()) return; // FIXME: We may need to scroll the subframe (recursively) in the future. Revisit this... if (elementFrame != mainFrame) { // Element is in a subframe. // Remove any scroll offset within the subframe to get the point relative to the main frame. selectionFocusRect.move(-elementFrame->view()->scrollPosition().x(), -elementFrame->view()->scrollPosition().y()); // Adjust the selection rect based on the frame offset in relation to the main frame if it's a subframe. if (elementFrame->ownerRenderer()) { WebCore::IntPoint frameOffset = elementFrame->ownerRenderer()->absoluteContentBox().location(); selectionFocusRect.move(frameOffset.x(), frameOffset.y()); } } WebCore::IntRect actualScreenRect = WebCore::IntRect(mainFrameView->scrollPosition(), m_webPage->actualVisibleSize()); if (actualScreenRect.contains(selectionFocusRect)) return; // Calculate a point such that the center of the requested rectangle // is at the center of the screen. FIXME: If the element was partially on screen // we might want to just bring the offscreen portion into view, someone needs // to decide if that's the behavior we want or not. WebCore::IntPoint pos(selectionFocusRect.center().x() - actualScreenRect.width() / 2, selectionFocusRect.center().y() - actualScreenRect.height() / 2); mainFrameView->setScrollPosition(pos); } void InputHandler::ensureFocusElementVisible(bool centerInView) { if (isActivePlugin()) ensureFocusPluginElementVisible(); else ensureFocusTextElementVisible(centerInView ? CenterAlways : CenterIfNeeded); } void InputHandler::frameUnloaded(const Frame* frame) { if (!isActiveTextEdit()) return; if (m_currentFocusElement->document()->frame() != frame) return; FocusLog(LogLevelInfo, "InputHandler::frameUnloaded"); setElementUnfocused(false /*refocusOccuring*/); } void InputHandler::setDelayKeyboardVisibilityChange(bool value) { m_delayKeyboardVisibilityChange = value; m_pendingKeyboardVisibilityChange = NoChange; } void InputHandler::processPendingKeyboardVisibilityChange() { if (!m_delayKeyboardVisibilityChange) { ASSERT(m_pendingKeyboardVisibilityChange == NoChange); return; } m_delayKeyboardVisibilityChange = false; if (m_pendingKeyboardVisibilityChange == NoChange) return; notifyClientOfKeyboardVisibilityChange(m_pendingKeyboardVisibilityChange == Visible); m_pendingKeyboardVisibilityChange = NoChange; } void InputHandler::notifyClientOfKeyboardVisibilityChange(bool visible, bool triggeredByFocusChange) { // If we aren't ready for input, keyboard changes should be ignored. if (!isInputModeEnabled() && visible) return; // If we are processing a change assume the keyboard is visbile to avoid // flooding the VKB with show requests. if (!triggeredByFocusChange && processingChange() && visible) return; if (!m_delayKeyboardVisibilityChange) { m_webPage->showVirtualKeyboard(visible); return; } m_pendingKeyboardVisibilityChange = visible ? Visible : NotVisible; } bool InputHandler::selectionAtStartOfElement() { if (!isActiveTextEdit()) return false; ASSERT(m_currentFocusElement->document() && m_currentFocusElement->document()->frame()); if (!selectionStart()) return true; return false; } bool InputHandler::selectionAtEndOfElement() { if (!isActiveTextEdit()) return false; ASSERT(m_currentFocusElement->document() && m_currentFocusElement->document()->frame()); return selectionStart() == static_cast(elementText().length()); } int InputHandler::selectionStart() const { return selectionPosition(true); } int InputHandler::selectionEnd() const { return selectionPosition(false); } int InputHandler::selectionPosition(bool start) const { if (!m_currentFocusElement->document() || !m_currentFocusElement->document()->frame()) return 0; if (HTMLTextFormControlElement* controlElement = DOMSupport::toTextControlElement(m_currentFocusElement.get())) return start ? controlElement->selectionStart() : controlElement->selectionEnd(); FrameSelection caretSelection; caretSelection.setSelection(m_currentFocusElement->document()->frame()->selection()->selection()); RefPtr rangeSelection = caretSelection.selection().toNormalizedRange(); if (!rangeSelection) return 0; int selectionPointInNode = start ? rangeSelection->startOffset() : rangeSelection->endOffset(); Node* containerNode = start ? rangeSelection->startContainer() : rangeSelection->endContainer(); ExceptionCode ec; RefPtr rangeForNode = rangeOfContents(m_currentFocusElement.get()); rangeForNode->setEnd(containerNode, selectionPointInNode, ec); ASSERT(!ec); return TextIterator::rangeLength(rangeForNode.get()); } void InputHandler::selectionChanged() { // This method can get called during WebPage shutdown process. // If that is the case, just bail out since the client is not // in a safe state of trust to request anything else from it. if (!m_webPage->m_mainFrame) return; if (!isActiveTextEdit()) return; if (processingChange()) return; // Scroll the field if necessary. This must be done even if we are processing // a change as the text change may have moved the caret. IMF doesn't require // the update, but the user needs to see the caret. ensureFocusTextElementVisible(EdgeIfNeeded); ASSERT(m_currentFocusElement->document() && m_currentFocusElement->document()->frame()); int newSelectionStart = selectionStart(); int newSelectionEnd = selectionEnd(); InputLog(LogLevelInfo, "InputHandler::selectionChanged selectionStart=%u, selectionEnd=%u", newSelectionStart, newSelectionEnd); m_webPage->m_client->inputSelectionChanged(newSelectionStart, newSelectionEnd); // Remove the attributed text markers as the previous call triggered an end to // the composition. removeAttributedTextMarker(); } bool InputHandler::setCursorPosition(int location) { return setSelection(location, location); } bool InputHandler::setSelection(int start, int end, bool changeIsPartOfComposition) { if (!isActiveTextEdit()) return false; ASSERT(m_currentFocusElement->document() && m_currentFocusElement->document()->frame()); ProcessingChangeGuard guard(this); VisibleSelection newSelection = DOMSupport::visibleSelectionForRangeInputElement(m_currentFocusElement.get(), start, end); m_currentFocusElement->document()->frame()->selection()->setSelection(newSelection, changeIsPartOfComposition ? 0 : FrameSelection::CloseTyping | FrameSelection::ClearTypingStyle); InputLog(LogLevelInfo, "InputHandler::setSelection selectionStart=%u, selectionEnd=%u", start, end); return start == selectionStart() && end == selectionEnd(); } WebCore::IntRect InputHandler::rectForCaret(int index) { if (!isActiveTextEdit()) return WebCore::IntRect(); ASSERT(m_currentFocusElement->document() && m_currentFocusElement->document()->frame()); if (index < 0 || index > static_cast(elementText().length())) { // Invalid request. return WebCore::IntRect(); } FrameSelection caretSelection; caretSelection.setSelection(DOMSupport::visibleSelectionForRangeInputElement(m_currentFocusElement.get(), index, index).visibleStart()); caretSelection.modify(FrameSelection::AlterationExtend, DirectionForward, CharacterGranularity); return caretSelection.selection().visibleStart().absoluteCaretBounds(); } void InputHandler::cancelSelection() { if (!isActiveTextEdit()) return; ASSERT(m_currentFocusElement->document() && m_currentFocusElement->document()->frame()); int selectionStartPosition = selectionStart(); ProcessingChangeGuard guard(this); setCursorPosition(selectionStartPosition); } bool InputHandler::handleKeyboardInput(const Platform::KeyboardEvent& keyboardEvent, bool changeIsPartOfComposition) { InputLog(LogLevelInfo, "InputHandler::handleKeyboardInput received character=%lc, type=%d", keyboardEvent.character(), keyboardEvent.type()); // Enable input mode if we are processing a key event. setInputModeEnabled(); // If we aren't specifically part of a composition, fail, IMF should never send key input // while composing text. If IMF has failed, we should have already finished the // composition manually. if (!changeIsPartOfComposition && compositionActive()) return false; ProcessingChangeGuard guard(this); unsigned adjustedModifiers = keyboardEvent.modifiers(); if (WTF::isASCIIUpper(keyboardEvent.character())) adjustedModifiers |= KEYMOD_SHIFT; ASSERT(m_webPage->m_page->focusController()); bool keyboardEventHandled = false; if (Frame* focusedFrame = m_webPage->m_page->focusController()->focusedFrame()) { bool isKeyChar = keyboardEvent.type() == Platform::KeyboardEvent::KeyChar; Platform::KeyboardEvent::Type type = keyboardEvent.type(); // If this is a KeyChar type then we handle it as a keydown followed by a key up. if (isKeyChar) type = Platform::KeyboardEvent::KeyDown; Platform::KeyboardEvent adjustedKeyboardEvent(keyboardEvent.character(), type, adjustedModifiers); keyboardEventHandled = focusedFrame->eventHandler()->keyEvent(PlatformKeyboardEvent(adjustedKeyboardEvent)); if (isKeyChar) { type = Platform::KeyboardEvent::KeyUp; adjustedKeyboardEvent = Platform::KeyboardEvent(keyboardEvent.character(), type, adjustedModifiers); keyboardEventHandled = focusedFrame->eventHandler()->keyEvent(PlatformKeyboardEvent(adjustedKeyboardEvent)) || keyboardEventHandled; } if (!changeIsPartOfComposition && type == Platform::KeyboardEvent::KeyUp) ensureFocusTextElementVisible(EdgeIfNeeded); } return keyboardEventHandled; } bool InputHandler::deleteSelection() { if (!isActiveTextEdit()) return false; ASSERT(m_currentFocusElement->document() && m_currentFocusElement->document()->frame()); Frame* frame = m_currentFocusElement->document()->frame(); if (frame->selection()->selectionType() != VisibleSelection::RangeSelection) return false; ASSERT(frame->editor()); return frame->editor()->command("DeleteBackward").execute(); } void InputHandler::insertText(const WTF::String& string) { if (!isActiveTextEdit()) return; ASSERT(m_currentFocusElement->document() && m_currentFocusElement->document()->frame() && m_currentFocusElement->document()->frame()->editor()); Editor* editor = m_currentFocusElement->document()->frame()->editor(); editor->command("InsertText").execute(string); } void InputHandler::clearField() { if (!isActiveTextEdit()) return; ASSERT(m_currentFocusElement->document() && m_currentFocusElement->document()->frame() && m_currentFocusElement->document()->frame()->editor()); Editor* editor = m_currentFocusElement->document()->frame()->editor(); editor->command("SelectAll").execute(); editor->command("DeleteBackward").execute(); } bool InputHandler::executeTextEditCommand(const WTF::String& commandName) { ASSERT(m_webPage->focusedOrMainFrame() && m_webPage->focusedOrMainFrame()->editor()); Editor* editor = m_webPage->focusedOrMainFrame()->editor(); return editor->command(commandName).execute(); } void InputHandler::cut() { executeTextEditCommand("Cut"); } void InputHandler::copy() { executeTextEditCommand("Copy"); } void InputHandler::paste() { executeTextEditCommand("Paste"); } void InputHandler::selectAll() { executeTextEditCommand("SelectAll"); } void InputHandler::addAttributedTextMarker(int start, int end, const AttributeTextStyle& style) { if ((end - start) < 1 || end > static_cast(elementText().length())) return; RefPtr markerRange = DOMSupport::visibleSelectionForRangeInputElement(m_currentFocusElement.get(), start, end).toNormalizedRange(); m_currentFocusElement->document()->markers()->addMarker(markerRange.get(), DocumentMarker::AttributeText, WTF::String("Input Marker"), style); } void InputHandler::removeAttributedTextMarker() { // Remove all attribute text markers. if (m_currentFocusElement && m_currentFocusElement->document()) m_currentFocusElement->document()->markers()->removeMarkers(DocumentMarker::AttributeText); m_composingTextStart = 0; m_composingTextEnd = 0; } void InputHandler::handleInputLocaleChanged(bool isRTL) { if (!isActiveTextEdit()) return; ASSERT(m_currentFocusElement->document() && m_currentFocusElement->document()->frame()); RenderObject* renderer = m_currentFocusElement->renderer(); if (!renderer) return; Editor* editor = m_currentFocusElement->document()->frame()->editor(); ASSERT(editor); if ((renderer->style()->direction() == RTL) != isRTL) editor->setBaseWritingDirection(isRTL ? RightToLeftWritingDirection : LeftToRightWritingDirection); } void InputHandler::clearCurrentFocusElement() { if (m_currentFocusElement) m_currentFocusElement->blur(); } bool InputHandler::willOpenPopupForNode(Node* node) { // This method must be kept synchronized with InputHandler::didNodeOpenPopup. if (!node) return false; ASSERT(!node->isInShadowTree()); if (node->hasTagName(HTMLNames::selectTag) || node->hasTagName(HTMLNames::optionTag)) { // We open list popups for options and selects. return true; } if (node->isElementNode()) { Element* element = static_cast(node); if (DOMSupport::isPopupInputField(element)) return true; } return false; } bool InputHandler::didNodeOpenPopup(Node* node) { // This method must be kept synchronized with InputHandler::willOpenPopupForNode. if (!node) return false; ASSERT(!node->isInShadowTree()); if (node->hasTagName(HTMLNames::selectTag)) return openSelectPopup(static_cast(node)); if (node->hasTagName(HTMLNames::optionTag)) { HTMLOptionElement* optionElement = static_cast(node); return openSelectPopup(optionElement->ownerSelectElement()); } if (HTMLInputElement* element = node->toInputElement()) { if (DOMSupport::isDateTimeInputField(element)) return openDatePopup(element, elementType(element)); if (DOMSupport::isColorInputField(element)) return openColorPopup(element); } return false; } bool InputHandler::openSelectPopup(HTMLSelectElement* select) { if (!select || select->disabled()) return false; // If there's no view, do nothing and return. if (!select->document()->view()) return false; if (isActiveTextEdit()) clearCurrentFocusElement(); m_currentFocusElement = select; m_currentFocusElementType = SelectPopup; const WTF::Vector& listItems = select->listItems(); int size = listItems.size(); bool multiple = select->multiple(); ScopeArray labels; labels.reset(new WebString[size]); // Check if popup already exists, close it if does. m_webPage->m_page->chrome()->client()->closePagePopup(0); bool* enableds = 0; int* itemTypes = 0; bool* selecteds = 0; if (size) { enableds = new bool[size]; itemTypes = new int[size]; selecteds = new bool[size]; for (int i = 0; i < size; i++) { if (listItems[i]->hasTagName(HTMLNames::optionTag)) { HTMLOptionElement* option = static_cast(listItems[i]); labels[i] = option->textIndentedToRespectGroupLabel(); enableds[i] = option->disabled() ? 0 : 1; selecteds[i] = option->selected(); itemTypes[i] = option->parentNode() && option->parentNode()->hasTagName(HTMLNames::optgroupTag) ? TypeOptionInGroup : TypeOption; } else if (listItems[i]->hasTagName(HTMLNames::optgroupTag)) { HTMLOptGroupElement* optGroup = static_cast(listItems[i]); labels[i] = optGroup->groupLabelText(); enableds[i] = optGroup->disabled() ? 0 : 1; selecteds[i] = false; itemTypes[i] = TypeGroup; } else if (listItems[i]->hasTagName(HTMLNames::hrTag)) { enableds[i] = false; selecteds[i] = false; itemTypes[i] = TypeSeparator; } } } SelectPopupClient* selectClient = new SelectPopupClient(multiple, size, labels, enableds, itemTypes, selecteds, m_webPage, select); WebCore::IntRect elementRectInRootView = select->document()->view()->contentsToRootView(enclosingIntRect(select->getRect())); // Fail to create HTML popup, use the old path if (!m_webPage->m_page->chrome()->client()->openPagePopup(selectClient, elementRectInRootView)) m_webPage->m_client->openPopupList(multiple, size, labels, enableds, itemTypes, selecteds); delete[] enableds; delete[] itemTypes; delete[] selecteds; return true; } void InputHandler::setPopupListIndex(int index) { if (index == -2) // Abandon return clearCurrentFocusElement(); if (!isActiveSelectPopup()) return clearCurrentFocusElement(); RenderObject* renderer = m_currentFocusElement->renderer(); if (renderer && renderer->isMenuList()) { RenderMenuList* renderMenu = toRenderMenuList(renderer); renderMenu->hidePopup(); } HTMLSelectElement* selectElement = static_cast(m_currentFocusElement.get()); int optionIndex = selectElement->listToOptionIndex(index); selectElement->optionSelectedByUser(optionIndex, true /* deselect = true */, true /* fireOnChangeNow = false */); clearCurrentFocusElement(); } void InputHandler::setPopupListIndexes(int size, const bool* selecteds) { if (!isActiveSelectPopup()) return clearCurrentFocusElement(); if (size < 0) return; HTMLSelectElement* selectElement = static_cast(m_currentFocusElement.get()); const WTF::Vector& items = selectElement->listItems(); if (items.size() != static_cast(size)) return; HTMLOptionElement* option; for (int i = 0; i < size; i++) { if (items[i]->hasTagName(HTMLNames::optionTag)) { option = static_cast(items[i]); option->setSelectedState(selecteds[i]); } } // Force repaint because we do not send mouse events to the select element // and the element doesn't automatically repaint itself. selectElement->dispatchFormControlChangeEvent(); selectElement->renderer()->repaint(); clearCurrentFocusElement(); } bool InputHandler::setBatchEditingActive(bool active) { if (!isActiveTextEdit()) return false; ASSERT(m_currentFocusElement->document()); ASSERT(m_currentFocusElement->document()->frame()); // FIXME switch this to m_currentFocusElement->document()->frame() when we have separate // backingstore for each frame. BackingStoreClient* backingStoreClient = m_webPage->backingStoreClient(); ASSERT(backingStoreClient); // Enable / Disable the backingstore to prevent visual updates. if (!active) backingStoreClient->backingStore()->resumeScreenAndBackingStoreUpdates(BackingStore::RenderAndBlit); else backingStoreClient->backingStore()->suspendScreenAndBackingStoreUpdates(); return true; } int InputHandler::caretPosition() const { if (!isActiveTextEdit()) return -1; // NOTE: This function is expected to return the start of the selection if // a selection is applied. return selectionStart(); } int relativeLeftOffset(int caretPosition, int leftOffset) { ASSERT(caretPosition >= 0); return std::max(0, caretPosition - leftOffset); } int relativeRightOffset(int caretPosition, unsigned totalLengthOfText, int rightOffset) { ASSERT(caretPosition >= 0); ASSERT(totalLengthOfText < INT_MAX); return std::min(caretPosition + rightOffset, static_cast(totalLengthOfText)); } bool InputHandler::deleteTextRelativeToCursor(int leftOffset, int rightOffset) { if (!isActiveTextEdit() || compositionActive()) return false; ProcessingChangeGuard guard(this); InputLog(LogLevelInfo, "InputHandler::deleteTextRelativeToCursor left %d right %d", leftOffset, rightOffset); int caretOffset = caretPosition(); int start = relativeLeftOffset(caretOffset, leftOffset); int end = relativeRightOffset(caretOffset, elementText().length(), rightOffset); if (!deleteText(start, end)) return false; // Scroll the field if necessary. The automatic update is suppressed // by the processing change guard. ensureFocusTextElementVisible(EdgeIfNeeded); return true; } bool InputHandler::deleteText(int start, int end) { if (!isActiveTextEdit()) return false; ProcessingChangeGuard guard(this); if (!setSelection(start, end, true /*changeIsPartOfComposition*/)) return false; InputLog(LogLevelInfo, "InputHandler::deleteText start %d end %d", start, end); return deleteSelection(); } spannable_string_t* InputHandler::spannableTextInRange(int start, int end, int32_t flags) { if (!isActiveTextEdit()) return 0; if (start == end) return 0; ASSERT(end > start); int length = end - start; WTF::String textString = elementText().substring(start, length); spannable_string_t* pst = (spannable_string_t*)fastMalloc(sizeof(spannable_string_t)); // Don't use fastMalloc in case the string is unreasonably long. fastMalloc will // crash immediately on failure. pst->str = (wchar_t*)malloc(sizeof(wchar_t) * (length + 1)); if (!pst->str) { logAlways(LogLevelCritical, "InputHandler::spannableTextInRange Cannot allocate memory for string."); free(pst); return 0; } int stringLength = 0; if (!convertStringToWchar(textString, pst->str, length + 1, &stringLength)) { logAlways(LogLevelCritical, "InputHandler::spannableTextInRange failed to convert string."); free(pst->str); free(pst); return 0; } pst->length = stringLength; pst->spans_count = 0; pst->spans = 0; return pst; } spannable_string_t* InputHandler::selectedText(int32_t flags) { if (!isActiveTextEdit()) return 0; return spannableTextInRange(selectionStart(), selectionEnd(), flags); } spannable_string_t* InputHandler::textBeforeCursor(int32_t length, int32_t flags) { if (!isActiveTextEdit()) return 0; int caretOffset = caretPosition(); int start = relativeLeftOffset(caretOffset, length); int end = caretOffset; return spannableTextInRange(start, end, flags); } spannable_string_t* InputHandler::textAfterCursor(int32_t length, int32_t flags) { if (!isActiveTextEdit()) return 0; int caretOffset = caretPosition(); int start = caretOffset; int end = relativeRightOffset(caretOffset, elementText().length(), length); return spannableTextInRange(start, end, flags); } extracted_text_t* InputHandler::extractedTextRequest(extracted_text_request_t* request, int32_t flags) { if (!isActiveTextEdit()) return 0; extracted_text_t* extractedText = (extracted_text_t *)fastMalloc(sizeof(extracted_text_t)); // 'flags' indicates whether the text is being monitored. This is not currently used. // FIXME add smart limiting based on the hint sizes. Currently return all text. extractedText->text = spannableTextInRange(0, elementText().length(), flags); // FIXME confirm these should be 0 on this requested. Text changes will likely require // the end be the length. extractedText->partial_start_offset = 0; extractedText->partial_end_offset = 0; extractedText->start_offset = 0; // Adjust selection values relative to the start offset, which may be a subset // of the text in the field. extractedText->selection_start = selectionStart() - extractedText->start_offset; extractedText->selection_end = selectionEnd() - extractedText->start_offset; // selectionActive is not limited to inside the extracted text. bool selectionActive = extractedText->selection_start != extractedText->selection_end; bool singleLine = m_currentFocusElement->hasTagName(HTMLNames::inputTag); // FIXME flags has two values in doc, enum not in header yet. extractedText->flags = selectionActive & singleLine; return extractedText; } static void addCompositionTextStyleToAttributeTextStyle(AttributeTextStyle& style) { style.setUnderline(AttributeTextStyle::StandardUnderline); } static void addActiveTextStyleToAttributeTextStyle(AttributeTextStyle& style) { style.setBackgroundColor(Color("blue")); style.setTextColor(Color("white")); } static AttributeTextStyle compositionTextStyle() { AttributeTextStyle style; addCompositionTextStyleToAttributeTextStyle(style); return style; } static AttributeTextStyle textStyleFromMask(int64_t mask) { AttributeTextStyle style; if (mask & COMPOSED_TEXT_ATTRIB) addCompositionTextStyleToAttributeTextStyle(style); if (mask & ACTIVE_REGION_ATTRIB) addActiveTextStyleToAttributeTextStyle(style); return style; } bool InputHandler::removeComposedText() { if (compositionActive()) { if (!deleteText(m_composingTextStart, m_composingTextEnd)) { // Could not remove the existing composition region. return false; } removeAttributedTextMarker(); } return true; } int32_t InputHandler::setComposingRegion(int32_t start, int32_t end) { if (!isActiveTextEdit()) return -1; if (!removeComposedText()) { // Could not remove the existing composition region. return -1; } m_composingTextStart = start; m_composingTextEnd = end; if (compositionActive()) addAttributedTextMarker(start, end, compositionTextStyle()); InputLog(LogLevelInfo, "InputHandler::setComposingRegion start %d end %d", start, end); return 0; } int32_t InputHandler::finishComposition() { if (!isActiveTextEdit()) return -1; // FIXME if no composition is active, should we return failure -1? if (!compositionActive()) return 0; // Remove all markers. removeAttributedTextMarker(); InputLog(LogLevelInfo, "InputHandler::finishComposition completed"); return 0; } span_t* InputHandler::firstSpanInString(spannable_string_t* spannableString, SpannableStringAttribute attrib) { span_t* span = spannableString->spans; for (unsigned int i = 0; i < spannableString->spans_count; i++) { if (span->attributes_mask & attrib) return span; span++; } return 0; } bool InputHandler::isTrailingSingleCharacter(span_t* span, unsigned stringLength, unsigned composingTextLength) { // Make sure the new string is one character larger than the old. if (composingTextLength != stringLength - 1) return false; if (!span) return false; // Has only 1 character changed? if (span->start == span->end) { // Is this character the last character in the string? if (span->start == stringLength - 1) return true; } // Return after the first changed_attrib is found. return false; } bool InputHandler::setText(spannable_string_t* spannableString) { if (!isActiveTextEdit() || !spannableString) return false; ASSERT(m_currentFocusElement->document() && m_currentFocusElement->document()->frame()); Frame* frame = m_currentFocusElement->document()->frame(); Editor* editor = frame->editor(); ASSERT(editor); // Disable selectionHandler's active selection as we will be resetting and these // changes should not be handled as notification event. m_webPage->m_selectionHandler->setSelectionActive(false); String textToInsert = convertSpannableStringToString(spannableString); int textLength = textToInsert.length(); InputLog(LogLevelInfo, "InputHandler::setText spannableString is '%s', of length %d", textToInsert.latin1().data(), textLength); span_t* changedSpan = firstSpanInString(spannableString, CHANGED_ATTRIB); int composingTextStart = m_composingTextStart; int composingTextEnd = m_composingTextEnd; int composingTextLength = compositionLength(); removeAttributedTextMarker(); if (isTrailingSingleCharacter(changedSpan, textLength, composingTextLength)) { // If the text is unconverted, do not allow JS processing as it is not a "real" // character in the field. if (firstSpanInString(spannableString, UNCONVERTED_TEXT_ATTRIB)) { InputLog(LogLevelInfo, "InputHandler::setText Single trailing character detected. Text is unconverted."); return editor->command("InsertText").execute(textToInsert.right(1)); } InputLog(LogLevelInfo, "InputHandler::setText Single trailing character detected."); return handleKeyboardInput(Platform::KeyboardEvent(textToInsert[textLength - 1], Platform::KeyboardEvent::KeyChar, 0), false /* changeIsPartOfComposition */); } // If no spans have changed, treat it as a delete operation. if (!changedSpan) { // If the composition length is the same as our string length, then we don't need to do anything. if (composingTextLength == textLength) { InputLog(LogLevelInfo, "InputHandler::setText No spans have changed. New text is the same length as the old. Nothing to do."); return true; } if (composingTextLength - textLength == 1) { InputLog(LogLevelInfo, "InputHandler::setText No spans have changed. New text is one character shorter than the old. Treating as 'delete'."); return editor->command("DeleteBackward").execute(); } } if (composingTextLength && !setSelection(composingTextStart, composingTextEnd, true /* changeIsPartOfComposition */)) return false; // If there is no text to add just delete. if (!textLength) { if (selectionActive()) return editor->command("DeleteBackward").execute(); // Nothing to do. return true; } // Triggering an insert of the text with a space character trailing // causes new text to adopt the previous text style. // Remove it and apply it as a keypress later. // Upstream Webkit bug created https://bugs.webkit.org/show_bug.cgi?id=70823 bool requiresSpaceKeyPress = false; if (textLength > 0 && textToInsert[textLength - 1] == 32 /* space */) { requiresSpaceKeyPress = true; textLength--; textToInsert.remove(textLength, 1); } InputLog(LogLevelInfo, "InputHandler::setText Request being processed. Text before processing: '%s'", elementText().latin1().data()); if (textLength == 1 && !spannableString->spans_count) { // Handle single key non-attributed entry as key press rather than insert to allow // triggering of javascript events. InputLog(LogLevelInfo, "InputHandler::setText Single character entry treated as key-press in the absense of spans."); return handleKeyboardInput(Platform::KeyboardEvent(textToInsert[0], Platform::KeyboardEvent::KeyChar, 0), true /* changeIsPartOfComposition */); } // Perform the text change as a single command if there is one. if (!textToInsert.isEmpty() && !editor->command("InsertText").execute(textToInsert)) { InputLog(LogLevelWarn, "InputHandler::setText Failed to insert text '%s'", textToInsert.latin1().data()); return false; } if (requiresSpaceKeyPress) handleKeyboardInput(Platform::KeyboardEvent(32 /* space */, Platform::KeyboardEvent::KeyChar, 0), true /* changeIsPartOfComposition */); InputLog(LogLevelInfo, "InputHandler::setText Request being processed. Text after processing '%s'", elementText().latin1().data()); return true; } bool InputHandler::setTextAttributes(int insertionPoint, spannable_string_t* spannableString) { // Apply the attributes to the field. span_t* span = spannableString->spans; for (unsigned int i = 0; i < spannableString->spans_count; i++) { unsigned int startPosition = insertionPoint + span->start; // The end point includes the character that it is before. Ie, 0, 0 // applies to the first character as the end point includes the character // at the position. This means the endPosition is always +1. unsigned int endPosition = insertionPoint + span->end + 1; if (endPosition < startPosition || endPosition > elementText().length()) return false; if (!span->attributes_mask) continue; // Nothing to do. // MISSPELLED_WORD_ATTRIB is present as an option, but it is not currently // used by IMF. When they add support for on the fly spell checking we can // use it to apply spelling markers and disable continuous spell checking. InputLog(LogLevelInfo, "InputHandler::setTextAttributes adding marker %d to %d - %llu", startPosition, endPosition, span->attributes_mask); addAttributedTextMarker(startPosition, endPosition, textStyleFromMask(span->attributes_mask)); span++; } InputLog(LogLevelInfo, "InputHandler::setTextAttributes attribute count %d", spannableString->spans_count); return true; } bool InputHandler::setRelativeCursorPosition(int insertionPoint, int relativeCursorPosition) { if (!isActiveTextEdit()) return false; // 1 place cursor at end of insertion text. if (relativeCursorPosition == 1) { m_currentFocusElement->document()->frame()->selection()->revealSelection(ScrollAlignment::alignToEdgeIfNeeded); return true; } int cursorPosition = 0; if (relativeCursorPosition <= 0) { // Zero = insertionPoint // Negative value, move the cursor the requested number of characters before // the start of the inserted text. cursorPosition = insertionPoint + relativeCursorPosition; } else { // Positive value, move the cursor the requested number of characters after // the end of the inserted text minus 1. cursorPosition = caretPosition() + relativeCursorPosition - 1; } if (cursorPosition < 0 || cursorPosition > (int)elementText().length()) return false; InputLog(LogLevelInfo, "InputHandler::setRelativeCursorPosition cursor position %d", cursorPosition); return setCursorPosition(cursorPosition); } bool InputHandler::setSpannableTextAndRelativeCursor(spannable_string_t* spannableString, int relativeCursorPosition, bool markTextAsComposing) { InputLog(LogLevelInfo, "InputHandler::setSpannableTextAndRelativeCursor(%d, %d, %d)", spannableString->length, relativeCursorPosition, markTextAsComposing); int insertionPoint = compositionActive() ? m_composingTextStart : selectionStart(); ProcessingChangeGuard guard(this); if (!setText(spannableString)) return false; if (!setTextAttributes(insertionPoint, spannableString)) return false; if (!setRelativeCursorPosition(insertionPoint, relativeCursorPosition)) return false; if (markTextAsComposing) { m_composingTextStart = insertionPoint; m_composingTextEnd = insertionPoint + spannableString->length; } // Scroll the field if necessary. The automatic update is suppressed // by the processing change guard. ensureFocusTextElementVisible(EdgeIfNeeded); return true; } int32_t InputHandler::setComposingText(spannable_string_t* spannableString, int32_t relativeCursorPosition) { if (!isActiveTextEdit()) return -1; if (!spannableString) return -1; InputLog(LogLevelInfo, "InputHandler::setComposingText at relativeCursorPosition: %d", relativeCursorPosition); // Enable input mode if we are processing a key event. setInputModeEnabled(); return setSpannableTextAndRelativeCursor(spannableString, relativeCursorPosition, true /* markTextAsComposing */) ? 0 : -1; } int32_t InputHandler::commitText(spannable_string_t* spannableString, int32_t relativeCursorPosition) { if (!isActiveTextEdit()) return -1; if (!spannableString) return -1; InputLog(LogLevelInfo, "InputHandler::commitText"); return setSpannableTextAndRelativeCursor(spannableString, relativeCursorPosition, false /* markTextAsComposing */) ? 0 : -1; } } }