1 /* This file is part of the KDE project
2 * Copyright (C) 2009 Ganesh Paramasivam <ganesh@crystalfab.com>
3 * Copyright (C) 2009 Pierre Stirnweiss <pstirnweiss@googlemail.com>
4 * Copyright (C) 2010 Thomas Zander <zander@kde.org>
5 * Copyright (C) 2012 C. Boemann <cbo@boemann.dk>
6 * Copyright (C) 2014-2015 Denis Kuplyakov <dener.kup@gmail.com>
7 *
8 * This library is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU Library General Public
10 * License as published by the Free Software Foundation; either
11 * version 2 of the License, or (at your option) any later version.
12 *
13 * This library is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * Library General Public License for more details.
17 *
18 * You should have received a copy of the GNU Library General Public License
19 * along with this library; see the file COPYING.LIB. If not, write to
20 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 * Boston, MA 02110-1301, USA.*/
22
23 #include "DeleteCommand.h"
24
25 #include <klocalizedstring.h>
26
27 #include <KoList.h>
28 #include <KoTextEditor.h>
29 #include <KoTextEditor_p.h>
30 #include <KoTextDocument.h>
31 #include <KoInlineTextObjectManager.h>
32 #include <KoTextRangeManager.h>
33 #include <KoAnchorInlineObject.h>
34 #include <KoAnchorTextRange.h>
35 #include <KoAnnotation.h>
36 #include <KoSection.h>
37 #include <KoSectionUtils.h>
38 #include <KoSectionModel.h>
39 #include <KoSectionEnd.h>
40 #include <KoShapeController.h>
41
42 #include <algorithm>
43
operator <(const DeleteCommand::SectionDeleteInfo & other) const44 bool DeleteCommand::SectionDeleteInfo::operator<(const DeleteCommand::SectionDeleteInfo &other) const
45 {
46 // At first we remove sections that lays deeper in tree
47 // On one level we delete sections by descending order of their childIdx
48 // That is needed on undo, cuz we want it to be simply done by inserting
49 // sections back in reverse order of their deletion.
50 // Without childIdx compare it is possible that we will want to insert
51 // section on position 2 while the number of children is less than 2.
52
53 if (section->level() != other.section->level()) {
54 return section->level() > other.section->level();
55 }
56 return childIdx > other.childIdx;
57 }
58
DeleteCommand(DeleteMode mode,QTextDocument * document,KoShapeController * shapeController,KUndo2Command * parent)59 DeleteCommand::DeleteCommand(DeleteMode mode,
60 QTextDocument *document,
61 KoShapeController *shapeController,
62 KUndo2Command *parent)
63 : KoTextCommandBase (parent)
64 , m_document(document)
65 , m_shapeController(shapeController)
66 , m_first(true)
67 , m_mode(mode)
68 , m_mergePossible(true)
69 {
70 setText(kundo2_i18n("Delete"));
71 }
72
undo()73 void DeleteCommand::undo()
74 {
75 KoTextCommandBase::undo();
76 UndoRedoFinalizer finalizer(this); // Look at KoTextCommandBase documentation
77
78 // KoList
79 updateListChanges();
80
81 // KoTextRange
82 KoTextRangeManager *rangeManager = KoTextDocument(m_document).textRangeManager();
83 foreach (KoTextRange *range, m_rangesToRemove) {
84 rangeManager->insert(range);
85 }
86
87 // KoInlineObject
88 foreach (KoInlineObject *object, m_invalidInlineObjects) {
89 object->manager()->addInlineObject(object);
90 }
91
92 // KoSectionModel
93 insertSectionsToModel();
94 }
95
redo()96 void DeleteCommand::redo()
97 {
98 if (!m_first) {
99 KoTextCommandBase::redo();
100 UndoRedoFinalizer finalizer(this); // Look at KoTextCommandBase documentation
101
102 // KoTextRange
103 KoTextRangeManager *rangeManager = KoTextDocument(m_document).textRangeManager();
104 foreach (KoTextRange *range, m_rangesToRemove) {
105 rangeManager->remove(range);
106 }
107
108 // KoSectionModel
109 deleteSectionsFromModel();
110
111 // TODO: there is nothing for InlineObjects and Lists. Is it OK?
112 } else {
113 m_first = false;
114 if (m_document) {
115 KoTextEditor *textEditor = KoTextDocument(m_document).textEditor();
116 if (textEditor) {
117 textEditor->beginEditBlock();
118 doDelete();
119 textEditor->endEditBlock();
120 }
121 }
122 }
123 }
124
125 // Section handling algorithm:
126 // At first, we go though the all section starts and ends
127 // that are in selection, and delete all pairs, because
128 // they will be deleted.
129 // Then we have multiple cases: selection start split some block
130 // or don't split any block.
131 // In the first case all formatting info will be stored in the
132 // split block(it has startBlockNum number).
133 // In the second case it will be stored in the block pointed by the
134 // selection end(it has endBlockNum number).
135 // Also there is a trivial case, when whole selection is inside
136 // one block, in this case hasEntirelyInsideBlock will be false
137 // and we will do nothing.
138
139 class DeleteVisitor : public KoTextVisitor
140 {
141 public:
DeleteVisitor(KoTextEditor * editor,DeleteCommand * command)142 DeleteVisitor(KoTextEditor *editor, DeleteCommand *command)
143 : KoTextVisitor(editor)
144 , m_first(true)
145 , m_command(command)
146 , m_startBlockNum(-1)
147 , m_endBlockNum(-1)
148 , m_hasEntirelyInsideBlock(false)
149 {
150 }
151
visitBlock(QTextBlock & block,const QTextCursor & caret)152 void visitBlock(QTextBlock &block, const QTextCursor &caret) override
153 {
154 for (QTextBlock::iterator it = block.begin(); it != block.end(); ++it) {
155 QTextCursor fragmentSelection(caret);
156 fragmentSelection.setPosition(qMax(caret.selectionStart(), it.fragment().position()));
157 fragmentSelection.setPosition(
158 qMin(caret.selectionEnd(), it.fragment().position() + it.fragment().length()),
159 QTextCursor::KeepAnchor
160 );
161
162 if (fragmentSelection.anchor() >= fragmentSelection.position()) {
163 continue;
164 }
165
166 visitFragmentSelection(fragmentSelection);
167 }
168
169 // Section handling below
170 bool doesBeginInside = false;
171 bool doesEndInside = false;
172 if (block.position() >= caret.selectionStart()) { // Begin of the block is inside selection.
173 doesBeginInside = true;
174 QList<KoSection *> openList = KoSectionUtils::sectionStartings(block.blockFormat());
175 foreach (KoSection *sec, openList) {
176 m_curSectionDelimiters.push_back(SectionHandle(sec->name(), sec));
177 }
178 }
179
180 if (block.position() + block.length() <= caret.selectionEnd()) { // End of the block is inside selection.
181 doesEndInside = true;
182 QList<KoSectionEnd *> closeList = KoSectionUtils::sectionEndings(block.blockFormat());
183 foreach (KoSectionEnd *se, closeList) {
184 if (!m_curSectionDelimiters.empty() && m_curSectionDelimiters.last().name == se->name()) {
185 KoSection *section = se->correspondingSection();
186 int childIdx = KoTextDocument(m_command->m_document).sectionModel()
187 ->findRowOfChild(section);
188
189 m_command->m_sectionsToRemove.push_back(
190 DeleteCommand::SectionDeleteInfo(
191 section,
192 childIdx
193 )
194 );
195 m_curSectionDelimiters.pop_back(); // This section will die
196 } else {
197 m_curSectionDelimiters.push_back(SectionHandle(se->name(), se));
198 }
199 }
200 }
201
202 if (!doesBeginInside && doesEndInside) {
203 m_startBlockNum = block.blockNumber();
204 } else if (doesBeginInside && !doesEndInside) {
205 m_endBlockNum = block.blockNumber();
206 } else if (doesBeginInside && doesEndInside) {
207 m_hasEntirelyInsideBlock = true;
208 }
209 }
210
visitFragmentSelection(QTextCursor & fragmentSelection)211 void visitFragmentSelection(QTextCursor &fragmentSelection) override
212 {
213 if (m_first) {
214 m_firstFormat = fragmentSelection.charFormat();
215 m_first = false;
216 }
217
218 if (m_command->m_mergePossible && fragmentSelection.charFormat() != m_firstFormat) {
219 m_command->m_mergePossible = false;
220 }
221
222 // Handling InlineObjects below
223 KoTextDocument textDocument(fragmentSelection.document());
224 KoInlineTextObjectManager *manager = textDocument.inlineTextObjectManager();
225
226 QString selected = fragmentSelection.selectedText();
227 fragmentSelection.setPosition(fragmentSelection.selectionStart() + 1);
228 int position = fragmentSelection.position();
229 const QChar *data = selected.constData();
230 for (int i = 0; i < selected.length(); i++) {
231 if (data->unicode() == QChar::ObjectReplacementCharacter) {
232 fragmentSelection.setPosition(position + i);
233 KoInlineObject *object = manager->inlineTextObject(fragmentSelection);
234 m_command->m_invalidInlineObjects.insert(object);
235 }
236 data++;
237 }
238 }
239
240 enum SectionHandleAction
241 {
242 SectionClose, ///< Denotes close of the section.
243 SectionOpen ///< Denotes start or beginning of the section.
244 };
245
246 /// Helper struct for handling sections.
247 struct SectionHandle {
248 QString name; ///< Name of the section.
249 SectionHandleAction type; ///< Action of a SectionHandle.
250
251 KoSection *dataSec; ///< Pointer to KoSection.
252 KoSectionEnd *dataSecEnd; ///< Pointer to KoSectionEnd.
253
SectionHandleDeleteVisitor::SectionHandle254 SectionHandle(const QString &_name, KoSection *_data)
255 : name(_name)
256 , type(SectionOpen)
257 , dataSec(_data)
258 , dataSecEnd(0)
259 {
260 }
261
SectionHandleDeleteVisitor::SectionHandle262 SectionHandle(const QString &_name, KoSectionEnd *_data)
263 : name(_name)
264 , type(SectionClose)
265 , dataSec(0)
266 , dataSecEnd(_data)
267 {
268 }
269 };
270
271 bool m_first;
272 DeleteCommand *m_command;
273 QTextCharFormat m_firstFormat;
274 int m_startBlockNum;
275 int m_endBlockNum;
276 bool m_hasEntirelyInsideBlock;
277 QList<SectionHandle> m_curSectionDelimiters;
278 };
279
finalizeSectionHandling(QTextCursor * cur,DeleteVisitor & v)280 void DeleteCommand::finalizeSectionHandling(QTextCursor *cur, DeleteVisitor &v)
281 {
282 // Lets handle pointers from block formats first
283 // It means that selection isn't within one block.
284 if (v.m_hasEntirelyInsideBlock || v.m_startBlockNum != -1 || v.m_endBlockNum != -1) {
285 QList<KoSection *> openList;
286 QList<KoSectionEnd *> closeList;
287 foreach (const DeleteVisitor::SectionHandle &handle, v.m_curSectionDelimiters) {
288 if (handle.type == v.SectionOpen) { // Start of the section.
289 openList << handle.dataSec;
290 } else { // End of the section.
291 closeList << handle.dataSecEnd;
292 }
293 }
294
295 // We're expanding ends in affected blocks to the end of the start block,
296 // delete all sections, that are entirely in affected blocks,
297 // and move ends, we have, to the begin of the next after the end block.
298 if (v.m_startBlockNum != -1) {
299 QTextBlockFormat fmt = cur->document()->findBlockByNumber(v.m_startBlockNum).blockFormat();
300 QTextBlockFormat fmt2 = cur->document()->findBlockByNumber(v.m_endBlockNum + 1).blockFormat();
301 fmt.clearProperty(KoParagraphStyle::SectionEndings);
302
303 // m_endBlockNum != -1 in this case.
304 QList<KoSectionEnd *> closeListEndBlock = KoSectionUtils::sectionEndings(
305 cur->document()->findBlockByNumber(v.m_endBlockNum).blockFormat());
306
307 while (!openList.empty() && !closeListEndBlock.empty()
308 && openList.last()->name() == closeListEndBlock.first()->name()) {
309
310 int childIdx = KoTextDocument(m_document)
311 .sectionModel()->findRowOfChild(openList.back());
312 m_sectionsToRemove.push_back(
313 DeleteCommand::SectionDeleteInfo(
314 openList.back(),
315 childIdx
316 )
317 );
318
319 openList.pop_back();
320 closeListEndBlock.pop_front();
321 }
322 openList << KoSectionUtils::sectionStartings(fmt2);
323 closeList << closeListEndBlock;
324
325 // We leave open section of start block untouched.
326 KoSectionUtils::setSectionStartings(fmt2, openList);
327 KoSectionUtils::setSectionEndings(fmt, closeList);
328
329 QTextCursor changer = *cur;
330 changer.setPosition(cur->document()->findBlockByNumber(v.m_startBlockNum).position());
331 changer.setBlockFormat(fmt);
332 if (v.m_endBlockNum + 1 < cur->document()->blockCount()) {
333 changer.setPosition(cur->document()->findBlockByNumber(v.m_endBlockNum + 1).position());
334 changer.setBlockFormat(fmt2);
335 }
336 } else { // v.m_startBlockNum == -1
337 // v.m_endBlockNum != -1 in this case.
338 // We're pushing all new section info to the end block.
339 QTextBlockFormat fmt = cur->document()->findBlockByNumber(v.m_endBlockNum).blockFormat();
340 QList<KoSection *> allStartings = KoSectionUtils::sectionStartings(fmt);
341 fmt.clearProperty(KoParagraphStyle::SectionStartings);
342
343 QList<KoSectionEnd *> pairedEndings;
344 QList<KoSectionEnd *> unpairedEndings;
345
346 foreach (KoSectionEnd *se, KoSectionUtils::sectionEndings(fmt)) {
347 KoSection *sec = se->correspondingSection();
348
349 if (allStartings.contains(sec)) {
350 pairedEndings << se;
351 } else {
352 unpairedEndings << se;
353 }
354 }
355
356 if (cur->selectionStart()) {
357 QTextCursor changer = *cur;
358 changer.setPosition(cur->selectionStart() - 1);
359
360 QTextBlockFormat prevFmt = changer.blockFormat();
361 QList<KoSectionEnd *> prevEndings = KoSectionUtils::sectionEndings(prevFmt);
362
363 prevEndings = prevEndings + closeList;
364
365 KoSectionUtils::setSectionEndings(prevFmt, prevEndings);
366 changer.setBlockFormat(prevFmt);
367 }
368
369 KoSectionUtils::setSectionStartings(fmt, openList);
370 KoSectionUtils::setSectionEndings(fmt, pairedEndings + unpairedEndings);
371
372 QTextCursor changer = *cur;
373 changer.setPosition(cur->document()->findBlockByNumber(v.m_endBlockNum).position());
374 changer.setBlockFormat(fmt);
375 }
376 }
377
378 // Now lets deal with KoSectionModel
379 std::sort(m_sectionsToRemove.begin(), m_sectionsToRemove.end());
380 deleteSectionsFromModel();
381 }
382
deleteSectionsFromModel()383 void DeleteCommand::deleteSectionsFromModel()
384 {
385 KoSectionModel *model = KoTextDocument(m_document).sectionModel();
386 foreach (const SectionDeleteInfo &info, m_sectionsToRemove) {
387 model->deleteFromModel(info.section);
388 }
389 }
390
insertSectionsToModel()391 void DeleteCommand::insertSectionsToModel()
392 {
393 KoSectionModel *model = KoTextDocument(m_document).sectionModel();
394 QList<SectionDeleteInfo>::ConstIterator it = m_sectionsToRemove.constEnd();
395 while (it != m_sectionsToRemove.constBegin()) {
396 --it;
397 model->insertToModel(it->section, it->childIdx);
398 }
399 }
400
doDelete()401 void DeleteCommand::doDelete()
402 {
403 KoTextEditor *textEditor = KoTextDocument(m_document).textEditor();
404 Q_ASSERT(textEditor);
405 QTextCursor *caret = textEditor->cursor();
406 QTextCharFormat charFormat = caret->charFormat();
407 bool caretAtBeginOfBlock = (caret->position() == caret->block().position());
408
409 if (!textEditor->hasSelection()) {
410 if (m_mode == PreviousChar) {
411 caret->movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
412 } else {
413 caret->movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
414 }
415 }
416
417 DeleteVisitor visitor(textEditor, this);
418 textEditor->recursivelyVisitSelection(m_document.data()->rootFrame()->begin(), visitor);
419
420 // Sections Model
421 finalizeSectionHandling(caret, visitor); // Finalize section handling routine.
422
423 // InlineObjects
424 foreach (KoInlineObject *object, m_invalidInlineObjects) {
425 deleteInlineObject(object);
426 }
427
428 // Ranges
429 KoTextRangeManager *rangeManager = KoTextDocument(m_document).textRangeManager();
430
431 m_rangesToRemove = rangeManager->textRangesChangingWithin(
432 textEditor->document(),
433 textEditor->selectionStart(),
434 textEditor->selectionEnd(),
435 textEditor->selectionStart(),
436 textEditor->selectionEnd()
437 );
438
439 foreach (KoTextRange *range, m_rangesToRemove) {
440 KoAnchorTextRange *anchorRange = dynamic_cast<KoAnchorTextRange *>(range);
441 KoAnnotation *annotation = dynamic_cast<KoAnnotation *>(range);
442 if (anchorRange) {
443 // we should only delete the anchor if the selection is covering it... not if the selection is
444 // just adjacent to the anchor. This is more in line with what other wordprocessors do
445 if (anchorRange->position() != textEditor->selectionStart()
446 && anchorRange->position() != textEditor->selectionEnd()) {
447 KoShape *shape = anchorRange->anchor()->shape();
448 if (m_shapeController) {
449 KUndo2Command *shapeDeleteCommand = m_shapeController->removeShape(shape, this);
450 shapeDeleteCommand->redo();
451 }
452 // via m_shapeController->removeShape a DeleteAnchorsCommand should be created that
453 // also calls rangeManager->remove(range), so we shouldn't do that here aswell
454 }
455 } else if (annotation) {
456 KoShape *shape = annotation->annotationShape();
457 if (m_shapeController) {
458 KUndo2Command *shapeDeleteCommand = m_shapeController->removeShape(shape, this);
459 shapeDeleteCommand->redo();
460 }
461 // via m_shapeController->removeShape a DeleteAnnotationsCommand should be created that
462 // also calls rangeManager->remove(range), so we shouldn't do that here aswell
463 } else {
464 rangeManager->remove(range);
465 }
466 }
467
468 // Check: is merge possible?
469 if (textEditor->hasComplexSelection()) {
470 m_mergePossible = false;
471 }
472
473 //FIXME: lets forbid merging of "section affecting" deletions by now
474 if (!m_sectionsToRemove.empty()) {
475 m_mergePossible = false;
476 }
477
478 if (m_mergePossible) {
479 // Store various info needed for checkMerge
480 m_format = textEditor->charFormat();
481 m_position = textEditor->selectionStart();
482 m_length = textEditor->selectionEnd() - textEditor->selectionStart();
483 }
484
485 // Actual deletion of text
486 caret->deleteChar();
487
488 if (m_mode != PreviousChar || !caretAtBeginOfBlock) {
489 caret->setCharFormat(charFormat);
490 }
491 }
492
deleteInlineObject(KoInlineObject * object)493 void DeleteCommand::deleteInlineObject(KoInlineObject *object)
494 {
495 if (object) {
496 KoAnchorInlineObject *anchorObject = dynamic_cast<KoAnchorInlineObject *>(object);
497 if (anchorObject) {
498 KoShape *shape = anchorObject->anchor()->shape();
499 KUndo2Command *shapeDeleteCommand = m_shapeController->removeShape(shape, this);
500 shapeDeleteCommand->redo();
501 } else {
502 object->manager()->removeInlineObject(object);
503 }
504 }
505 }
506
id() const507 int DeleteCommand::id() const
508 {
509 // Should be an enum declared somewhere. KoTextCommandBase.h ???
510 return 56789;
511 }
512
mergeWith(const KUndo2Command * command)513 bool DeleteCommand::mergeWith(const KUndo2Command *command)
514 {
515 class UndoTextCommand : public KUndo2Command
516 {
517 public:
518 UndoTextCommand(QTextDocument *document, KUndo2Command *parent = 0)
519 : KUndo2Command(kundo2_i18n("Text"), parent),
520 m_document(document)
521 {}
522
523 void undo() override {
524 QTextDocument *doc = m_document.data();
525 if (doc)
526 doc->undo(KoTextDocument(doc).textEditor()->cursor());
527 }
528
529 void redo() override {
530 QTextDocument *doc = m_document.data();
531 if (doc)
532 doc->redo(KoTextDocument(doc).textEditor()->cursor());
533 }
534
535 QPointer<QTextDocument> m_document;
536 };
537
538 KoTextEditor *textEditor = KoTextDocument(m_document).textEditor();
539 if (textEditor == 0)
540 return false;
541
542 if (command->id() != id())
543 return false;
544
545 if (!checkMerge(command))
546 return false;
547
548 DeleteCommand *other = const_cast<DeleteCommand *>(static_cast<const DeleteCommand *>(command));
549
550 m_invalidInlineObjects += other->m_invalidInlineObjects;
551 other->m_invalidInlineObjects.clear();
552
553 for (int i=0; i < command->childCount(); i++)
554 new UndoTextCommand(const_cast<QTextDocument*>(textEditor->document()), this);
555
556 return true;
557 }
558
checkMerge(const KUndo2Command * command)559 bool DeleteCommand::checkMerge(const KUndo2Command *command)
560 {
561 DeleteCommand *other = const_cast<DeleteCommand *>(static_cast<const DeleteCommand *>(command));
562
563 if (!(m_mergePossible && other->m_mergePossible))
564 return false;
565
566 if (m_position == other->m_position && m_format == other->m_format) {
567 m_length += other->m_length;
568 return true;
569 }
570
571 if ((other->m_position + other->m_length == m_position)
572 && (m_format == other->m_format)) {
573 m_position = other->m_position;
574 m_length += other->m_length;
575 return true;
576 }
577 return false;
578 }
579
updateListChanges()580 void DeleteCommand::updateListChanges()
581 {
582 KoTextEditor *textEditor = KoTextDocument(m_document).textEditor();
583 if (textEditor == 0)
584 return;
585 QTextDocument *document = const_cast<QTextDocument*>(textEditor->document());
586 QTextCursor tempCursor(document);
587 QTextBlock startBlock = document->findBlock(m_position);
588 QTextBlock endBlock = document->findBlock(m_position + m_length);
589 if (endBlock != document->end())
590 endBlock = endBlock.next();
591 QTextList *currentList;
592
593 for (QTextBlock currentBlock = startBlock; currentBlock != endBlock; currentBlock = currentBlock.next()) {
594 tempCursor.setPosition(currentBlock.position());
595 currentList = tempCursor.currentList();
596 if (currentList) {
597 KoListStyle::ListIdType listId;
598 if (sizeof(KoListStyle::ListIdType) == sizeof(uint))
599 listId = currentList->format().property(KoListStyle::ListId).toUInt();
600 else
601 listId = currentList->format().property(KoListStyle::ListId).toULongLong();
602
603 if (!KoTextDocument(document).list(currentBlock)) {
604 KoList *list = KoTextDocument(document).list(listId);
605 if (list) {
606 list->updateStoredList(currentBlock);
607 }
608 }
609 }
610 }
611 }
612
~DeleteCommand()613 DeleteCommand::~DeleteCommand()
614 {
615 }
616