2008-11-04 Anders Carlsson <andersca@apple.com>
[webkit/qt.git] / WebCore / editing / IndentOutdentCommand.cpp
blob385f1b7caac367d79ca24c51095d87b8dd7c263d
1 /*
2 * Copyright (C) 2006, 2008 Apple Inc. All rights reserved.
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.
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 (IndentOutdentCommandINCLUDING, 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.
26 #include "config.h"
27 #include "Element.h"
28 #include "IndentOutdentCommand.h"
29 #include "InsertListCommand.h"
30 #include "Document.h"
31 #include "htmlediting.h"
32 #include "HTMLElement.h"
33 #include "HTMLNames.h"
34 #include "InsertLineBreakCommand.h"
35 #include "Range.h"
36 #include "SplitElementCommand.h"
37 #include "TextIterator.h"
38 #include "visible_units.h"
40 namespace WebCore {
42 using namespace HTMLNames;
44 static String indentBlockquoteString()
46 static String string = "webkit-indent-blockquote";
47 return string;
50 static PassRefPtr<Element> createIndentBlockquoteElement(Document* document)
52 RefPtr<Element> indentBlockquoteElement = createElement(document, "blockquote");
53 indentBlockquoteElement->setAttribute(classAttr, indentBlockquoteString());
54 indentBlockquoteElement->setAttribute(styleAttr, "margin: 0 0 0 40px; border: none; padding: 0px;");
55 return indentBlockquoteElement.release();
58 static bool isIndentBlockquote(const Node* node)
60 if (!node || !node->hasTagName(blockquoteTag) || !node->isElementNode())
61 return false;
63 const Element* elem = static_cast<const Element*>(node);
64 return elem->getAttribute(classAttr) == indentBlockquoteString();
67 static bool isListOrIndentBlockquote(const Node* node)
69 return node && (node->hasTagName(ulTag) || node->hasTagName(olTag) || isIndentBlockquote(node));
72 IndentOutdentCommand::IndentOutdentCommand(Document* document, EIndentType typeOfAction, int marginInPixels)
73 : CompositeEditCommand(document), m_typeOfAction(typeOfAction), m_marginInPixels(marginInPixels)
76 // This function is a workaround for moveParagraph's tendency to strip blockquotes. It updates lastBlockquote to point to the
77 // correct level for the current paragraph, and returns a pointer to a placeholder br where the insertion should be performed.
78 Node* IndentOutdentCommand::prepareBlockquoteLevelForInsertion(VisiblePosition& currentParagraph, Node** lastBlockquote)
80 int currentBlockquoteLevel = 0;
81 int lastBlockquoteLevel = 0;
82 Node* node = currentParagraph.deepEquivalent().node();
83 while ((node = enclosingNodeOfType(Position(node->parentNode(), 0), &isIndentBlockquote)))
84 currentBlockquoteLevel++;
85 node = *lastBlockquote;
86 while ((node = enclosingNodeOfType(Position(node->parentNode(), 0), &isIndentBlockquote)))
87 lastBlockquoteLevel++;
88 while (currentBlockquoteLevel > lastBlockquoteLevel) {
89 RefPtr<Node> newBlockquote = createIndentBlockquoteElement(document());
90 appendNode(newBlockquote.get(), *lastBlockquote);
91 *lastBlockquote = newBlockquote.get();
92 lastBlockquoteLevel++;
94 while (currentBlockquoteLevel < lastBlockquoteLevel) {
95 *lastBlockquote = enclosingNodeOfType(Position((*lastBlockquote)->parentNode(), 0), &isIndentBlockquote);
96 lastBlockquoteLevel--;
98 RefPtr<Node> placeholder = createBreakElement(document());
99 appendNode(placeholder.get(), *lastBlockquote);
100 // Add another br before the placeholder if it collapsed.
101 VisiblePosition visiblePos(Position(placeholder.get(), 0));
102 if (!isStartOfParagraph(visiblePos))
103 insertNodeBefore(createBreakElement(document()).get(), placeholder.get());
104 return placeholder.get();
107 void IndentOutdentCommand::indentRegion()
109 Selection selection = selectionForParagraphIteration(endingSelection());
110 VisiblePosition startOfSelection = selection.visibleStart();
111 VisiblePosition endOfSelection = selection.visibleEnd();
112 int startIndex = indexForVisiblePosition(startOfSelection);
113 int endIndex = indexForVisiblePosition(endOfSelection);
115 ASSERT(!startOfSelection.isNull());
116 ASSERT(!endOfSelection.isNull());
118 // Special case empty root editable elements because there's nothing to split
119 // and there's nothing to move.
120 Position start = startOfSelection.deepEquivalent().downstream();
121 if (start.node() == editableRootForPosition(start)) {
122 RefPtr<Node> blockquote = createIndentBlockquoteElement(document());
123 insertNodeAt(blockquote.get(), start);
124 RefPtr<Node> placeholder = createBreakElement(document());
125 appendNode(placeholder.get(), blockquote.get());
126 setEndingSelection(Selection(Position(placeholder.get(), 0), DOWNSTREAM));
127 return;
130 Node* previousListNode = 0;
131 Node* newListNode = 0;
132 Node* newBlockquote = 0;
133 VisiblePosition endOfCurrentParagraph = endOfParagraph(startOfSelection);
134 VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
135 while (endOfCurrentParagraph != endAfterSelection) {
136 // Iterate across the selected paragraphs...
137 VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
138 Node* listNode = enclosingList(endOfCurrentParagraph.deepEquivalent().node());
139 Node* insertionPoint;
140 if (listNode) {
141 RefPtr<Node> placeholder = createBreakElement(document());
142 insertionPoint = placeholder.get();
143 newBlockquote = 0;
144 RefPtr<Node> listItem = createListItemElement(document());
145 if (listNode == previousListNode) {
146 // The previous paragraph was inside the same list, so add this list item to the list we already created
147 appendNode(listItem.get(), newListNode);
148 appendNode(placeholder.get(), listItem.get());
149 } else {
150 // Clone the list element, insert it before the current paragraph, and move the paragraph into it.
151 RefPtr<Node> clonedList = static_cast<Element*>(listNode)->cloneNode(false);
152 insertNodeBefore(clonedList.get(), enclosingListChild(endOfCurrentParagraph.deepEquivalent().node()));
153 appendNode(listItem.get(), clonedList.get());
154 appendNode(placeholder.get(), listItem.get());
155 newListNode = clonedList.get();
156 previousListNode = listNode;
158 } else if (newBlockquote)
159 // The previous paragraph was put into a new blockquote, so move this paragraph there as well
160 insertionPoint = prepareBlockquoteLevelForInsertion(endOfCurrentParagraph, &newBlockquote);
161 else {
162 // Create a new blockquote and insert it as a child of the root editable element. We accomplish
163 // this by splitting all parents of the current paragraph up to that point.
164 RefPtr<Node> blockquote = createIndentBlockquoteElement(document());
165 Position start = startOfParagraph(endOfCurrentParagraph).deepEquivalent();
167 Node* enclosingCell = enclosingNodeOfType(start, &isTableCell);
168 Node* nodeToSplitTo = enclosingCell ? enclosingCell : editableRootForPosition(start);
169 RefPtr<Node> startOfNewBlock = splitTreeToNode(start.node(), nodeToSplitTo);
170 insertNodeBefore(blockquote.get(), startOfNewBlock.get());
171 newBlockquote = blockquote.get();
172 insertionPoint = prepareBlockquoteLevelForInsertion(endOfCurrentParagraph, &newBlockquote);
173 // Don't put the next paragraph in the blockquote we just created for this paragraph unless
174 // the next paragraph is in the same cell.
175 if (enclosingCell && enclosingCell != enclosingNodeOfType(endOfNextParagraph.deepEquivalent(), &isTableCell))
176 newBlockquote = 0;
178 moveParagraph(startOfParagraph(endOfCurrentParagraph), endOfCurrentParagraph, VisiblePosition(Position(insertionPoint, 0)), true);
179 // moveParagraph should not destroy content that contains endOfNextParagraph, but if it does, return here
180 // to avoid a crash.
181 if (endOfNextParagraph.isNotNull() && !endOfNextParagraph.deepEquivalent().node()->inDocument()) {
182 ASSERT_NOT_REACHED();
183 return;
185 endOfCurrentParagraph = endOfNextParagraph;
188 RefPtr<Range> startRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), startIndex, 0, true);
189 RefPtr<Range> endRange = TextIterator::rangeFromLocationAndLength(document()->documentElement(), endIndex, 0, true);
190 if (startRange && endRange)
191 setEndingSelection(Selection(startRange->startPosition(), endRange->startPosition(), DOWNSTREAM));
194 void IndentOutdentCommand::outdentParagraph()
196 VisiblePosition visibleStartOfParagraph = startOfParagraph(endingSelection().visibleStart());
197 VisiblePosition visibleEndOfParagraph = endOfParagraph(visibleStartOfParagraph);
199 Node* enclosingNode = enclosingNodeOfType(visibleStartOfParagraph.deepEquivalent(), &isListOrIndentBlockquote);
200 if (!enclosingNode)
201 return;
203 // Use InsertListCommand to remove the selection from the list
204 if (enclosingNode->hasTagName(olTag)) {
205 applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::OrderedList, ""));
206 return;
208 if (enclosingNode->hasTagName(ulTag)) {
209 applyCommandToComposite(InsertListCommand::create(document(), InsertListCommand::UnorderedList, ""));
210 return;
213 // The selection is inside a blockquote
214 VisiblePosition positionInEnclosingBlock = VisiblePosition(Position(enclosingNode, 0));
215 VisiblePosition startOfEnclosingBlock = startOfBlock(positionInEnclosingBlock);
216 VisiblePosition endOfEnclosingBlock = endOfBlock(positionInEnclosingBlock);
217 if (visibleStartOfParagraph == startOfEnclosingBlock &&
218 visibleEndOfParagraph == endOfEnclosingBlock) {
219 // The blockquote doesn't contain anything outside the paragraph, so it can be totally removed.
220 removeNodePreservingChildren(enclosingNode);
221 updateLayout();
222 visibleStartOfParagraph = VisiblePosition(visibleStartOfParagraph.deepEquivalent());
223 visibleEndOfParagraph = VisiblePosition(visibleEndOfParagraph.deepEquivalent());
224 if (visibleStartOfParagraph.isNotNull() && !isStartOfParagraph(visibleStartOfParagraph))
225 insertNodeAt(createBreakElement(document()).get(), visibleStartOfParagraph.deepEquivalent());
226 if (visibleEndOfParagraph.isNotNull() && !isEndOfParagraph(visibleEndOfParagraph))
227 insertNodeAt(createBreakElement(document()).get(), visibleEndOfParagraph.deepEquivalent());
228 return;
230 Node* enclosingBlockFlow = enclosingBlockFlowElement(visibleStartOfParagraph);
231 RefPtr<Node> splitBlockquoteNode = enclosingNode;
232 if (enclosingBlockFlow != enclosingNode)
233 splitBlockquoteNode = splitTreeToNode(enclosingBlockFlowElement(visibleStartOfParagraph), enclosingNode, true);
234 RefPtr<Node> placeholder = createBreakElement(document());
235 insertNodeBefore(placeholder.get(), splitBlockquoteNode.get());
236 moveParagraph(startOfParagraph(visibleStartOfParagraph), endOfParagraph(visibleEndOfParagraph), VisiblePosition(Position(placeholder.get(), 0)), true);
239 void IndentOutdentCommand::outdentRegion()
241 VisiblePosition startOfSelection = endingSelection().visibleStart();
242 VisiblePosition endOfSelection = endingSelection().visibleEnd();
243 VisiblePosition endOfLastParagraph = endOfParagraph(endOfSelection);
245 ASSERT(!startOfSelection.isNull());
246 ASSERT(!endOfSelection.isNull());
248 if (endOfParagraph(startOfSelection) == endOfLastParagraph) {
249 outdentParagraph();
250 return;
253 Position originalSelectionEnd = endingSelection().end();
254 setEndingSelection(endingSelection().visibleStart());
255 outdentParagraph();
256 Position originalSelectionStart = endingSelection().start();
257 VisiblePosition endOfCurrentParagraph = endOfParagraph(endOfParagraph(endingSelection().visibleStart()).next(true));
258 VisiblePosition endAfterSelection = endOfParagraph(endOfParagraph(endOfSelection).next());
259 while (endOfCurrentParagraph != endAfterSelection) {
260 VisiblePosition endOfNextParagraph = endOfParagraph(endOfCurrentParagraph.next());
261 if (endOfCurrentParagraph == endOfLastParagraph)
262 setEndingSelection(Selection(originalSelectionEnd, DOWNSTREAM));
263 else
264 setEndingSelection(endOfCurrentParagraph);
265 outdentParagraph();
266 endOfCurrentParagraph = endOfNextParagraph;
268 setEndingSelection(Selection(originalSelectionStart, endingSelection().end(), DOWNSTREAM));
271 void IndentOutdentCommand::doApply()
273 if (endingSelection().isNone())
274 return;
276 if (!endingSelection().rootEditableElement())
277 return;
279 VisiblePosition visibleEnd = endingSelection().visibleEnd();
280 VisiblePosition visibleStart = endingSelection().visibleStart();
281 // When a selection ends at the start of a paragraph, we rarely paint
282 // the selection gap before that paragraph, because there often is no gap.
283 // In a case like this, it's not obvious to the user that the selection
284 // ends "inside" that paragraph, so it would be confusing if Indent/Outdent
285 // operated on that paragraph.
286 // FIXME: We paint the gap before some paragraphs that are indented with left
287 // margin/padding, but not others. We should make the gap painting more consistent and
288 // then use a left margin/padding rule here.
289 if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd))
290 setEndingSelection(Selection(visibleStart, visibleEnd.previous(true)));
292 if (m_typeOfAction == Indent)
293 indentRegion();
294 else
295 outdentRegion();