blob: f480c0788f8a32951539a21aa25eb7f01607257d [file] [log] [blame]
/*
* Copyright (C) 2005, 2006, 2007 Apple Inc. All rights reserved.
*
* 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 "TypingCommand.h"
#include "BeforeTextInsertedEvent.h"
#include "BreakBlockquoteCommand.h"
#include "DeleteSelectionCommand.h"
#include "Document.h"
#include "Editor.h"
#include "Element.h"
#include "Frame.h"
#include "InsertLineBreakCommand.h"
#include "InsertParagraphSeparatorCommand.h"
#include "InsertTextCommand.h"
#include "SelectionController.h"
#include "VisiblePosition.h"
#include "htmlediting.h"
#include "visible_units.h"
namespace WebCore {
TypingCommand::TypingCommand(Document *document, ETypingCommand commandType, const String &textToInsert, bool selectInsertedText, TextGranularity granularity)
: CompositeEditCommand(document),
m_commandType(commandType),
m_textToInsert(textToInsert),
m_openForMoreTyping(true),
m_applyEditing(false),
m_selectInsertedText(selectInsertedText),
m_smartDelete(false),
m_granularity(granularity),
m_openedByBackwardDelete(false)
{
}
void TypingCommand::deleteSelection(Document* document, bool smartDelete)
{
ASSERT(document);
Frame* frame = document->frame();
ASSERT(frame);
if (!frame->selectionController()->isRange())
return;
EditCommand* lastEditCommand = frame->editor()->lastEditCommand();
if (isOpenForMoreTypingCommand(lastEditCommand)) {
static_cast<TypingCommand*>(lastEditCommand)->deleteSelection(smartDelete);
return;
}
RefPtr<TypingCommand> typingCommand = new TypingCommand(document, DeleteSelection, "", false);
typingCommand->setSmartDelete(smartDelete);
typingCommand->apply();
}
void TypingCommand::deleteKeyPressed(Document *document, bool smartDelete, TextGranularity granularity)
{
ASSERT(document);
Frame *frame = document->frame();
ASSERT(frame);
EditCommand* lastEditCommand = frame->editor()->lastEditCommand();
if (isOpenForMoreTypingCommand(lastEditCommand)) {
static_cast<TypingCommand*>(lastEditCommand)->deleteKeyPressed(granularity);
return;
}
RefPtr<TypingCommand> typingCommand = new TypingCommand(document, DeleteKey, "", false, granularity);
typingCommand->setSmartDelete(smartDelete);
typingCommand->apply();
}
void TypingCommand::forwardDeleteKeyPressed(Document *document, bool smartDelete, TextGranularity granularity)
{
// FIXME: Forward delete in TextEdit appears to open and close a new typing command.
ASSERT(document);
Frame *frame = document->frame();
ASSERT(frame);
EditCommand* lastEditCommand = frame->editor()->lastEditCommand();
if (isOpenForMoreTypingCommand(lastEditCommand)) {
static_cast<TypingCommand*>(lastEditCommand)->forwardDeleteKeyPressed(granularity);
return;
}
RefPtr<TypingCommand> typingCommand = new TypingCommand(document, ForwardDeleteKey, "", false, granularity);
typingCommand->setSmartDelete(smartDelete);
typingCommand->apply();
}
void TypingCommand::insertText(Document* document, const String& text, bool selectInsertedText, bool insertedTextIsComposition)
{
ASSERT(document);
Frame* frame = document->frame();
ASSERT(frame);
insertText(document, text, frame->selectionController()->selection(), selectInsertedText, insertedTextIsComposition);
}
void TypingCommand::insertText(Document* document, const String& text, const Selection& selectionForInsertion, bool selectInsertedText, bool insertedTextIsComposition)
{
ASSERT(document);
RefPtr<Frame> frame = document->frame();
ASSERT(frame);
Selection currentSelection = frame->selectionController()->selection();
bool changeSelection = currentSelection != selectionForInsertion;
String newText = text;
Node* startNode = selectionForInsertion.start().node();
if (startNode && startNode->rootEditableElement() && !insertedTextIsComposition) {
// Send BeforeTextInsertedEvent. The event handler will update text if necessary.
ExceptionCode ec = 0;
RefPtr<BeforeTextInsertedEvent> evt = new BeforeTextInsertedEvent(text);
startNode->rootEditableElement()->dispatchEvent(evt, ec, true);
newText = evt->text();
}
if (newText.isEmpty())
return;
// Set the starting and ending selection appropriately if we are using a selection
// that is different from the current selection. In the future, we should change EditCommand
// to deal with custom selections in a general way that can be used by all of the commands.
RefPtr<EditCommand> lastEditCommand = frame->editor()->lastEditCommand();
if (isOpenForMoreTypingCommand(lastEditCommand.get())) {
TypingCommand* lastTypingCommand = static_cast<TypingCommand*>(lastEditCommand.get());
if (changeSelection) {
lastTypingCommand->setStartingSelection(selectionForInsertion);
lastTypingCommand->setEndingSelection(selectionForInsertion);
}
lastTypingCommand->insertText(newText, selectInsertedText);
if (changeSelection) {
lastTypingCommand->setEndingSelection(currentSelection);
frame->selectionController()->setSelection(currentSelection);
}
return;
}
RefPtr<TypingCommand> cmd = new TypingCommand(document, InsertText, newText, selectInsertedText);
if (changeSelection) {
cmd->setStartingSelection(selectionForInsertion);
cmd->setEndingSelection(selectionForInsertion);
}
applyCommand(cmd);
if (changeSelection) {
cmd->setEndingSelection(currentSelection);
frame->selectionController()->setSelection(currentSelection);
}
}
void TypingCommand::insertLineBreak(Document *document)
{
ASSERT(document);
Frame *frame = document->frame();
ASSERT(frame);
EditCommand* lastEditCommand = frame->editor()->lastEditCommand();
if (isOpenForMoreTypingCommand(lastEditCommand)) {
static_cast<TypingCommand*>(lastEditCommand)->insertLineBreak();
return;
}
applyCommand(new TypingCommand(document, InsertLineBreak));
}
void TypingCommand::insertParagraphSeparatorInQuotedContent(Document *document)
{
ASSERT(document);
Frame *frame = document->frame();
ASSERT(frame);
EditCommand* lastEditCommand = frame->editor()->lastEditCommand();
if (isOpenForMoreTypingCommand(lastEditCommand)) {
static_cast<TypingCommand*>(lastEditCommand)->insertParagraphSeparatorInQuotedContent();
return;
}
applyCommand(new TypingCommand(document, InsertParagraphSeparatorInQuotedContent));
}
void TypingCommand::insertParagraphSeparator(Document *document)
{
ASSERT(document);
Frame *frame = document->frame();
ASSERT(frame);
EditCommand* lastEditCommand = frame->editor()->lastEditCommand();
if (isOpenForMoreTypingCommand(lastEditCommand)) {
static_cast<TypingCommand*>(lastEditCommand)->insertParagraphSeparator();
return;
}
applyCommand(new TypingCommand(document, InsertParagraphSeparator));
}
bool TypingCommand::isOpenForMoreTypingCommand(const EditCommand* cmd)
{
return cmd && cmd->isTypingCommand() && static_cast<const TypingCommand*>(cmd)->isOpenForMoreTyping();
}
void TypingCommand::closeTyping(EditCommand* cmd)
{
if (isOpenForMoreTypingCommand(cmd))
static_cast<TypingCommand*>(cmd)->closeTyping();
}
void TypingCommand::doApply()
{
if (endingSelection().isNone())
return;
if (m_commandType == DeleteKey)
if (m_commands.isEmpty())
m_openedByBackwardDelete = true;
switch (m_commandType) {
case DeleteSelection:
deleteSelection(m_smartDelete);
return;
case DeleteKey:
deleteKeyPressed(m_granularity);
return;
case ForwardDeleteKey:
forwardDeleteKeyPressed(m_granularity);
return;
case InsertLineBreak:
insertLineBreak();
return;
case InsertParagraphSeparator:
insertParagraphSeparator();
return;
case InsertParagraphSeparatorInQuotedContent:
insertParagraphSeparatorInQuotedContent();
return;
case InsertText:
insertText(m_textToInsert, m_selectInsertedText);
return;
}
ASSERT_NOT_REACHED();
}
EditAction TypingCommand::editingAction() const
{
return EditActionTyping;
}
void TypingCommand::markMisspellingsAfterTyping()
{
// Take a look at the selection that results after typing and determine whether we need to spellcheck.
// Since the word containing the current selection is never marked, this does a check to
// see if typing made a new word that is not in the current selection. Basically, you
// get this by being at the end of a word and typing a space.
VisiblePosition start(endingSelection().start(), endingSelection().affinity());
VisiblePosition previous = start.previous();
if (previous.isNotNull()) {
VisiblePosition p1 = startOfWord(previous, LeftWordIfOnBoundary);
VisiblePosition p2 = startOfWord(start, LeftWordIfOnBoundary);
if (p1 != p2)
document()->frame()->editor()->markMisspellingsAfterTypingToPosition(p1);
}
}
void TypingCommand::typingAddedToOpenCommand()
{
markMisspellingsAfterTyping();
// Do not apply editing to the frame on the first time through.
// The frame will get told in the same way as all other commands.
// But since this command stays open and is used for additional typing,
// we need to tell the frame here as other commands are added.
if (m_applyEditing)
document()->frame()->editor()->appliedEditing(this);
m_applyEditing = true;
}
void TypingCommand::insertText(const String &text, bool selectInsertedText)
{
// FIXME: Need to implement selectInsertedText for cases where more than one insert is involved.
// This requires support from insertTextRunWithoutNewlines and insertParagraphSeparator for extending
// an existing selection; at the moment they can either put the caret after what's inserted or
// select what's inserted, but there's no way to "extend selection" to include both an old selection
// that ends just before where we want to insert text and the newly inserted text.
int offset = 0;
int newline;
while ((newline = text.find('\n', offset)) != -1) {
if (newline != offset)
insertTextRunWithoutNewlines(text.substring(offset, newline - offset), false);
insertParagraphSeparator();
offset = newline + 1;
}
if (offset == 0)
insertTextRunWithoutNewlines(text, selectInsertedText);
else {
int length = text.length();
if (length != offset) {
insertTextRunWithoutNewlines(text.substring(offset, length - offset), selectInsertedText);
}
}
}
void TypingCommand::insertTextRunWithoutNewlines(const String &text, bool selectInsertedText)
{
RefPtr<InsertTextCommand> command;
if (!document()->frame()->typingStyle() && !m_commands.isEmpty()) {
EditCommand* lastCommand = m_commands.last().get();
if (lastCommand->isInsertTextCommand())
command = static_cast<InsertTextCommand*>(lastCommand);
}
if (!command) {
command = new InsertTextCommand(document());
applyCommandToComposite(command);
}
command->input(text, selectInsertedText);
typingAddedToOpenCommand();
}
void TypingCommand::insertLineBreak()
{
applyCommandToComposite(new InsertLineBreakCommand(document()));
typingAddedToOpenCommand();
}
void TypingCommand::insertParagraphSeparator()
{
applyCommandToComposite(new InsertParagraphSeparatorCommand(document()));
typingAddedToOpenCommand();
}
void TypingCommand::insertParagraphSeparatorInQuotedContent()
{
applyCommandToComposite(new BreakBlockquoteCommand(document()));
typingAddedToOpenCommand();
}
void TypingCommand::deleteKeyPressed(TextGranularity granularity)
{
Selection selectionToDelete;
Selection selectionAfterUndo;
switch (endingSelection().state()) {
case Selection::RANGE:
selectionToDelete = endingSelection();
selectionAfterUndo = selectionToDelete;
break;
case Selection::CARET: {
m_smartDelete = false;
SelectionController selectionController;
selectionController.setSelection(endingSelection());
selectionController.modify(SelectionController::EXTEND, SelectionController::BACKWARD, granularity);
// When the caret is at the start of the editable area in an empty list item, break out of the list item.
if (endingSelection().visibleStart().previous(true).isNull()) {
if (breakOutOfEmptyListItem()) {
typingAddedToOpenCommand();
return;
}
}
VisiblePosition visibleStart(endingSelection().visibleStart());
// If the caret is at the start of a paragraph after a table, move content into the last table cell.
if (isStartOfParagraph(visibleStart) && isFirstPositionAfterTable(visibleStart.previous(true))) {
// Unless the caret is just before a table. We don't want to move a table into the last table cell.
if (isLastPositionBeforeTable(visibleStart))
return;
// Extend the selection backward into the last cell, then deletion will handle the move.
selectionController.modify(SelectionController::EXTEND, SelectionController::BACKWARD, granularity);
// If the caret is just after a table, select the table and don't delete anything.
} else if (Node* table = isFirstPositionAfterTable(visibleStart)) {
setEndingSelection(Selection(Position(table, 0), endingSelection().start(), DOWNSTREAM));
typingAddedToOpenCommand();
return;
}
selectionToDelete = selectionController.selection();
if (!startingSelection().isRange() || selectionToDelete.base() != startingSelection().start())
selectionAfterUndo = selectionToDelete;
else
// It's a little tricky to compute what the starting selection would have been in the original document.
// We can't let the Selection class's validation kick in or it'll adjust for us based on
// the current state of the document and we'll get the wrong result.
selectionAfterUndo.setWithoutValidation(startingSelection().end(), selectionToDelete.extent());
break;
}
case Selection::NONE:
ASSERT_NOT_REACHED();
break;
}
if (selectionToDelete.isCaretOrRange() && document()->frame()->shouldDeleteSelection(selectionToDelete)) {
// Make undo select everything that has been deleted, unless an undo will undo more than just this deletion.
// FIXME: This behaves like TextEdit except for the case where you open with text insertion and then delete
// more text than you insert. In that case all of the text that was around originally should be selected.
if (m_openedByBackwardDelete)
setStartingSelection(selectionAfterUndo);
CompositeEditCommand::deleteSelection(selectionToDelete, m_smartDelete);
setSmartDelete(false);
typingAddedToOpenCommand();
}
}
void TypingCommand::forwardDeleteKeyPressed(TextGranularity granularity)
{
Selection selectionToDelete;
Selection selectionAfterUndo;
switch (endingSelection().state()) {
case Selection::RANGE:
selectionToDelete = endingSelection();
selectionAfterUndo = selectionToDelete;
break;
case Selection::CARET: {
m_smartDelete = false;
// Handle delete at beginning-of-block case.
// Do nothing in the case that the caret is at the start of a
// root editable element or at the start of a document.
SelectionController selectionController;
selectionController.setSelection(endingSelection());
selectionController.modify(SelectionController::EXTEND, SelectionController::FORWARD, granularity);
Position downstreamEnd = endingSelection().end().downstream();
VisiblePosition visibleEnd = endingSelection().visibleEnd();
if (visibleEnd == endOfParagraph(visibleEnd))
downstreamEnd = visibleEnd.next(true).deepEquivalent().downstream();
// When deleting tables: Select the table first, then perform the deletion
if (downstreamEnd.node() && downstreamEnd.node()->renderer() && downstreamEnd.node()->renderer()->isTable() && downstreamEnd.offset() == 0) {
setEndingSelection(Selection(endingSelection().end(), Position(downstreamEnd.node(), maxDeepOffset(downstreamEnd.node())), DOWNSTREAM));
typingAddedToOpenCommand();
return;
}
// deleting to end of paragraph when at end of paragraph needs to merge the next paragraph (if any)
if (granularity == ParagraphBoundary && selectionController.selection().isCaret() && isEndOfParagraph(selectionController.selection().visibleEnd()))
selectionController.modify(SelectionController::EXTEND, SelectionController::FORWARD, CharacterGranularity);
selectionToDelete = selectionController.selection();
if (!startingSelection().isRange() || selectionToDelete.base() != startingSelection().start())
selectionAfterUndo = selectionToDelete;
else {
// It's a little tricky to compute what the starting selection would have been in the original document.
// We can't let the Selection class's validation kick in or it'll adjust for us based on
// the current state of the document and we'll get the wrong result.
Position extent = startingSelection().end();
if (extent.node() != selectionToDelete.end().node())
extent = selectionToDelete.extent();
else {
int extraCharacters;
if (selectionToDelete.start().node() == selectionToDelete.end().node())
extraCharacters = selectionToDelete.end().offset() - selectionToDelete.start().offset();
else
extraCharacters = selectionToDelete.end().offset();
extent = Position(extent.node(), extent.offset() + extraCharacters);
}
selectionAfterUndo.setWithoutValidation(startingSelection().start(), extent);
}
break;
}
case Selection::NONE:
ASSERT_NOT_REACHED();
break;
}
if (selectionToDelete.isCaretOrRange() && document()->frame()->shouldDeleteSelection(selectionToDelete)) {
// make undo select what was deleted
setStartingSelection(selectionAfterUndo);
CompositeEditCommand::deleteSelection(selectionToDelete, m_smartDelete);
setSmartDelete(false);
typingAddedToOpenCommand();
}
}
void TypingCommand::deleteSelection(bool smartDelete)
{
CompositeEditCommand::deleteSelection(smartDelete);
typingAddedToOpenCommand();
}
bool TypingCommand::preservesTypingStyle() const
{
switch (m_commandType) {
case DeleteSelection:
case DeleteKey:
case ForwardDeleteKey:
case InsertParagraphSeparator:
case InsertLineBreak:
return true;
case InsertParagraphSeparatorInQuotedContent:
case InsertText:
return false;
}
ASSERT_NOT_REACHED();
return false;
}
bool TypingCommand::isTypingCommand() const
{
return true;
}
} // namespace WebCore