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