fix tricky regression noticed by Vyacheslav Tokarev on Google Reader.
[kdelibs.git] / khtml / editing / editor.cpp
blobaa1158288ac7340b91afef25a2e2c78ab4dce45a
1 /* This file is part of the KDE project
3 * Copyright (C) 2004 Leo Savernik <l.savernik@aon.at>
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Library General Public
7 * License as published by the Free Software Foundation; either
8 * version 2 of the License, or (at your option) any later version.
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Library General Public License for more details.
15 * You should have received a copy of the GNU Library General Public License
16 * along with this library; see the file COPYING.LIB. If not, write to
17 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
18 * Boston, MA 02111-1307, USA.
21 #include "editor.h"
23 #include "edit_command.h"
24 #include "htmlediting.h"
25 #include "jsediting.h"
27 #include "css/css_renderstyledeclarationimpl.h"
28 #include "css/css_valueimpl.h"
29 #include "misc/htmlattrs.h"
30 #include "xml/dom_selection.h"
31 #include "xml/dom_docimpl.h"
32 #include "xml/dom_elementimpl.h"
33 #include "xml/dom_textimpl.h"
34 #include "xml/dom2_rangeimpl.h"
35 #include "khtml_part.h"
36 #include "khtml_ext.h"
37 #include "khtmlpart_p.h"
39 #include <QStack>
41 #ifndef APPLE_CHANGES
42 # ifdef assert
43 # undef assert
44 # endif
45 # define assert(x) Q_ASSERT(x)
46 #endif
48 #define PREPARE_JSEDITOR_CALL(command, retval) \
49 JSEditor *js = m_part->xmlDocImpl() ? m_part->xmlDocImpl()->jsEditor() : 0; \
50 if (!js) return retval; \
51 const CommandImp *imp = js->commandImp(command)
53 // --------------------------------------------------------------------------
55 namespace DOM {
57 static const int sMaxUndoSteps = 1000;
59 class EditorPrivate {
60 public:
61 void registerUndo( const khtml::EditCommand& cmd, bool clearRedoStack = true ) {
62 if (m_undo.count()>= sMaxUndoSteps)
63 m_undo.pop_front();
64 if (clearRedoStack)
65 m_redo.clear();
66 m_undo.push( cmd );
68 void registerRedo( const khtml::EditCommand& cmd ) {
69 if (m_redo.count()>= sMaxUndoSteps)
70 m_redo.pop_front();
71 m_redo.push( cmd );
73 khtml::EditCommand m_lastEditCommand;
74 QStack<khtml::EditCommand> m_undo;
75 QStack<khtml::EditCommand> m_redo;
81 using namespace DOM;
82 using khtml::ApplyStyleCommand;
83 using khtml::EditorContext;
84 using khtml::EditCommand;
85 using khtml::RenderStyleDeclarationImpl;
86 using khtml::TypingCommand;
88 // ==========================================================================
90 Editor::Editor(KHTMLPart *part)
91 : d(new EditorPrivate), m_typingStyle(0), m_part(part) {
94 Editor::~Editor() {
95 if (m_typingStyle)
96 m_typingStyle->deref();
97 delete d;
100 bool Editor::execCommand(const DOMString &command, bool userInterface, const DOMString &value)
102 PREPARE_JSEDITOR_CALL(command, false);
103 return js->execCommand(imp, userInterface, value);
106 bool Editor::queryCommandEnabled(const DOMString &command)
108 PREPARE_JSEDITOR_CALL(command, false);
109 return js->queryCommandEnabled(imp);
112 bool Editor::queryCommandIndeterm(const DOMString &command)
114 PREPARE_JSEDITOR_CALL(command, false);
115 return js->queryCommandIndeterm(imp);
118 bool Editor::queryCommandState(const DOMString &command)
120 PREPARE_JSEDITOR_CALL(command, false);
121 return js->queryCommandState(imp);
124 bool Editor::queryCommandSupported(const DOMString &command)
126 PREPARE_JSEDITOR_CALL(command, false);
127 return js->queryCommandSupported(imp);
130 DOMString Editor::queryCommandValue(const DOMString &command)
132 PREPARE_JSEDITOR_CALL(command, DOMString());
133 return js->queryCommandValue(imp);
136 bool Editor::execCommand(EditorCommand command, bool userInterface, const DOMString &value)
138 PREPARE_JSEDITOR_CALL(command, false);
139 return js->execCommand(imp, userInterface, value);
142 bool Editor::queryCommandEnabled(EditorCommand command)
144 PREPARE_JSEDITOR_CALL(command, false);
145 return js->queryCommandEnabled(imp);
148 bool Editor::queryCommandIndeterm(EditorCommand command)
150 PREPARE_JSEDITOR_CALL(command, false);
151 return js->queryCommandIndeterm(imp);
154 bool Editor::queryCommandState(EditorCommand command)
156 PREPARE_JSEDITOR_CALL(command, false);
157 return js->queryCommandState(imp);
160 bool Editor::queryCommandSupported(EditorCommand command)
162 PREPARE_JSEDITOR_CALL(command, false);
163 return js->queryCommandSupported(imp);
166 DOMString Editor::queryCommandValue(EditorCommand command)
168 PREPARE_JSEDITOR_CALL(command, DOMString());
169 return js->queryCommandValue(imp);
172 void Editor::copy()
174 static_cast<KHTMLPartBrowserExtension*>(m_part->browserExtension())->copy();
177 void Editor::cut()
179 // ###
180 static_cast<KHTMLPartBrowserExtension*>(m_part->browserExtension())->cut();
183 void Editor::paste()
185 // ###
186 // security?
187 // static_cast<KHTMLPartBrowserExtension*>(m_part->browserExtension())->paste();
190 void Editor::print()
192 static_cast<KHTMLPartBrowserExtension*>(m_part->browserExtension())->print();
195 bool Editor::canPaste() const
197 // ###
198 return false;
201 void Editor::redo()
203 if (d->m_redo.isEmpty())
204 return;
205 EditCommand e = d->m_redo.pop();
206 e.reapply();
209 void Editor::undo()
211 if (d->m_undo.isEmpty())
212 return;
213 EditCommand e = d->m_undo.pop();
214 e.unapply();
217 bool Editor::canRedo() const
219 return !d->m_redo.isEmpty();
222 bool Editor::canUndo() const
224 return !d->m_undo.isEmpty();
227 void Editor::applyStyle(CSSStyleDeclarationImpl *style)
229 switch (m_part->caret().state()) {
230 case Selection::NONE:
231 // do nothing
232 break;
233 case Selection::CARET:
234 // FIXME: This blows away all the other properties of the typing style.
235 setTypingStyle(style);
236 break;
237 case Selection::RANGE:
238 if (m_part->xmlDocImpl() && style) {
239 ApplyStyleCommand cmd(m_part->xmlDocImpl(), style);
240 cmd.apply();
242 break;
246 static void updateState(CSSStyleDeclarationImpl *desiredStyle, CSSStyleDeclarationImpl *computedStyle, bool &atStart, Editor::TriState &state)
248 QListIterator<CSSProperty*> it(*desiredStyle->values());
249 while (it.hasNext()) {
250 int propertyID = it.next()->id();
251 DOMString desiredProperty = desiredStyle->getPropertyValue(propertyID);
252 DOMString computedProperty = computedStyle->getPropertyValue(propertyID);
253 Editor::TriState propertyState = strcasecmp(desiredProperty, computedProperty) == 0
254 ? Editor::TrueTriState : Editor::FalseTriState;
255 if (atStart) {
256 state = propertyState;
257 atStart = false;
258 } else if (state != propertyState) {
259 state = Editor::MixedTriState;
260 break;
265 Editor::TriState Editor::selectionHasStyle(CSSStyleDeclarationImpl *style) const
267 bool atStart = true;
268 TriState state = FalseTriState;
270 EditorContext *ctx = m_part->editorContext();
271 if (ctx->m_selection.state() != Selection::RANGE) {
272 NodeImpl *nodeToRemove;
273 CSSStyleDeclarationImpl *selectionStyle = selectionComputedStyle(nodeToRemove);
274 if (!selectionStyle)
275 return FalseTriState;
276 selectionStyle->ref();
277 updateState(style, selectionStyle, atStart, state);
278 selectionStyle->deref();
279 if (nodeToRemove) {
280 int exceptionCode = 0;
281 nodeToRemove->remove(exceptionCode);
282 assert(exceptionCode == 0);
284 } else {
285 for (NodeImpl *node = ctx->m_selection.start().node(); node; node = node->traverseNextNode()) {
286 if (node->isHTMLElement()) {
287 CSSStyleDeclarationImpl *computedStyle = new RenderStyleDeclarationImpl(node);
288 computedStyle->ref();
289 updateState(style, computedStyle, atStart, state);
290 computedStyle->deref();
291 if (state == MixedTriState)
292 break;
294 if (node == ctx->m_selection.end().node())
295 break;
299 return state;
302 bool Editor::selectionStartHasStyle(CSSStyleDeclarationImpl *style) const
304 NodeImpl *nodeToRemove;
305 CSSStyleDeclarationImpl *selectionStyle = selectionComputedStyle(nodeToRemove);
306 if (!selectionStyle)
307 return false;
309 selectionStyle->ref();
311 bool match = true;
313 QListIterator<CSSProperty*> it(*style->values());
314 while (it.hasNext()) {
315 int propertyID = it.next()->id();
316 DOMString desiredProperty = style->getPropertyValue(propertyID);
317 DOMString selectionProperty = selectionStyle->getPropertyValue(propertyID);
318 if (strcasecmp(selectionProperty, desiredProperty) != 0) {
319 match = false;
320 break;
324 selectionStyle->deref();
326 if (nodeToRemove) {
327 int exceptionCode = 0;
328 nodeToRemove->remove(exceptionCode);
329 assert(exceptionCode == 0);
332 return match;
335 DOMString Editor::selectionStartStylePropertyValue(int stylePropertyID) const
337 NodeImpl *nodeToRemove;
338 CSSStyleDeclarationImpl *selectionStyle = selectionComputedStyle(nodeToRemove);
339 if (!selectionStyle)
340 return DOMString();
342 selectionStyle->ref();
343 DOMString value = selectionStyle->getPropertyValue(stylePropertyID);
344 selectionStyle->deref();
346 if (nodeToRemove) {
347 int exceptionCode = 0;
348 nodeToRemove->remove(exceptionCode);
349 assert(exceptionCode == 0);
352 return value;
355 CSSStyleDeclarationImpl *Editor::selectionComputedStyle(NodeImpl *&nodeToRemove) const
357 nodeToRemove = 0;
359 if (!m_part->xmlDocImpl())
360 return 0;
362 EditorContext *ctx = m_part->editorContext();
363 if (ctx->m_selection.state() == Selection::NONE)
364 return 0;
366 Range range(ctx->m_selection.toRange());
367 Position pos(range.startContainer().handle(), range.startOffset());
368 assert(pos.notEmpty());
369 ElementImpl *elem = pos.element();
370 ElementImpl *styleElement = elem;
371 int exceptionCode = 0;
373 if (m_typingStyle) {
374 styleElement = m_part->xmlDocImpl()->createHTMLElement("SPAN");
375 // assert(exceptionCode == 0);
377 styleElement->setAttribute(ATTR_STYLE, m_typingStyle->cssText().implementation());
378 // assert(exceptionCode == 0);
380 TextImpl *text = m_part->xmlDocImpl()->createEditingTextNode("");
381 styleElement->appendChild(text, exceptionCode);
382 assert(exceptionCode == 0);
384 elem->appendChild(styleElement, exceptionCode);
385 assert(exceptionCode == 0);
387 nodeToRemove = styleElement;
390 return new RenderStyleDeclarationImpl(styleElement);
393 EditCommand Editor::lastEditCommand() const
395 return d->m_lastEditCommand;
398 void Editor::appliedEditing(EditCommand &cmd)
400 // make sure we have all the changes in rendering tree applied with relayout if needed before setting caret
401 // in particular that could be required for inline boxes recomputation when inserting text
402 m_part->xmlDocImpl()->updateLayout();
404 m_part->setCaret(cmd.endingSelection(), false);
405 // Command will be equal to last edit command only in the case of typing
406 if (d->m_lastEditCommand == cmd) {
407 assert(cmd.commandID() == khtml::TypingCommandID);
409 else {
410 // Only register a new undo command if the command passed in is
411 // different from the last command
412 d->registerUndo( cmd );
413 d->m_lastEditCommand = cmd;
415 m_part->selectionLayoutChanged();
416 // ### only emit if caret pos changed
417 m_part->emitCaretPositionChanged(cmd.endingSelection().caretPos());
420 void Editor::unappliedEditing(EditCommand &cmd)
422 // see comment in appliedEditing()
423 m_part->xmlDocImpl()->updateLayout();
425 m_part->setCaret(cmd.startingSelection());
426 d->registerRedo( cmd );
427 #ifdef APPLE_CHANGES
428 KWQ(this)->respondToChangedContents();
429 #else
430 m_part->selectionLayoutChanged();
431 // ### only emit if caret pos changed
432 m_part->emitCaretPositionChanged(cmd.startingSelection().caretPos());
433 #endif
434 d->m_lastEditCommand = EditCommand::emptyCommand();
437 void Editor::reappliedEditing(EditCommand &cmd)
439 // see comment in appliedEditing()
440 m_part->xmlDocImpl()->updateLayout();
442 m_part->setCaret(cmd.endingSelection());
443 d->registerUndo( cmd, false /*clearRedoStack*/ );
444 #ifdef APPLE_CHANGES
445 KWQ(this)->respondToChangedContents();
446 #else
447 m_part->selectionLayoutChanged();
448 // ### only emit if caret pos changed
449 m_part->emitCaretPositionChanged(cmd.endingSelection().caretPos());
450 #endif
451 d->m_lastEditCommand = EditCommand::emptyCommand();
454 CSSStyleDeclarationImpl *Editor::typingStyle() const
456 return m_typingStyle;
459 void Editor::setTypingStyle(CSSStyleDeclarationImpl *style)
461 CSSStyleDeclarationImpl *old = m_typingStyle;
462 m_typingStyle = style;
463 if (m_typingStyle)
464 m_typingStyle->ref();
465 if (old)
466 old->deref();
469 void Editor::clearTypingStyle()
471 setTypingStyle(0);
474 bool Editor::handleKeyEvent(QKeyEvent *_ke)
476 bool handled = false;
478 bool ctrl = _ke->modifiers() & Qt::ControlModifier;
479 bool alt = _ke->modifiers() & Qt::AltModifier;
480 bool shift = _ke->modifiers() & Qt::ShiftModifier;
481 bool meta = _ke->modifiers() & Qt::MetaModifier;
483 if (ctrl || alt || meta) {
484 return false;
487 switch(_ke->key()) {
489 case Qt::Key_Delete: {
490 Selection selectionToDelete = m_part->caret();
491 kDebug(6200) << "========== KEY_DELETE ==========" << endl;
492 if (selectionToDelete.state() == Selection::CARET) {
493 Position pos(selectionToDelete.start());
494 kDebug(6200) << "pos.inLastEditableInRootEditableElement " << pos.inLastEditableInRootEditableElement() << " pos.offset " << pos.offset() << " pos.max " << pos.node()->caretMaxRenderedOffset() << endl;
495 if (pos.nextCharacterPosition() == pos) {
496 // we're at the end of a root editable block...do nothing
497 kDebug(6200) << "no delete!!!!!!!!!!" << endl;
498 break;
500 m_part->d->editor_context.m_selection
501 = Selection(pos, pos.nextCharacterPosition());
503 // fall through
505 case Qt::Key_Backspace:
506 TypingCommand::deleteKeyPressed(m_part->xmlDocImpl());
507 handled = true;
508 break;
510 case Qt::Key_Return:
511 case Qt::Key_Enter:
512 // if (shift)
513 TypingCommand::insertNewline(m_part->xmlDocImpl());
514 // else
515 // TypingCommand::insertParagraph(m_part->xmlDocImpl());
516 handled = true;
517 break;
519 case Qt::Key_Escape:
520 case Qt::Key_Insert:
521 // FIXME implement me
522 handled = true;
523 break;
525 default:
526 // handle_input:
527 if (!_ke->text().isEmpty()) {
528 TypingCommand::insertText(m_part->xmlDocImpl(), _ke->text());
529 handled = true;
534 //if (handled) {
535 // ### check when to emit it
536 // m_part->emitSelectionChanged();
539 return handled;
544 #include "editor.moc"