blob: ee58c4708f2d20c74f7f6f39cfcfe99952bd3145 [file] [edit]
/*
* Copyright (C) 2006, 2007, 2008, 2011 Apple Inc. All rights reserved.
* Copyright (C) 2008 Nokia Corporation and/or its subsidiary(-ies)
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "core/editing/InputMethodController.h"
#include "core/dom/Document.h"
#include "core/dom/Element.h"
#include "core/dom/Text.h"
#include "core/editing/EditingUtilities.h"
#include "core/editing/Editor.h"
#include "core/editing/commands/TypingCommand.h"
#include "core/editing/markers/DocumentMarkerController.h"
#include "core/events/CompositionEvent.h"
#include "core/frame/LocalFrame.h"
#include "core/html/HTMLTextAreaElement.h"
#include "core/input/EventHandler.h"
#include "core/layout/LayoutObject.h"
#include "core/layout/LayoutTheme.h"
#include "core/page/ChromeClient.h"
namespace blink {
InputMethodController::SelectionOffsetsScope::SelectionOffsetsScope(InputMethodController* inputMethodController)
: m_inputMethodController(inputMethodController)
, m_offsets(inputMethodController->getSelectionOffsets())
{
}
InputMethodController::SelectionOffsetsScope::~SelectionOffsetsScope()
{
m_inputMethodController->setSelectionOffsets(m_offsets);
}
// ----------------------------
PassOwnPtrWillBeRawPtr<InputMethodController> InputMethodController::create(LocalFrame& frame)
{
return adoptPtrWillBeNoop(new InputMethodController(frame));
}
InputMethodController::InputMethodController(LocalFrame& frame)
: m_frame(&frame)
, m_isDirty(false)
, m_hasComposition(false)
{
}
InputMethodController::~InputMethodController()
{
}
bool InputMethodController::hasComposition() const
{
return m_hasComposition;
}
inline Editor& InputMethodController::editor() const
{
return frame().editor();
}
void InputMethodController::clear()
{
m_hasComposition = false;
if (m_compositionRange) {
m_compositionRange->setStart(frame().document(), 0);
m_compositionRange->collapse(true);
}
frame().document()->markers().removeMarkers(DocumentMarker::Composition);
m_isDirty = false;
}
void InputMethodController::documentDetached()
{
clear();
m_compositionRange = nullptr;
}
bool InputMethodController::insertTextForConfirmedComposition(const String& text)
{
return frame().eventHandler().handleTextInputEvent(text, 0, TextEventInputComposition);
}
void InputMethodController::selectComposition() const
{
const EphemeralRange range = compositionEphemeralRange();
if (range.isNull())
return;
// The composition can start inside a composed character sequence, so we have to override checks.
// See <http://bugs.webkit.org/show_bug.cgi?id=15781>
VisibleSelection selection;
selection.setWithoutValidation(range.startPosition(), range.endPosition());
frame().selection().setSelection(selection, 0);
}
bool InputMethodController::confirmComposition()
{
if (!hasComposition())
return false;
return confirmComposition(plainText(compositionEphemeralRange()));
}
bool InputMethodController::confirmComposition(const String& text)
{
return finishComposition(text, ConfirmComposition);
}
bool InputMethodController::confirmCompositionOrInsertText(const String& text, ConfirmCompositionBehavior confirmBehavior)
{
if (!hasComposition()) {
if (!text.length())
return false;
editor().insertText(text, 0);
return true;
}
if (text.length()) {
confirmComposition(text);
return true;
}
if (confirmBehavior != KeepSelection)
return confirmComposition();
SelectionOffsetsScope selectionOffsetsScope(this);
return confirmComposition();
}
void InputMethodController::cancelComposition()
{
finishComposition(emptyString(), CancelComposition);
}
void InputMethodController::cancelCompositionIfSelectionIsInvalid()
{
if (!hasComposition() || editor().preventRevealSelection())
return;
// Check if selection start and selection end are valid.
FrameSelection& selection = frame().selection();
if (!selection.isNone() && !m_compositionRange->collapsed()) {
if (selection.start().compareTo(m_compositionRange->startPosition()) >= 0
&& selection.end().compareTo(m_compositionRange->endPosition()) <= 0)
return;
}
cancelComposition();
frame().chromeClient().didCancelCompositionOnSelectionChange();
}
bool InputMethodController::finishComposition(const String& text, FinishCompositionMode mode)
{
if (!hasComposition())
return false;
ASSERT(mode == ConfirmComposition || mode == CancelComposition);
Editor::RevealSelectionScope revealSelectionScope(&editor());
if (mode == CancelComposition)
ASSERT(text == emptyString());
else
selectComposition();
if (frame().selection().isNone())
return false;
// Dispatch a compositionend event to the focused node.
// We should send this event before sending a TextEvent as written in Section 6.2.2 and 6.2.3 of
// the DOM Event specification.
if (Element* target = frame().document()->focusedElement()) {
RefPtrWillBeRawPtr<CompositionEvent> event = CompositionEvent::create(EventTypeNames::compositionend, frame().domWindow(), text);
target->dispatchEvent(event);
}
bool dirty = m_isDirty || plainText(compositionEphemeralRange()) != text;
// If text is empty, then delete the old composition here. If text is non-empty, InsertTextCommand::input
// will delete the old composition with an optimized replace operation.
if (text.isEmpty() && mode != CancelComposition && dirty) {
ASSERT(frame().document());
TypingCommand::deleteSelection(*frame().document(), 0);
}
clear();
if (dirty)
insertTextForConfirmedComposition(text);
if (mode == CancelComposition) {
// An open typing command that disagrees about current selection would cause issues with typing later on.
TypingCommand::closeTyping(m_frame);
}
return true;
}
void InputMethodController::setComposition(const String& text, const Vector<CompositionUnderline>& underlines, unsigned selectionStart, unsigned selectionEnd)
{
Editor::RevealSelectionScope revealSelectionScope(&editor());
// Updates styles before setting selection for composition to prevent
// inserting the previous composition text into text nodes oddly.
// See https://bugs.webkit.org/show_bug.cgi?id=46868
frame().document()->updateLayoutTreeIfNeeded();
selectComposition();
if (frame().selection().isNone())
return;
if (Element* target = frame().document()->focusedElement()) {
// Dispatch an appropriate composition event to the focused node.
// We check the composition status and choose an appropriate composition event since this
// function is used for three purposes:
// 1. Starting a new composition.
// Send a compositionstart and a compositionupdate event when this function creates
// a new composition node, i.e.
// !hasComposition() && !text.isEmpty().
// Sending a compositionupdate event at this time ensures that at least one
// compositionupdate event is dispatched.
// 2. Updating the existing composition node.
// Send a compositionupdate event when this function updates the existing composition
// node, i.e. hasComposition() && !text.isEmpty().
// 3. Canceling the ongoing composition.
// Send a compositionend event when function deletes the existing composition node, i.e.
// !hasComposition() && test.isEmpty().
RefPtrWillBeRawPtr<CompositionEvent> event = nullptr;
if (!hasComposition()) {
// We should send a compositionstart event only when the given text is not empty because this
// function doesn't create a composition node when the text is empty.
if (!text.isEmpty()) {
target->dispatchEvent(CompositionEvent::create(EventTypeNames::compositionstart, frame().domWindow(), frame().selectedText()));
event = CompositionEvent::create(EventTypeNames::compositionupdate, frame().domWindow(), text);
}
} else {
if (!text.isEmpty())
event = CompositionEvent::create(EventTypeNames::compositionupdate, frame().domWindow(), text);
else
event = CompositionEvent::create(EventTypeNames::compositionend, frame().domWindow(), text);
}
if (event.get())
target->dispatchEvent(event);
}
// If text is empty, then delete the old composition here. If text is non-empty, InsertTextCommand::input
// will delete the old composition with an optimized replace operation.
if (text.isEmpty()) {
ASSERT(frame().document());
TypingCommand::deleteSelection(*frame().document(), TypingCommand::PreventSpellChecking);
}
clear();
if (text.isEmpty())
return;
ASSERT(frame().document());
TypingCommand::insertText(*frame().document(), text, TypingCommand::SelectInsertedText | TypingCommand::PreventSpellChecking, TypingCommand::TextCompositionUpdate);
// Find out what node has the composition now.
Position base = mostForwardCaretPosition(frame().selection().base());
Node* baseNode = base.anchorNode();
if (!baseNode || !baseNode->isTextNode())
return;
Position extent = frame().selection().extent();
Node* extentNode = extent.anchorNode();
if (baseNode != extentNode)
return;
unsigned extentOffset = extent.computeOffsetInContainerNode();
unsigned baseOffset = base.computeOffsetInContainerNode();
if (baseOffset + text.length() != extentOffset)
return;
m_isDirty = true;
m_hasComposition = true;
if (!m_compositionRange)
m_compositionRange = Range::create(baseNode->document());
m_compositionRange->setStart(baseNode, baseOffset);
m_compositionRange->setEnd(baseNode, extentOffset);
if (baseNode->layoutObject())
baseNode->layoutObject()->setShouldDoFullPaintInvalidation();
unsigned start = std::min(baseOffset + selectionStart, extentOffset);
unsigned end = std::min(std::max(start, baseOffset + selectionEnd), extentOffset);
RefPtrWillBeRawPtr<Range> selectedRange = Range::create(baseNode->document(), baseNode, start, baseNode, end);
frame().selection().setSelectedRange(selectedRange.get(), TextAffinity::Downstream, SelectionDirectionalMode::NonDirectional, NotUserTriggered);
if (underlines.isEmpty()) {
frame().document()->markers().addCompositionMarker(m_compositionRange->startPosition(), m_compositionRange->endPosition(), Color::black, false, LayoutTheme::theme().platformDefaultCompositionBackgroundColor());
return;
}
for (const auto& underline : underlines) {
unsigned underlineStart = baseOffset + underline.startOffset;
unsigned underlineEnd = baseOffset + underline.endOffset;
EphemeralRange ephemeralLineRange = EphemeralRange(Position(baseNode, underlineStart), Position(baseNode, underlineEnd));
if (ephemeralLineRange.isNull())
continue;
frame().document()->markers().addCompositionMarker(ephemeralLineRange.startPosition(), ephemeralLineRange.endPosition(), underline.color, underline.thick, underline.backgroundColor);
}
}
void InputMethodController::setCompositionFromExistingText(const Vector<CompositionUnderline>& underlines, unsigned compositionStart, unsigned compositionEnd)
{
Element* editable = frame().selection().rootEditableElement();
if (!editable)
return;
const EphemeralRange range = PlainTextRange(compositionStart, compositionEnd).createRange(*editable);
if (range.isNull())
return;
const Position start = range.startPosition();
if (editableRootForPosition(start) != editable)
return;
const Position end = range.endPosition();
if (editableRootForPosition(end) != editable)
return;
clear();
for (const auto& underline : underlines) {
unsigned underlineStart = compositionStart + underline.startOffset;
unsigned underlineEnd = compositionStart + underline.endOffset;
EphemeralRange ephemeralLineRange = PlainTextRange(underlineStart, underlineEnd).createRange(*editable);
if (ephemeralLineRange.isNull())
continue;
frame().document()->markers().addCompositionMarker(ephemeralLineRange.startPosition(), ephemeralLineRange.endPosition(), underline.color, underline.thick, underline.backgroundColor);
}
m_hasComposition = true;
if (!m_compositionRange)
m_compositionRange = Range::create(range.document());
m_compositionRange->setStart(range.startPosition());
m_compositionRange->setEnd(range.endPosition());
}
EphemeralRange InputMethodController::compositionEphemeralRange() const
{
if (!hasComposition())
return EphemeralRange();
return EphemeralRange(m_compositionRange.get());
}
PassRefPtrWillBeRawPtr<Range> InputMethodController::compositionRange() const
{
return hasComposition() ? m_compositionRange : nullptr;
}
PlainTextRange InputMethodController::getSelectionOffsets() const
{
EphemeralRange range = firstEphemeralRangeOf(frame().selection().selection());
if (range.isNull())
return PlainTextRange();
ContainerNode* editable = frame().selection().rootEditableElementOrTreeScopeRootNode();
ASSERT(editable);
return PlainTextRange::create(*editable, range);
}
bool InputMethodController::setSelectionOffsets(const PlainTextRange& selectionOffsets)
{
if (selectionOffsets.isNull())
return false;
Element* rootEditableElement = frame().selection().rootEditableElement();
if (!rootEditableElement)
return false;
const EphemeralRange range = selectionOffsets.createRange(*rootEditableElement);
if (range.isNull())
return false;
return frame().selection().setSelectedRange(range, VP_DEFAULT_AFFINITY, SelectionDirectionalMode::NonDirectional, FrameSelection::CloseTyping);
}
bool InputMethodController::setEditableSelectionOffsets(const PlainTextRange& selectionOffsets)
{
if (!editor().canEdit())
return false;
return setSelectionOffsets(selectionOffsets);
}
void InputMethodController::extendSelectionAndDelete(int before, int after)
{
if (!editor().canEdit())
return;
PlainTextRange selectionOffsets(getSelectionOffsets());
if (selectionOffsets.isNull())
return;
// A common call of before=1 and after=0 will fail if the last character
// is multi-code-word UTF-16, including both multi-16bit code-points and
// Unicode combining character sequences of multiple single-16bit code-
// points (officially called "compositions"). Try more until success.
// http://crbug.com/355995
//
// FIXME: Note that this is not an ideal solution when this function is
// called to implement "backspace". In that case, there should be some call
// that will not delete a full multi-code-point composition but rather
// only the last code-point so that it's possible for a user to correct
// a composition without starting it from the beginning.
// http://crbug.com/37993
do {
if (!setSelectionOffsets(PlainTextRange(std::max(static_cast<int>(selectionOffsets.start()) - before, 0), selectionOffsets.end() + after)))
return;
if (before == 0)
break;
++before;
} while (frame().selection().start() == frame().selection().end() && before <= static_cast<int>(selectionOffsets.start()));
TypingCommand::deleteSelection(*frame().document());
}
DEFINE_TRACE(InputMethodController)
{
visitor->trace(m_frame);
visitor->trace(m_compositionRange);
}
} // namespace blink