File indexing completed on 2024-04-28 15:23:21

0001 /* This file is part of the KDE project
0002  *
0003  * Copyright (C) 2004 Leo Savernik <l.savernik@aon.at>
0004  *
0005  * This library is free software; you can redistribute it and/or
0006  * modify it under the terms of the GNU Library General Public
0007  * License as published by the Free Software Foundation; either
0008  * version 2 of the License, or (at your option) any later version.
0009  *
0010  * This library is distributed in the hope that it will be useful,
0011  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0012  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0013  * Library General Public License for more details.
0014  *
0015  * You should have received a copy of the GNU Library General Public License
0016  * along with this library; see the file COPYING.LIB.  If not, write to
0017  * the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
0018  * Boston, MA 02111-1307, USA.
0019  */
0020 
0021 #include "editor.h"
0022 
0023 #include "jsediting.h"
0024 #include "htmlediting_impl.h"
0025 
0026 #include "css/css_renderstyledeclarationimpl.h"
0027 #include "css/css_valueimpl.h"
0028 #include "xml/dom_selection.h"
0029 #include "xml/dom_docimpl.h"
0030 #include "xml/dom_elementimpl.h"
0031 #include "xml/dom_textimpl.h"
0032 #include "xml/dom2_rangeimpl.h"
0033 #include "khtml_part.h"
0034 #include "khtml_ext.h"
0035 #include "khtmlpart_p.h"
0036 
0037 #include <QStack>
0038 #include <QKeyEvent>
0039 
0040 #ifndef APPLE_CHANGES
0041 #  ifdef assert
0042 #    undef assert
0043 #  endif
0044 #  define assert(x) Q_ASSERT(x)
0045 #endif
0046 
0047 #define PREPARE_JSEDITOR_CALL(command, retval) \
0048     JSEditor *js = m_part->xmlDocImpl() ? m_part->xmlDocImpl()->jsEditor() : nullptr; \
0049     if (!js) return retval; \
0050     const CommandImp *imp = js->commandImp(command)
0051 
0052 #define DEBUG_COMMANDS
0053 
0054 using namespace WTF;
0055 
0056 using namespace DOM;
0057 
0058 using khtml::RenderStyleDeclarationImpl;
0059 using khtml::EditCommandImpl;
0060 using khtml::ApplyStyleCommandImpl;
0061 using khtml::TypingCommandImpl;
0062 using khtml::EditorContext;
0063 using khtml::IndentOutdentCommandImpl;
0064 
0065 // --------------------------------------------------------------------------
0066 
0067 namespace DOM
0068 {
0069 
0070 static const int sMaxUndoSteps = 1000;
0071 
0072 class EditorPrivate
0073 {
0074 public:
0075     void registerUndo(EditCommandImpl *cmd, bool clearRedoStack = true)
0076     {
0077         if (m_undo.count() >= sMaxUndoSteps) {
0078             m_undo.pop_front();
0079         }
0080         if (clearRedoStack) {
0081             m_redo.clear();
0082         }
0083         m_undo.push(cmd);
0084     }
0085     void registerRedo(EditCommandImpl *cmd)
0086     {
0087         if (m_redo.count() >= sMaxUndoSteps) {
0088             m_redo.pop_front();
0089         }
0090         m_redo.push(cmd);
0091     }
0092     RefPtr<EditCommandImpl> m_lastEditCommand;
0093     QStack<RefPtr<EditCommandImpl> > m_undo;
0094     QStack<RefPtr<EditCommandImpl> > m_redo;
0095 };
0096 
0097 }
0098 
0099 // ==========================================================================
0100 
0101 Editor::Editor(KHTMLPart *part)
0102     : d(new EditorPrivate), m_typingStyle(nullptr), m_part(part)
0103 {
0104 }
0105 
0106 Editor::~Editor()
0107 {
0108     if (m_typingStyle) {
0109         m_typingStyle->deref();
0110     }
0111     delete d;
0112 }
0113 
0114 bool Editor::execCommand(const DOMString &command, bool userInterface, const DOMString &value)
0115 {
0116     PREPARE_JSEDITOR_CALL(command, false);
0117     return js->execCommand(imp, userInterface, value);
0118 }
0119 
0120 bool Editor::queryCommandEnabled(const DOMString &command)
0121 {
0122     PREPARE_JSEDITOR_CALL(command, false);
0123     return js->queryCommandEnabled(imp);
0124 }
0125 
0126 bool Editor::queryCommandIndeterm(const DOMString &command)
0127 {
0128     PREPARE_JSEDITOR_CALL(command, false);
0129     return js->queryCommandIndeterm(imp);
0130 }
0131 
0132 bool Editor::queryCommandState(const DOMString &command)
0133 {
0134     PREPARE_JSEDITOR_CALL(command, false);
0135     return js->queryCommandState(imp);
0136 }
0137 
0138 bool Editor::queryCommandSupported(const DOMString &command)
0139 {
0140     PREPARE_JSEDITOR_CALL(command, false);
0141     return js->queryCommandSupported(imp);
0142 }
0143 
0144 DOMString Editor::queryCommandValue(const DOMString &command)
0145 {
0146     PREPARE_JSEDITOR_CALL(command, DOMString());
0147     return js->queryCommandValue(imp);
0148 }
0149 
0150 bool Editor::execCommand(EditorCommand command, bool userInterface, const DOMString &value)
0151 {
0152     PREPARE_JSEDITOR_CALL(command, false);
0153     return js->execCommand(imp, userInterface, value);
0154 }
0155 
0156 bool Editor::queryCommandEnabled(EditorCommand command)
0157 {
0158     PREPARE_JSEDITOR_CALL(command, false);
0159     return js->queryCommandEnabled(imp);
0160 }
0161 
0162 bool Editor::queryCommandIndeterm(EditorCommand command)
0163 {
0164     PREPARE_JSEDITOR_CALL(command, false);
0165     return js->queryCommandIndeterm(imp);
0166 }
0167 
0168 bool Editor::queryCommandState(EditorCommand command)
0169 {
0170     PREPARE_JSEDITOR_CALL(command, false);
0171     return js->queryCommandState(imp);
0172 }
0173 
0174 bool Editor::queryCommandSupported(EditorCommand command)
0175 {
0176     PREPARE_JSEDITOR_CALL(command, false);
0177     return js->queryCommandSupported(imp);
0178 }
0179 
0180 DOMString Editor::queryCommandValue(EditorCommand command)
0181 {
0182     PREPARE_JSEDITOR_CALL(command, DOMString());
0183     return js->queryCommandValue(imp);
0184 }
0185 
0186 void Editor::copy()
0187 {
0188     static_cast<KHTMLPartBrowserExtension *>(m_part->browserExtension())->copy();
0189 }
0190 
0191 void Editor::cut()
0192 {
0193     // ###
0194     static_cast<KHTMLPartBrowserExtension *>(m_part->browserExtension())->cut();
0195 }
0196 
0197 void Editor::paste()
0198 {
0199     // ###
0200     // security?
0201     // static_cast<KHTMLPartBrowserExtension*>(m_part->browserExtension())->paste();
0202 }
0203 
0204 void Editor::print()
0205 {
0206     static_cast<KHTMLPartBrowserExtension *>(m_part->browserExtension())->print();
0207 }
0208 
0209 bool Editor::canPaste() const
0210 {
0211     // ###
0212     return false;
0213 }
0214 
0215 void Editor::redo()
0216 {
0217     if (d->m_redo.isEmpty()) {
0218         return;
0219     }
0220     RefPtr<EditCommandImpl> e = d->m_redo.pop();
0221     e->reapply();
0222 }
0223 
0224 void Editor::undo()
0225 {
0226     if (d->m_undo.isEmpty()) {
0227         return;
0228     }
0229     RefPtr<EditCommandImpl> e = d->m_undo.pop();
0230     e->unapply();
0231 }
0232 
0233 bool Editor::canRedo() const
0234 {
0235     return !d->m_redo.isEmpty();
0236 }
0237 
0238 bool Editor::canUndo() const
0239 {
0240     return !d->m_undo.isEmpty();
0241 }
0242 
0243 void Editor::applyStyle(CSSStyleDeclarationImpl *style)
0244 {
0245     switch (m_part->caret().state()) {
0246     case Selection::NONE:
0247         // do nothing
0248         break;
0249     case Selection::CARET:
0250         // FIXME: This blows away all the other properties of the typing style.
0251         setTypingStyle(style);
0252         break;
0253     case Selection::RANGE:
0254         if (m_part->xmlDocImpl() && style) {
0255 #ifdef DEBUG_COMMANDS
0256             // qCDebug(KHTML_LOG) << "[create ApplyStyleCommand]";
0257 #endif
0258             // FIXME
0259             (new ApplyStyleCommandImpl(m_part->xmlDocImpl(), style))->apply();
0260         }
0261         break;
0262     }
0263 }
0264 
0265 static void updateState(CSSStyleDeclarationImpl *desiredStyle, CSSStyleDeclarationImpl *computedStyle, bool &atStart, Editor::TriState &state)
0266 {
0267     QListIterator<CSSProperty *> it(*desiredStyle->values());
0268     while (it.hasNext()) {
0269         int propertyID = it.next()->id();
0270         DOMString desiredProperty = desiredStyle->getPropertyValue(propertyID);
0271         DOMString computedProperty = computedStyle->getPropertyValue(propertyID);
0272         Editor::TriState propertyState = strcasecmp(desiredProperty, computedProperty) == 0
0273                                          ? Editor::TrueTriState : Editor::FalseTriState;
0274         if (atStart) {
0275             state = propertyState;
0276             atStart = false;
0277         } else if (state != propertyState) {
0278             state = Editor::MixedTriState;
0279             break;
0280         }
0281     }
0282 }
0283 
0284 Editor::TriState Editor::selectionHasStyle(CSSStyleDeclarationImpl *style) const
0285 {
0286     bool atStart = true;
0287     TriState state = FalseTriState;
0288 
0289     EditorContext *ctx = m_part->editorContext();
0290     if (ctx->m_selection.state() != Selection::RANGE) {
0291         NodeImpl *nodeToRemove;
0292         CSSStyleDeclarationImpl *selectionStyle = selectionComputedStyle(nodeToRemove);
0293         if (!selectionStyle) {
0294             return FalseTriState;
0295         }
0296         selectionStyle->ref();
0297         updateState(style, selectionStyle, atStart, state);
0298         selectionStyle->deref();
0299         if (nodeToRemove) {
0300             int exceptionCode = 0;
0301             nodeToRemove->remove(exceptionCode);
0302             assert(exceptionCode == 0);
0303         }
0304     } else {
0305         for (NodeImpl *node = ctx->m_selection.start().node(); node; node = node->traverseNextNode()) {
0306             if (node->isHTMLElement()) {
0307                 CSSStyleDeclarationImpl *computedStyle = new RenderStyleDeclarationImpl(node);
0308                 computedStyle->ref();
0309                 updateState(style, computedStyle, atStart, state);
0310                 computedStyle->deref();
0311                 if (state == MixedTriState) {
0312                     break;
0313                 }
0314             }
0315             if (node == ctx->m_selection.end().node()) {
0316                 break;
0317             }
0318         }
0319     }
0320 
0321     return state;
0322 }
0323 
0324 bool Editor::selectionStartHasStyle(CSSStyleDeclarationImpl *style) const
0325 {
0326     NodeImpl *nodeToRemove;
0327     CSSStyleDeclarationImpl *selectionStyle = selectionComputedStyle(nodeToRemove);
0328     if (!selectionStyle) {
0329         return false;
0330     }
0331 
0332     selectionStyle->ref();
0333 
0334     bool match = true;
0335 
0336     QListIterator<CSSProperty *> it(*style->values());
0337     while (it.hasNext()) {
0338         int propertyID = it.next()->id();
0339         DOMString desiredProperty = style->getPropertyValue(propertyID);
0340         DOMString selectionProperty = selectionStyle->getPropertyValue(propertyID);
0341         if (strcasecmp(selectionProperty, desiredProperty) != 0) {
0342             match = false;
0343             break;
0344         }
0345     }
0346 
0347     selectionStyle->deref();
0348 
0349     if (nodeToRemove) {
0350         int exceptionCode = 0;
0351         nodeToRemove->remove(exceptionCode);
0352         assert(exceptionCode == 0);
0353     }
0354 
0355     return match;
0356 }
0357 
0358 DOMString Editor::selectionStartStylePropertyValue(int stylePropertyID) const
0359 {
0360     NodeImpl *nodeToRemove;
0361     CSSStyleDeclarationImpl *selectionStyle = selectionComputedStyle(nodeToRemove);
0362     if (!selectionStyle) {
0363         return DOMString();
0364     }
0365 
0366     selectionStyle->ref();
0367     DOMString value = selectionStyle->getPropertyValue(stylePropertyID);
0368     selectionStyle->deref();
0369 
0370     if (nodeToRemove) {
0371         int exceptionCode = 0;
0372         nodeToRemove->remove(exceptionCode);
0373         assert(exceptionCode == 0);
0374     }
0375 
0376     return value;
0377 }
0378 
0379 CSSStyleDeclarationImpl *Editor::selectionComputedStyle(NodeImpl *&nodeToRemove) const
0380 {
0381     nodeToRemove = nullptr;
0382 
0383     if (!m_part->xmlDocImpl()) {
0384         return nullptr;
0385     }
0386 
0387     EditorContext *ctx = m_part->editorContext();
0388     if (ctx->m_selection.state() == Selection::NONE) {
0389         return nullptr;
0390     }
0391 
0392     Range range(ctx->m_selection.toRange());
0393     Position pos(range.startContainer().handle(), range.startOffset());
0394     assert(pos.notEmpty());
0395     ElementImpl *elem = pos.element();
0396     ElementImpl *styleElement = elem;
0397     int exceptionCode = 0;
0398 
0399     if (m_typingStyle) {
0400         styleElement = m_part->xmlDocImpl()->createHTMLElement("SPAN");
0401 //     assert(exceptionCode == 0);
0402 
0403         styleElement->setAttribute(ATTR_STYLE, m_typingStyle->cssText().implementation());
0404 //     assert(exceptionCode == 0);
0405 
0406         TextImpl *text = m_part->xmlDocImpl()->createEditingTextNode("");
0407         styleElement->appendChild(text, exceptionCode);
0408         assert(exceptionCode == 0);
0409 
0410         elem->appendChild(styleElement, exceptionCode);
0411         assert(exceptionCode == 0);
0412 
0413         nodeToRemove = styleElement;
0414     }
0415 
0416     return new RenderStyleDeclarationImpl(styleElement);
0417 }
0418 
0419 PassRefPtr<EditCommandImpl> Editor::lastEditCommand() const
0420 {
0421     return d->m_lastEditCommand;
0422 }
0423 
0424 void Editor::appliedEditing(EditCommandImpl *cmd)
0425 {
0426 #ifdef DEBUG_COMMANDS
0427     // qCDebug(KHTML_LOG) << "[Applied editing]";
0428 #endif
0429     // make sure we have all the changes in rendering tree applied with relayout if needed before setting caret
0430     // in particular that could be required for inline boxes recomputation when inserting text
0431     m_part->xmlDocImpl()->updateLayout();
0432 
0433     m_part->setCaret(cmd->endingSelection(), false);
0434     // Command will be equal to last edit command only in the case of typing
0435     if (d->m_lastEditCommand == cmd) {
0436         assert(cmd->isTypingCommand());
0437     } else {
0438         // Only register a new undo command if the command passed in is
0439         // different from the last command
0440         d->registerUndo(cmd);
0441         d->m_lastEditCommand = cmd;
0442     }
0443     m_part->editorContext()->m_selection.setNeedsLayout(true);
0444     m_part->selectionLayoutChanged();
0445     // ### only emit if caret pos changed
0446     m_part->emitCaretPositionChanged(cmd->endingSelection().caretPos());
0447 }
0448 
0449 void Editor::unappliedEditing(EditCommandImpl *cmd)
0450 {
0451     // see comment in appliedEditing()
0452     m_part->xmlDocImpl()->updateLayout();
0453 
0454     m_part->setCaret(cmd->startingSelection());
0455     d->registerRedo(cmd);
0456 #ifdef APPLE_CHANGES
0457     KWQ(this)->respondToChangedContents();
0458 #else
0459     m_part->editorContext()->m_selection.setNeedsLayout(true);
0460     m_part->selectionLayoutChanged();
0461     // ### only emit if caret pos changed
0462     m_part->emitCaretPositionChanged(cmd->startingSelection().caretPos());
0463 #endif
0464     d->m_lastEditCommand = nullptr;
0465 }
0466 
0467 void Editor::reappliedEditing(EditCommandImpl *cmd)
0468 {
0469     // see comment in appliedEditing()
0470     m_part->xmlDocImpl()->updateLayout();
0471 
0472     m_part->setCaret(cmd->endingSelection());
0473     d->registerUndo(cmd, false /*clearRedoStack*/);
0474 #ifdef APPLE_CHANGES
0475     KWQ(this)->respondToChangedContents();
0476 #else
0477     m_part->selectionLayoutChanged();
0478     // ### only emit if caret pos changed
0479     m_part->emitCaretPositionChanged(cmd->endingSelection().caretPos());
0480 #endif
0481     d->m_lastEditCommand = nullptr;
0482 }
0483 
0484 CSSStyleDeclarationImpl *Editor::typingStyle() const
0485 {
0486     return m_typingStyle;
0487 }
0488 
0489 void Editor::setTypingStyle(CSSStyleDeclarationImpl *style)
0490 {
0491     CSSStyleDeclarationImpl *old = m_typingStyle;
0492     m_typingStyle = style;
0493     if (m_typingStyle) {
0494         m_typingStyle->ref();
0495     }
0496     if (old) {
0497         old->deref();
0498     }
0499 }
0500 
0501 void Editor::clearTypingStyle()
0502 {
0503     setTypingStyle(nullptr);
0504 }
0505 
0506 void Editor::closeTyping()
0507 {
0508     EditCommandImpl *lastCommand = lastEditCommand().get();
0509     if (lastCommand && lastCommand->isTypingCommand()) {
0510         static_cast<TypingCommandImpl *>(lastCommand)->closeTyping();
0511     }
0512 }
0513 
0514 void Editor::indent()
0515 {
0516     RefPtr<IndentOutdentCommandImpl> command = new IndentOutdentCommandImpl(m_part->xmlDocImpl(),
0517             IndentOutdentCommandImpl::Indent);
0518     command->apply();
0519 }
0520 
0521 void Editor::outdent()
0522 {
0523     RefPtr<IndentOutdentCommandImpl> command = new IndentOutdentCommandImpl(m_part->xmlDocImpl(),
0524             IndentOutdentCommandImpl::Outdent);
0525     command->apply();
0526 }
0527 
0528 bool Editor::handleKeyEvent(QKeyEvent *_ke)
0529 {
0530     bool handled = false;
0531 
0532     bool ctrl  = _ke->modifiers() & Qt::ControlModifier;
0533     bool alt   = _ke->modifiers() & Qt::AltModifier;
0534     //bool shift = _ke->modifiers() & Qt::ShiftModifier;
0535     bool meta  = _ke->modifiers() & Qt::MetaModifier;
0536 
0537     if (ctrl || alt || meta) {
0538         return false;
0539     }
0540 
0541     switch (_ke->key()) {
0542 
0543     case Qt::Key_Delete: {
0544         Selection selectionToDelete = m_part->caret();
0545 #ifdef DEBUG_COMMANDS
0546         // qCDebug(KHTML_LOG) << "========== KEY_DELETE ==========";
0547 #endif
0548         if (selectionToDelete.state() == Selection::CARET) {
0549             Position pos(selectionToDelete.start());
0550 #ifdef DEBUG_COMMANDS
0551             // qCDebug(KHTML_LOG) << "pos.inLastEditableInRootEditableElement " << pos.inLastEditableInRootEditableElement() << " pos.offset " << pos.offset() << " pos.max " << pos.node()->caretMaxRenderedOffset();
0552 #endif
0553             if (pos.nextCharacterPosition() == pos) {
0554                 // we're at the end of a root editable block...do nothing
0555 #ifdef DEBUG_COMMANDS
0556                 // qCDebug(KHTML_LOG) << "no delete!!!!!!!!!!";
0557 #endif
0558                 break;
0559             }
0560             m_part->d->editor_context.m_selection
0561                 = Selection(pos, pos.nextCharacterPosition());
0562         }
0563         // fall through
0564     }
0565     case Qt::Key_Backspace:
0566         TypingCommandImpl::deleteKeyPressed0(m_part->xmlDocImpl());
0567         handled = true;
0568         break;
0569 
0570     case Qt::Key_Return:
0571     case Qt::Key_Enter:
0572 //       if (shift)
0573         TypingCommandImpl::insertNewline0(m_part->xmlDocImpl());
0574 //       else
0575 //         TypingCommand::insertParagraph(m_part->xmlDocImpl());
0576         handled = true;
0577         break;
0578 
0579     case Qt::Key_Escape:
0580     case Qt::Key_Insert:
0581         // FIXME implement me
0582         handled = true;
0583         break;
0584 
0585     default:
0586 // handle_input:
0587         if (m_part->caret().state() != Selection::CARET) {
0588             // We didn't get a chance to grab the caret, likely because
0589             // a script messed with contentEditable in the middle of events
0590             // acquire it now if there isn't a selection
0591             // qCDebug(KHTML_LOG) << "Editable node w/o caret!";
0592             DOM::NodeImpl *focus = m_part->xmlDocImpl()->focusNode();
0593             if (m_part->caret().state() == Selection::NONE) {
0594                 if (focus) {
0595                     m_part->setCaret(Position(focus, focus->caretMinOffset()));
0596                 } else {
0597                     break;
0598                 }
0599             }
0600         }
0601 
0602         if (!_ke->text().isEmpty()) {
0603             TypingCommandImpl::insertText0(m_part->xmlDocImpl(), _ke->text());
0604             handled = true;
0605         }
0606 
0607     }
0608 
0609     //if (handled) {
0610     // ### check when to emit it
0611 //     m_part->emitSelectionChanged();
0612     //}
0613 
0614     return handled;
0615 
0616 }
0617 
0618 #include "moc_editor.cpp"