File indexing completed on 2024-04-28 11:38:07
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"