1 /*
2 * Copyright (C) 2005, 2006, 2007, 2008 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26 #include "config.h"
27 #include "TypingCommand.h"
28
29 #include "BeforeTextInsertedEvent.h"
30 #include "BreakBlockquoteCommand.h"
31 #include "DeleteSelectionCommand.h"
32 #include "Document.h"
33 #include "Editor.h"
34 #include "Element.h"
35 #include "Frame.h"
36 #include "InsertLineBreakCommand.h"
37 #include "InsertParagraphSeparatorCommand.h"
38 #include "InsertTextCommand.h"
39 #include "SelectionController.h"
40 #include "VisiblePosition.h"
41 #include "htmlediting.h"
42 #include "visible_units.h"
43
44 namespace WebCore {
45
TypingCommand(Document * document,ETypingCommand commandType,const String & textToInsert,bool selectInsertedText,TextGranularity granularity,bool killRing)46 TypingCommand::TypingCommand(Document *document, ETypingCommand commandType, const String &textToInsert, bool selectInsertedText, TextGranularity granularity, bool killRing)
47 : CompositeEditCommand(document),
48 m_commandType(commandType),
49 m_textToInsert(textToInsert),
50 m_openForMoreTyping(true),
51 m_applyEditing(false),
52 m_selectInsertedText(selectInsertedText),
53 m_smartDelete(false),
54 m_granularity(granularity),
55 m_killRing(killRing),
56 m_openedByBackwardDelete(false)
57 {
58 }
59
deleteSelection(Document * document,bool smartDelete)60 void TypingCommand::deleteSelection(Document* document, bool smartDelete)
61 {
62 ASSERT(document);
63
64 Frame* frame = document->frame();
65 ASSERT(frame);
66
67 if (!frame->selection()->isRange())
68 return;
69
70 EditCommand* lastEditCommand = frame->editor()->lastEditCommand();
71 if (isOpenForMoreTypingCommand(lastEditCommand)) {
72 static_cast<TypingCommand*>(lastEditCommand)->deleteSelection(smartDelete);
73 return;
74 }
75
76 RefPtr<TypingCommand> typingCommand = TypingCommand::create(document, DeleteSelection, "", false);
77 typingCommand->setSmartDelete(smartDelete);
78 typingCommand->apply();
79 }
80
deleteKeyPressed(Document * document,bool smartDelete,TextGranularity granularity,bool killRing)81 void TypingCommand::deleteKeyPressed(Document *document, bool smartDelete, TextGranularity granularity, bool killRing)
82 {
83 ASSERT(document);
84
85 Frame *frame = document->frame();
86 ASSERT(frame);
87
88 EditCommand* lastEditCommand = frame->editor()->lastEditCommand();
89 if (isOpenForMoreTypingCommand(lastEditCommand)) {
90 static_cast<TypingCommand*>(lastEditCommand)->deleteKeyPressed(granularity, killRing);
91 return;
92 }
93
94 RefPtr<TypingCommand> typingCommand = TypingCommand::create(document, DeleteKey, "", false, granularity, killRing);
95 typingCommand->setSmartDelete(smartDelete);
96 typingCommand->apply();
97 }
98
forwardDeleteKeyPressed(Document * document,bool smartDelete,TextGranularity granularity,bool killRing)99 void TypingCommand::forwardDeleteKeyPressed(Document *document, bool smartDelete, TextGranularity granularity, bool killRing)
100 {
101 // FIXME: Forward delete in TextEdit appears to open and close a new typing command.
102 ASSERT(document);
103
104 Frame *frame = document->frame();
105 ASSERT(frame);
106
107 EditCommand* lastEditCommand = frame->editor()->lastEditCommand();
108 if (isOpenForMoreTypingCommand(lastEditCommand)) {
109 static_cast<TypingCommand*>(lastEditCommand)->forwardDeleteKeyPressed(granularity, killRing);
110 return;
111 }
112
113 RefPtr<TypingCommand> typingCommand = TypingCommand::create(document, ForwardDeleteKey, "", false, granularity, killRing);
114 typingCommand->setSmartDelete(smartDelete);
115 typingCommand->apply();
116 }
117
insertText(Document * document,const String & text,bool selectInsertedText,bool insertedTextIsComposition)118 void TypingCommand::insertText(Document* document, const String& text, bool selectInsertedText, bool insertedTextIsComposition)
119 {
120 ASSERT(document);
121
122 Frame* frame = document->frame();
123 ASSERT(frame);
124
125 insertText(document, text, frame->selection()->selection(), selectInsertedText, insertedTextIsComposition);
126 }
127
insertText(Document * document,const String & text,const Selection & selectionForInsertion,bool selectInsertedText,bool insertedTextIsComposition)128 void TypingCommand::insertText(Document* document, const String& text, const Selection& selectionForInsertion, bool selectInsertedText, bool insertedTextIsComposition)
129 {
130 ASSERT(document);
131
132 RefPtr<Frame> frame = document->frame();
133 ASSERT(frame);
134
135 Selection currentSelection = frame->selection()->selection();
136 bool changeSelection = currentSelection != selectionForInsertion;
137
138 String newText = text;
139 Node* startNode = selectionForInsertion.start().node();
140
141 if (startNode && startNode->rootEditableElement() && !insertedTextIsComposition) {
142 // Send BeforeTextInsertedEvent. The event handler will update text if necessary.
143 ExceptionCode ec = 0;
144 RefPtr<BeforeTextInsertedEvent> evt = BeforeTextInsertedEvent::create(text);
145 startNode->rootEditableElement()->dispatchEvent(evt, ec);
146 newText = evt->text();
147 }
148
149 if (newText.isEmpty())
150 return;
151
152 // Set the starting and ending selection appropriately if we are using a selection
153 // that is different from the current selection. In the future, we should change EditCommand
154 // to deal with custom selections in a general way that can be used by all of the commands.
155 RefPtr<EditCommand> lastEditCommand = frame->editor()->lastEditCommand();
156 if (isOpenForMoreTypingCommand(lastEditCommand.get())) {
157 TypingCommand* lastTypingCommand = static_cast<TypingCommand*>(lastEditCommand.get());
158 if (changeSelection) {
159 lastTypingCommand->setStartingSelection(selectionForInsertion);
160 lastTypingCommand->setEndingSelection(selectionForInsertion);
161 }
162 lastTypingCommand->insertText(newText, selectInsertedText);
163 if (changeSelection) {
164 lastTypingCommand->setEndingSelection(currentSelection);
165 frame->selection()->setSelection(currentSelection);
166 }
167 return;
168 }
169
170 RefPtr<TypingCommand> cmd = TypingCommand::create(document, InsertText, newText, selectInsertedText);
171 if (changeSelection) {
172 cmd->setStartingSelection(selectionForInsertion);
173 cmd->setEndingSelection(selectionForInsertion);
174 }
175 applyCommand(cmd);
176 if (changeSelection) {
177 cmd->setEndingSelection(currentSelection);
178 frame->selection()->setSelection(currentSelection);
179 }
180 }
181
insertLineBreak(Document * document)182 void TypingCommand::insertLineBreak(Document *document)
183 {
184 ASSERT(document);
185
186 Frame *frame = document->frame();
187 ASSERT(frame);
188
189 EditCommand* lastEditCommand = frame->editor()->lastEditCommand();
190 if (isOpenForMoreTypingCommand(lastEditCommand)) {
191 static_cast<TypingCommand*>(lastEditCommand)->insertLineBreak();
192 return;
193 }
194
195 applyCommand(TypingCommand::create(document, InsertLineBreak));
196 }
197
insertParagraphSeparatorInQuotedContent(Document * document)198 void TypingCommand::insertParagraphSeparatorInQuotedContent(Document *document)
199 {
200 ASSERT(document);
201
202 Frame *frame = document->frame();
203 ASSERT(frame);
204
205 EditCommand* lastEditCommand = frame->editor()->lastEditCommand();
206 if (isOpenForMoreTypingCommand(lastEditCommand)) {
207 static_cast<TypingCommand*>(lastEditCommand)->insertParagraphSeparatorInQuotedContent();
208 return;
209 }
210
211 applyCommand(TypingCommand::create(document, InsertParagraphSeparatorInQuotedContent));
212 }
213
insertParagraphSeparator(Document * document)214 void TypingCommand::insertParagraphSeparator(Document *document)
215 {
216 ASSERT(document);
217
218 Frame *frame = document->frame();
219 ASSERT(frame);
220
221 EditCommand* lastEditCommand = frame->editor()->lastEditCommand();
222 if (isOpenForMoreTypingCommand(lastEditCommand)) {
223 static_cast<TypingCommand*>(lastEditCommand)->insertParagraphSeparator();
224 return;
225 }
226
227 applyCommand(TypingCommand::create(document, InsertParagraphSeparator));
228 }
229
isOpenForMoreTypingCommand(const EditCommand * cmd)230 bool TypingCommand::isOpenForMoreTypingCommand(const EditCommand* cmd)
231 {
232 return cmd && cmd->isTypingCommand() && static_cast<const TypingCommand*>(cmd)->isOpenForMoreTyping();
233 }
234
closeTyping(EditCommand * cmd)235 void TypingCommand::closeTyping(EditCommand* cmd)
236 {
237 if (isOpenForMoreTypingCommand(cmd))
238 static_cast<TypingCommand*>(cmd)->closeTyping();
239 }
240
doApply()241 void TypingCommand::doApply()
242 {
243 if (endingSelection().isNone())
244 return;
245
246 if (m_commandType == DeleteKey)
247 if (m_commands.isEmpty())
248 m_openedByBackwardDelete = true;
249
250 switch (m_commandType) {
251 case DeleteSelection:
252 deleteSelection(m_smartDelete);
253 return;
254 case DeleteKey:
255 deleteKeyPressed(m_granularity, m_killRing);
256 return;
257 case ForwardDeleteKey:
258 forwardDeleteKeyPressed(m_granularity, m_killRing);
259 return;
260 case InsertLineBreak:
261 insertLineBreak();
262 return;
263 case InsertParagraphSeparator:
264 insertParagraphSeparator();
265 return;
266 case InsertParagraphSeparatorInQuotedContent:
267 insertParagraphSeparatorInQuotedContent();
268 return;
269 case InsertText:
270 insertText(m_textToInsert, m_selectInsertedText);
271 return;
272 }
273
274 ASSERT_NOT_REACHED();
275 }
276
editingAction() const277 EditAction TypingCommand::editingAction() const
278 {
279 return EditActionTyping;
280 }
281
markMisspellingsAfterTyping()282 void TypingCommand::markMisspellingsAfterTyping()
283 {
284 if (!document()->frame()->editor()->isContinuousSpellCheckingEnabled())
285 return;
286 // Take a look at the selection that results after typing and determine whether we need to spellcheck.
287 // Since the word containing the current selection is never marked, this does a check to
288 // see if typing made a new word that is not in the current selection. Basically, you
289 // get this by being at the end of a word and typing a space.
290 VisiblePosition start(endingSelection().start(), endingSelection().affinity());
291 VisiblePosition previous = start.previous();
292 if (previous.isNotNull()) {
293 VisiblePosition p1 = startOfWord(previous, LeftWordIfOnBoundary);
294 VisiblePosition p2 = startOfWord(start, LeftWordIfOnBoundary);
295 if (p1 != p2)
296 document()->frame()->editor()->markMisspellingsAfterTypingToPosition(p1);
297 }
298 }
299
typingAddedToOpenCommand()300 void TypingCommand::typingAddedToOpenCommand()
301 {
302 markMisspellingsAfterTyping();
303 // Do not apply editing to the frame on the first time through.
304 // The frame will get told in the same way as all other commands.
305 // But since this command stays open and is used for additional typing,
306 // we need to tell the frame here as other commands are added.
307 if (m_applyEditing)
308 document()->frame()->editor()->appliedEditing(this);
309 m_applyEditing = true;
310 }
311
insertText(const String & text,bool selectInsertedText)312 void TypingCommand::insertText(const String &text, bool selectInsertedText)
313 {
314 // FIXME: Need to implement selectInsertedText for cases where more than one insert is involved.
315 // This requires support from insertTextRunWithoutNewlines and insertParagraphSeparator for extending
316 // an existing selection; at the moment they can either put the caret after what's inserted or
317 // select what's inserted, but there's no way to "extend selection" to include both an old selection
318 // that ends just before where we want to insert text and the newly inserted text.
319 int offset = 0;
320 int newline;
321 while ((newline = text.find('\n', offset)) != -1) {
322 if (newline != offset)
323 insertTextRunWithoutNewlines(text.substring(offset, newline - offset), false);
324 insertParagraphSeparator();
325 offset = newline + 1;
326 }
327 if (offset == 0)
328 insertTextRunWithoutNewlines(text, selectInsertedText);
329 else {
330 int length = text.length();
331 if (length != offset) {
332 insertTextRunWithoutNewlines(text.substring(offset, length - offset), selectInsertedText);
333 }
334 }
335 }
336
insertTextRunWithoutNewlines(const String & text,bool selectInsertedText)337 void TypingCommand::insertTextRunWithoutNewlines(const String &text, bool selectInsertedText)
338 {
339 RefPtr<InsertTextCommand> command;
340 if (!document()->frame()->typingStyle() && !m_commands.isEmpty()) {
341 EditCommand* lastCommand = m_commands.last().get();
342 if (lastCommand->isInsertTextCommand())
343 command = static_cast<InsertTextCommand*>(lastCommand);
344 }
345 if (!command) {
346 command = InsertTextCommand::create(document());
347 applyCommandToComposite(command);
348 }
349 command->input(text, selectInsertedText);
350 typingAddedToOpenCommand();
351 }
352
insertLineBreak()353 void TypingCommand::insertLineBreak()
354 {
355 applyCommandToComposite(InsertLineBreakCommand::create(document()));
356 typingAddedToOpenCommand();
357 }
358
insertParagraphSeparator()359 void TypingCommand::insertParagraphSeparator()
360 {
361 applyCommandToComposite(InsertParagraphSeparatorCommand::create(document()));
362 typingAddedToOpenCommand();
363 }
364
insertParagraphSeparatorInQuotedContent()365 void TypingCommand::insertParagraphSeparatorInQuotedContent()
366 {
367 applyCommandToComposite(BreakBlockquoteCommand::create(document()));
368 typingAddedToOpenCommand();
369 }
370
deleteKeyPressed(TextGranularity granularity,bool killRing)371 void TypingCommand::deleteKeyPressed(TextGranularity granularity, bool killRing)
372 {
373 Selection selectionToDelete;
374 Selection selectionAfterUndo;
375
376 switch (endingSelection().state()) {
377 case Selection::RANGE:
378 selectionToDelete = endingSelection();
379 selectionAfterUndo = selectionToDelete;
380 break;
381 case Selection::CARET: {
382 if (breakOutOfEmptyMailBlockquotedParagraph()) {
383 typingAddedToOpenCommand();
384 return;
385 }
386
387 m_smartDelete = false;
388
389 SelectionController selection;
390 selection.setSelection(endingSelection());
391 selection.modify(SelectionController::EXTEND, SelectionController::BACKWARD, granularity);
392 if (killRing && selection.isCaret() && granularity != CharacterGranularity)
393 selection.modify(SelectionController::EXTEND, SelectionController::BACKWARD, CharacterGranularity);
394
395 // When the caret is at the start of the editable area in an empty list item, break out of the list item.
396 if (endingSelection().visibleStart().previous(true).isNull()) {
397 if (breakOutOfEmptyListItem()) {
398 typingAddedToOpenCommand();
399 return;
400 }
401 }
402
403 VisiblePosition visibleStart(endingSelection().visibleStart());
404 // If the caret is at the start of a paragraph after a table, move content into the last table cell.
405 if (isStartOfParagraph(visibleStart) && isFirstPositionAfterTable(visibleStart.previous(true))) {
406 // Unless the caret is just before a table. We don't want to move a table into the last table cell.
407 if (isLastPositionBeforeTable(visibleStart))
408 return;
409 // Extend the selection backward into the last cell, then deletion will handle the move.
410 selection.modify(SelectionController::EXTEND, SelectionController::BACKWARD, granularity);
411 // If the caret is just after a table, select the table and don't delete anything.
412 } else if (Node* table = isFirstPositionAfterTable(visibleStart)) {
413 setEndingSelection(Selection(Position(table, 0), endingSelection().start(), DOWNSTREAM));
414 typingAddedToOpenCommand();
415 return;
416 }
417
418 selectionToDelete = selection.selection();
419
420 if (granularity == CharacterGranularity && selectionToDelete.end().offset() - selectionToDelete.start().offset() > 1) {
421 // When we delete a ligature consisting of multiple Unicode characters with a backspace key,
422 // we should not delete the ligature but delete only its last characeter. To check we are deleting
423 // a ligature, we retrieve the previous position of the caret and count the number of
424 // characters to be deleted.
425 // To prevent from calculating the previous position every time when pressing a backspace key,
426 // we retrieve the previous position only when the given selection consists of two or more characters.
427 if (selectionToDelete.end().offset() - selectionToDelete.end().previous(UsingComposedCharacters).offset() > 1)
428 selectionToDelete.setWithoutValidation(selectionToDelete.end(), selectionToDelete.end().previous(NotUsingComposedCharacters));
429 }
430
431 if (!startingSelection().isRange() || selectionToDelete.base() != startingSelection().start())
432 selectionAfterUndo = selectionToDelete;
433 else
434 // It's a little tricky to compute what the starting selection would have been in the original document.
435 // We can't let the Selection class's validation kick in or it'll adjust for us based on
436 // the current state of the document and we'll get the wrong result.
437 selectionAfterUndo.setWithoutValidation(startingSelection().end(), selectionToDelete.extent());
438 break;
439 }
440 case Selection::NONE:
441 ASSERT_NOT_REACHED();
442 break;
443 }
444
445 if (selectionToDelete.isCaretOrRange() && document()->frame()->shouldDeleteSelection(selectionToDelete)) {
446 if (killRing)
447 document()->frame()->editor()->addToKillRing(selectionToDelete.toRange().get(), false);
448 // Make undo select everything that has been deleted, unless an undo will undo more than just this deletion.
449 // FIXME: This behaves like TextEdit except for the case where you open with text insertion and then delete
450 // more text than you insert. In that case all of the text that was around originally should be selected.
451 if (m_openedByBackwardDelete)
452 setStartingSelection(selectionAfterUndo);
453 CompositeEditCommand::deleteSelection(selectionToDelete, m_smartDelete);
454 setSmartDelete(false);
455 typingAddedToOpenCommand();
456 }
457 }
458
forwardDeleteKeyPressed(TextGranularity granularity,bool killRing)459 void TypingCommand::forwardDeleteKeyPressed(TextGranularity granularity, bool killRing)
460 {
461 Selection selectionToDelete;
462 Selection selectionAfterUndo;
463
464 switch (endingSelection().state()) {
465 case Selection::RANGE:
466 selectionToDelete = endingSelection();
467 selectionAfterUndo = selectionToDelete;
468 break;
469 case Selection::CARET: {
470 m_smartDelete = false;
471
472 // Handle delete at beginning-of-block case.
473 // Do nothing in the case that the caret is at the start of a
474 // root editable element or at the start of a document.
475 SelectionController selection;
476 selection.setSelection(endingSelection());
477 selection.modify(SelectionController::EXTEND, SelectionController::FORWARD, granularity);
478 if (killRing && selection.isCaret() && granularity != CharacterGranularity)
479 selection.modify(SelectionController::EXTEND, SelectionController::FORWARD, CharacterGranularity);
480
481 Position downstreamEnd = endingSelection().end().downstream();
482 VisiblePosition visibleEnd = endingSelection().visibleEnd();
483 if (visibleEnd == endOfParagraph(visibleEnd))
484 downstreamEnd = visibleEnd.next(true).deepEquivalent().downstream();
485 // When deleting tables: Select the table first, then perform the deletion
486 if (downstreamEnd.node() && downstreamEnd.node()->renderer() && downstreamEnd.node()->renderer()->isTable() && downstreamEnd.offset() == 0) {
487 setEndingSelection(Selection(endingSelection().end(), Position(downstreamEnd.node(), maxDeepOffset(downstreamEnd.node())), DOWNSTREAM));
488 typingAddedToOpenCommand();
489 return;
490 }
491
492 // deleting to end of paragraph when at end of paragraph needs to merge the next paragraph (if any)
493 if (granularity == ParagraphBoundary && selection.selection().isCaret() && isEndOfParagraph(selection.selection().visibleEnd()))
494 selection.modify(SelectionController::EXTEND, SelectionController::FORWARD, CharacterGranularity);
495
496 selectionToDelete = selection.selection();
497 if (!startingSelection().isRange() || selectionToDelete.base() != startingSelection().start())
498 selectionAfterUndo = selectionToDelete;
499 else {
500 // It's a little tricky to compute what the starting selection would have been in the original document.
501 // We can't let the Selection class's validation kick in or it'll adjust for us based on
502 // the current state of the document and we'll get the wrong result.
503 Position extent = startingSelection().end();
504 if (extent.node() != selectionToDelete.end().node())
505 extent = selectionToDelete.extent();
506 else {
507 int extraCharacters;
508 if (selectionToDelete.start().node() == selectionToDelete.end().node())
509 extraCharacters = selectionToDelete.end().offset() - selectionToDelete.start().offset();
510 else
511 extraCharacters = selectionToDelete.end().offset();
512 extent = Position(extent.node(), extent.offset() + extraCharacters);
513 }
514 selectionAfterUndo.setWithoutValidation(startingSelection().start(), extent);
515 }
516 break;
517 }
518 case Selection::NONE:
519 ASSERT_NOT_REACHED();
520 break;
521 }
522
523 if (selectionToDelete.isCaretOrRange() && document()->frame()->shouldDeleteSelection(selectionToDelete)) {
524 if (killRing)
525 document()->frame()->editor()->addToKillRing(selectionToDelete.toRange().get(), false);
526 // make undo select what was deleted
527 setStartingSelection(selectionAfterUndo);
528 CompositeEditCommand::deleteSelection(selectionToDelete, m_smartDelete);
529 setSmartDelete(false);
530 typingAddedToOpenCommand();
531 }
532 }
533
deleteSelection(bool smartDelete)534 void TypingCommand::deleteSelection(bool smartDelete)
535 {
536 CompositeEditCommand::deleteSelection(smartDelete);
537 typingAddedToOpenCommand();
538 }
539
preservesTypingStyle() const540 bool TypingCommand::preservesTypingStyle() const
541 {
542 switch (m_commandType) {
543 case DeleteSelection:
544 case DeleteKey:
545 case ForwardDeleteKey:
546 case InsertParagraphSeparator:
547 case InsertLineBreak:
548 return true;
549 case InsertParagraphSeparatorInQuotedContent:
550 case InsertText:
551 return false;
552 }
553 ASSERT_NOT_REACHED();
554 return false;
555 }
556
isTypingCommand() const557 bool TypingCommand::isTypingCommand() const
558 {
559 return true;
560 }
561
562 } // namespace WebCore
563