/* This file is part of the KDE project * * Copyright (C) 2004 Leo Savernik * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ #include "editor.h" #include "jsediting.h" #include "htmlediting_impl.h" #include "css/css_renderstyledeclarationimpl.h" #include "css/css_valueimpl.h" #include "xml/dom_selection.h" #include "xml/dom_docimpl.h" #include "xml/dom_elementimpl.h" #include "xml/dom_textimpl.h" #include "xml/dom2_rangeimpl.h" #include "khtml_part.h" #include "khtml_ext.h" #include "khtmlpart_p.h" #include #ifndef APPLE_CHANGES # ifdef assert # undef assert # endif # define assert(x) Q_ASSERT(x) #endif #define PREPARE_JSEDITOR_CALL(command, retval) \ JSEditor *js = m_part->xmlDocImpl() ? m_part->xmlDocImpl()->jsEditor() : 0; \ if (!js) return retval; \ const CommandImp *imp = js->commandImp(command) #define DEBUG_COMMANDS using namespace WTF; using namespace DOM; using khtml::RenderStyleDeclarationImpl; using khtml::EditCommandImpl; using khtml::ApplyStyleCommandImpl; using khtml::TypingCommandImpl; using khtml::EditorContext; using khtml::IndentOutdentCommandImpl; // -------------------------------------------------------------------------- namespace DOM { static const int sMaxUndoSteps = 1000; class EditorPrivate { public: void registerUndo(EditCommandImpl *cmd, bool clearRedoStack = true) { if (m_undo.count()>= sMaxUndoSteps) m_undo.pop_front(); if (clearRedoStack) m_redo.clear(); m_undo.push(cmd); } void registerRedo(EditCommandImpl *cmd) { if (m_redo.count()>= sMaxUndoSteps) m_redo.pop_front(); m_redo.push(cmd); } RefPtr m_lastEditCommand; QStack > m_undo; QStack > m_redo; }; } // ========================================================================== Editor::Editor(KHTMLPart *part) : d(new EditorPrivate), m_typingStyle(0), m_part(part) { } Editor::~Editor() { if (m_typingStyle) m_typingStyle->deref(); delete d; } bool Editor::execCommand(const DOMString &command, bool userInterface, const DOMString &value) { PREPARE_JSEDITOR_CALL(command, false); return js->execCommand(imp, userInterface, value); } bool Editor::queryCommandEnabled(const DOMString &command) { PREPARE_JSEDITOR_CALL(command, false); return js->queryCommandEnabled(imp); } bool Editor::queryCommandIndeterm(const DOMString &command) { PREPARE_JSEDITOR_CALL(command, false); return js->queryCommandIndeterm(imp); } bool Editor::queryCommandState(const DOMString &command) { PREPARE_JSEDITOR_CALL(command, false); return js->queryCommandState(imp); } bool Editor::queryCommandSupported(const DOMString &command) { PREPARE_JSEDITOR_CALL(command, false); return js->queryCommandSupported(imp); } DOMString Editor::queryCommandValue(const DOMString &command) { PREPARE_JSEDITOR_CALL(command, DOMString()); return js->queryCommandValue(imp); } bool Editor::execCommand(EditorCommand command, bool userInterface, const DOMString &value) { PREPARE_JSEDITOR_CALL(command, false); return js->execCommand(imp, userInterface, value); } bool Editor::queryCommandEnabled(EditorCommand command) { PREPARE_JSEDITOR_CALL(command, false); return js->queryCommandEnabled(imp); } bool Editor::queryCommandIndeterm(EditorCommand command) { PREPARE_JSEDITOR_CALL(command, false); return js->queryCommandIndeterm(imp); } bool Editor::queryCommandState(EditorCommand command) { PREPARE_JSEDITOR_CALL(command, false); return js->queryCommandState(imp); } bool Editor::queryCommandSupported(EditorCommand command) { PREPARE_JSEDITOR_CALL(command, false); return js->queryCommandSupported(imp); } DOMString Editor::queryCommandValue(EditorCommand command) { PREPARE_JSEDITOR_CALL(command, DOMString()); return js->queryCommandValue(imp); } void Editor::copy() { static_cast(m_part->browserExtension())->copy(); } void Editor::cut() { // ### static_cast(m_part->browserExtension())->cut(); } void Editor::paste() { // ### // security? // static_cast(m_part->browserExtension())->paste(); } void Editor::print() { static_cast(m_part->browserExtension())->print(); } bool Editor::canPaste() const { // ### return false; } void Editor::redo() { if (d->m_redo.isEmpty()) return; RefPtr e = d->m_redo.pop(); e->reapply(); } void Editor::undo() { if (d->m_undo.isEmpty()) return; RefPtr e = d->m_undo.pop(); e->unapply(); } bool Editor::canRedo() const { return !d->m_redo.isEmpty(); } bool Editor::canUndo() const { return !d->m_undo.isEmpty(); } void Editor::applyStyle(CSSStyleDeclarationImpl *style) { switch (m_part->caret().state()) { case Selection::NONE: // do nothing break; case Selection::CARET: // FIXME: This blows away all the other properties of the typing style. setTypingStyle(style); break; case Selection::RANGE: if (m_part->xmlDocImpl() && style) { #ifdef DEBUG_COMMANDS kDebug() << "[create ApplyStyleCommand]" << endl; #endif // FIXME (new ApplyStyleCommandImpl(m_part->xmlDocImpl(), style))->apply(); } break; } } static void updateState(CSSStyleDeclarationImpl *desiredStyle, CSSStyleDeclarationImpl *computedStyle, bool &atStart, Editor::TriState &state) { QListIterator it(*desiredStyle->values()); while (it.hasNext()) { int propertyID = it.next()->id(); DOMString desiredProperty = desiredStyle->getPropertyValue(propertyID); DOMString computedProperty = computedStyle->getPropertyValue(propertyID); Editor::TriState propertyState = strcasecmp(desiredProperty, computedProperty) == 0 ? Editor::TrueTriState : Editor::FalseTriState; if (atStart) { state = propertyState; atStart = false; } else if (state != propertyState) { state = Editor::MixedTriState; break; } } } Editor::TriState Editor::selectionHasStyle(CSSStyleDeclarationImpl *style) const { bool atStart = true; TriState state = FalseTriState; EditorContext *ctx = m_part->editorContext(); if (ctx->m_selection.state() != Selection::RANGE) { NodeImpl *nodeToRemove; CSSStyleDeclarationImpl *selectionStyle = selectionComputedStyle(nodeToRemove); if (!selectionStyle) return FalseTriState; selectionStyle->ref(); updateState(style, selectionStyle, atStart, state); selectionStyle->deref(); if (nodeToRemove) { int exceptionCode = 0; nodeToRemove->remove(exceptionCode); assert(exceptionCode == 0); } } else { for (NodeImpl *node = ctx->m_selection.start().node(); node; node = node->traverseNextNode()) { if (node->isHTMLElement()) { CSSStyleDeclarationImpl *computedStyle = new RenderStyleDeclarationImpl(node); computedStyle->ref(); updateState(style, computedStyle, atStart, state); computedStyle->deref(); if (state == MixedTriState) break; } if (node == ctx->m_selection.end().node()) break; } } return state; } bool Editor::selectionStartHasStyle(CSSStyleDeclarationImpl *style) const { NodeImpl *nodeToRemove; CSSStyleDeclarationImpl *selectionStyle = selectionComputedStyle(nodeToRemove); if (!selectionStyle) return false; selectionStyle->ref(); bool match = true; QListIterator it(*style->values()); while (it.hasNext()) { int propertyID = it.next()->id(); DOMString desiredProperty = style->getPropertyValue(propertyID); DOMString selectionProperty = selectionStyle->getPropertyValue(propertyID); if (strcasecmp(selectionProperty, desiredProperty) != 0) { match = false; break; } } selectionStyle->deref(); if (nodeToRemove) { int exceptionCode = 0; nodeToRemove->remove(exceptionCode); assert(exceptionCode == 0); } return match; } DOMString Editor::selectionStartStylePropertyValue(int stylePropertyID) const { NodeImpl *nodeToRemove; CSSStyleDeclarationImpl *selectionStyle = selectionComputedStyle(nodeToRemove); if (!selectionStyle) return DOMString(); selectionStyle->ref(); DOMString value = selectionStyle->getPropertyValue(stylePropertyID); selectionStyle->deref(); if (nodeToRemove) { int exceptionCode = 0; nodeToRemove->remove(exceptionCode); assert(exceptionCode == 0); } return value; } CSSStyleDeclarationImpl *Editor::selectionComputedStyle(NodeImpl *&nodeToRemove) const { nodeToRemove = 0; if (!m_part->xmlDocImpl()) return 0; EditorContext *ctx = m_part->editorContext(); if (ctx->m_selection.state() == Selection::NONE) return 0; Range range(ctx->m_selection.toRange()); Position pos(range.startContainer().handle(), range.startOffset()); assert(pos.notEmpty()); ElementImpl *elem = pos.element(); ElementImpl *styleElement = elem; int exceptionCode = 0; if (m_typingStyle) { styleElement = m_part->xmlDocImpl()->createHTMLElement("SPAN"); // assert(exceptionCode == 0); styleElement->setAttribute(ATTR_STYLE, m_typingStyle->cssText().implementation()); // assert(exceptionCode == 0); TextImpl *text = m_part->xmlDocImpl()->createEditingTextNode(""); styleElement->appendChild(text, exceptionCode); assert(exceptionCode == 0); elem->appendChild(styleElement, exceptionCode); assert(exceptionCode == 0); nodeToRemove = styleElement; } return new RenderStyleDeclarationImpl(styleElement); } PassRefPtr Editor::lastEditCommand() const { return d->m_lastEditCommand; } void Editor::appliedEditing(EditCommandImpl *cmd) { #ifdef DEBUG_COMMANDS kDebug() << "[Applied editing]" << endl; #endif // make sure we have all the changes in rendering tree applied with relayout if needed before setting caret // in particular that could be required for inline boxes recomputation when inserting text m_part->xmlDocImpl()->updateLayout(); m_part->setCaret(cmd->endingSelection(), false); // Command will be equal to last edit command only in the case of typing if (d->m_lastEditCommand == cmd) { assert(cmd->isTypingCommand()); } else { // Only register a new undo command if the command passed in is // different from the last command d->registerUndo(cmd); d->m_lastEditCommand = cmd; } m_part->editorContext()->m_selection.setNeedsLayout(true); m_part->selectionLayoutChanged(); // ### only emit if caret pos changed m_part->emitCaretPositionChanged(cmd->endingSelection().caretPos()); } void Editor::unappliedEditing(EditCommandImpl *cmd) { // see comment in appliedEditing() m_part->xmlDocImpl()->updateLayout(); m_part->setCaret(cmd->startingSelection()); d->registerRedo(cmd); #ifdef APPLE_CHANGES KWQ(this)->respondToChangedContents(); #else m_part->editorContext()->m_selection.setNeedsLayout(true); m_part->selectionLayoutChanged(); // ### only emit if caret pos changed m_part->emitCaretPositionChanged(cmd->startingSelection().caretPos()); #endif d->m_lastEditCommand = 0; } void Editor::reappliedEditing(EditCommandImpl *cmd) { // see comment in appliedEditing() m_part->xmlDocImpl()->updateLayout(); m_part->setCaret(cmd->endingSelection()); d->registerUndo(cmd, false /*clearRedoStack*/); #ifdef APPLE_CHANGES KWQ(this)->respondToChangedContents(); #else m_part->selectionLayoutChanged(); // ### only emit if caret pos changed m_part->emitCaretPositionChanged(cmd->endingSelection().caretPos()); #endif d->m_lastEditCommand = 0; } CSSStyleDeclarationImpl *Editor::typingStyle() const { return m_typingStyle; } void Editor::setTypingStyle(CSSStyleDeclarationImpl *style) { CSSStyleDeclarationImpl *old = m_typingStyle; m_typingStyle = style; if (m_typingStyle) m_typingStyle->ref(); if (old) old->deref(); } void Editor::clearTypingStyle() { setTypingStyle(0); } void Editor::closeTyping() { EditCommandImpl *lastCommand = lastEditCommand().get(); if (lastCommand && lastCommand->isTypingCommand()) static_cast(lastCommand)->closeTyping(); } void Editor::indent() { RefPtr command = new IndentOutdentCommandImpl(m_part->xmlDocImpl(), IndentOutdentCommandImpl::Indent); command->apply(); } void Editor::outdent() { RefPtr command = new IndentOutdentCommandImpl(m_part->xmlDocImpl(), IndentOutdentCommandImpl::Outdent); command->apply(); } bool Editor::handleKeyEvent(QKeyEvent *_ke) { bool handled = false; bool ctrl = _ke->modifiers() & Qt::ControlModifier; bool alt = _ke->modifiers() & Qt::AltModifier; //bool shift = _ke->modifiers() & Qt::ShiftModifier; bool meta = _ke->modifiers() & Qt::MetaModifier; if (ctrl || alt || meta) { return false; } switch(_ke->key()) { case Qt::Key_Delete: { Selection selectionToDelete = m_part->caret(); #ifdef DEBUG_COMMANDS kDebug(6200) << "========== KEY_DELETE ==========" << endl; #endif if (selectionToDelete.state() == Selection::CARET) { Position pos(selectionToDelete.start()); #ifdef DEBUG_COMMANDS kDebug(6200) << "pos.inLastEditableInRootEditableElement " << pos.inLastEditableInRootEditableElement() << " pos.offset " << pos.offset() << " pos.max " << pos.node()->caretMaxRenderedOffset() << endl; #endif if (pos.nextCharacterPosition() == pos) { // we're at the end of a root editable block...do nothing #ifdef DEBUG_COMMANDS kDebug(6200) << "no delete!!!!!!!!!!" << endl; #endif break; } m_part->d->editor_context.m_selection = Selection(pos, pos.nextCharacterPosition()); } // fall through } case Qt::Key_Backspace: TypingCommandImpl::deleteKeyPressed0(m_part->xmlDocImpl()); handled = true; break; case Qt::Key_Return: case Qt::Key_Enter: // if (shift) TypingCommandImpl::insertNewline0(m_part->xmlDocImpl()); // else // TypingCommand::insertParagraph(m_part->xmlDocImpl()); handled = true; break; case Qt::Key_Escape: case Qt::Key_Insert: // FIXME implement me handled = true; break; default: // handle_input: if (m_part->caret().state() != Selection::CARET) { // We didn't get a chance to grab the caret, likely because // a script messed with contentEditable in the middle of events // acquire it now if there isn't a selection kDebug(6200) << "Editable node w/o caret!"; DOM::NodeImpl* focus = m_part->xmlDocImpl()->focusNode(); if (m_part->caret().state() == Selection::NONE) { if (focus) m_part->setCaret(Position(focus, focus->caretMinOffset())); else break; } } if (!_ke->text().isEmpty()) { TypingCommandImpl::insertText0(m_part->xmlDocImpl(), _ke->text()); handled = true; } } //if (handled) { // ### check when to emit it // m_part->emitSelectionChanged(); //} return handled; } #include "moc_editor.cpp"