| /* |
| * 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 <BlackBerryPlatformKeyboardEvent.h> |
| #include <BlackBerryPlatformLog.h> |
| #include <BlackBerryPlatformMisc.h> |
| #include <BlackBerryPlatformScreen.h> |
| #include <BlackBerryPlatformSettings.h> |
| #include <sys/keycodes.h> |
| #include <wtf/text/CString.h> |
| |
| #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: |
| { |
| // Regular input mode, disable help if autocomplete is off. |
| int imfMask = 0; |
| DOMSupport::AttributeState autoCompleteState = DOMSupport::elementSupportsAutocomplete(element); |
| if (autoCompleteState == DOMSupport::Off) |
| imfMask = NO_AUTO_TEXT | NO_PREDICTION; |
| else if (autoCompleteState != DOMSupport::On |
| && DOMSupport::elementIdOrNameIndicatesNoAutocomplete(element)) |
| imfMask = NO_AUTO_TEXT | NO_PREDICTION; |
| |
| // Disable autocorrection if it's specifically off, of if it is in default mode |
| // and we have disabled auto text and prediction. |
| if (DOMSupport::elementSupportsAutocorrect(element) == DOMSupport::Off |
| || (imfMask && DOMSupport::elementSupportsAutocorrect(element) == DOMSupport::Default)) |
| imfMask |= NO_AUTO_CORRECTION; |
| |
| if (imfMask) |
| return imfMask; |
| if ((type == InputTypeEmail || type == InputTypeURL) && autoCompleteState != DOMSupport::On) |
| return NO_AUTO_TEXT | NO_PREDICTION | NO_AUTO_CORRECTION; |
| break; |
| } |
| 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<const HTMLFormControlElement*>(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<const HTMLFormControlElement*>(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 |
| { |
| // <isIndex> 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<const HTMLInputElement*>(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<Element*>(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<UChar32*>(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<wchar_t>& 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<UChar> 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<UChar32*>(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<wchar_t> 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<int>(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<WebCore::TextCheckingRequest> 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<TextCheckingResult> 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; |
| |
| SpellingLog(LogLevelInfo, "InputHandler::shouldRequestSpellCheckingOptionsForPoint Found spelling marker at point %d, %d", point.x(), point.y()); |
| |
| // imf_sp_text_t should be generated in pixel viewport coordinates. |
| WebCore::IntRect rect = m_webPage->mapToTransformed(m_webPage->focusedOrMainFrame()->view()->contentsToWindow(enclosingIntRect(marker->renderedRect()))); |
| m_webPage->clipToTransformedContentsRect(rect); |
| |
| // TODO use the actual caret position after it is placed. |
| spellCheckingOptionRequest.caret_rect.caret_top_x = point.x(); |
| spellCheckingOptionRequest.caret_rect.caret_top_y = rect.y(); |
| spellCheckingOptionRequest.caret_rect.caret_bottom_x = point.x(); |
| spellCheckingOptionRequest.caret_rect.caret_bottom_y = rect.y() + rect.height(); |
| spellCheckingOptionRequest.startTextPosition = marker->startOffset(); |
| spellCheckingOptionRequest.endTextPosition = marker->endOffset(); |
| |
| SpellingLog(LogLevelInfo, "InputHandler::shouldRequestSpellCheckingOptionsForPoint spellCheckingOptionRequest\ntop %d, %d\nbottom %d %d\nMarker start %d end %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); |
| |
| return true; |
| } |
| |
| void InputHandler::requestSpellingCheckingOptions(imf_sp_text_t& spellCheckingOptionRequest) |
| { |
| 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); |
| |
| if (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); |
| |
| 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<const HTMLFormControlElement*>(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); |
| |
| #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<Range> 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<Range> 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<HTMLInputElement*>(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<RenderWidget*>(m_currentFocusElement->renderer()); |
| if (renderWidget) { |
| PluginView* pluginView = static_cast<PluginView*>(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) |
| { |
| // If we aren't ready for input, keyboard changes should be ignored. |
| if (!isInputModeEnabled() && 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<int>(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<Range> 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<Range> 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<int>(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<int>(elementText().length())) |
| return; |
| |
| RefPtr<Range> 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<Element*>(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<HTMLSelectElement*>(node)); |
| |
| if (node->hasTagName(HTMLNames::optionTag)) { |
| HTMLOptionElement* optionElement = static_cast<HTMLOptionElement*>(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<HTMLElement*>& listItems = select->listItems(); |
| int size = listItems.size(); |
| |
| bool multiple = select->multiple(); |
| ScopeArray<WebString> 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<HTMLOptionElement*>(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<HTMLOptGroupElement*>(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<HTMLSelectElement*>(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<HTMLSelectElement*>(m_currentFocusElement.get()); |
| const WTF::Vector<HTMLElement*>& items = selectElement->listItems(); |
| if (items.size() != static_cast<unsigned int>(size)) |
| return; |
| |
| HTMLOptionElement* option; |
| for (int i = 0; i < size; i++) { |
| if (items[i]->hasTagName(HTMLNames::optionTag)) { |
| option = static_cast<HTMLOptionElement*>(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* backingStoreClientForFrame = m_webPage->backingStoreClientForFrame(m_webPage->mainFrame()); |
| ASSERT(backingStoreClientForFrame); |
| |
| // Enable / Disable the backingstore to prevent visual updates. |
| if (!active) |
| backingStoreClientForFrame->backingStore()->resumeScreenAndBackingStoreUpdates(BackingStore::RenderAndBlit); |
| else |
| backingStoreClientForFrame->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<int>(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; |
| } |
| |
| } |
| } |