1 /*
2  This file is part of the KDE project
3  * Copyright (C) 2009 Ganesh Paramasivam <ganesh@crystalfab.com>
4  * Copyright (C) 2009 Pierre Stirnweiss <pstirnweiss@googlemail.com>
5  * Copyright (C) 2010 Thomas Zander <zander@kde.org>
6  *
7  * This library is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Library General Public
9  * License as published by the Free Software Foundation; either
10  * version 2 of the License, or (at your option) any later version.
11  *
12  * This library is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Library General Public License for more details.
16  *
17  * You should have received a copy of the GNU Library General Public License
18  * along with this library; see the file COPYING.LIB.  If not, write to
19  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20  * Boston, MA 02110-1301, USA.*/
21 
22 #include "ChangeTrackedDeleteCommand.h"
23 
24 #include "TextPasteCommand.h"
25 #include "ListItemNumberingCommand.h"
26 #include "ChangeListCommand.h"
27 
28 #include <KoTextEditor.h>
29 #include <KoChangeTracker.h>
30 #include <KoChangeTrackerElement.h>
31 #include <KoTextDocument.h>
32 #include <KoInlineTextObjectManager.h>
33 #include <KoShapeController.h>
34 #include <KoList.h>
35 #include <KoParagraphStyle.h>
36 #include <KoTextOdfSaveHelper.h>
37 #include <KoTextDrag.h>
38 #include <KoOdf.h>
39 #include <KoDocumentRdfBase.h>
40 
41 #include <QTextDocumentFragment>
42 
43 #include <klocalizedstring.h>
44 
45 #include "TextDebug.h"
46 
47 //A convenience function to get a ListIdType from a format
48 
ListId(const QTextListFormat & format)49 static KoListStyle::ListIdType ListId(const QTextListFormat &format)
50 {
51     KoListStyle::ListIdType listId;
52 
53     if (sizeof(KoListStyle::ListIdType) == sizeof(uint))
54         listId = format.property(KoListStyle::ListId).toUInt();
55     else
56         listId = format.property(KoListStyle::ListId).toULongLong();
57 
58     return listId;
59 }
60 
61 using namespace std;
ChangeTrackedDeleteCommand(DeleteMode mode,QTextDocument * document,KoShapeController * shapeController,KUndo2Command * parent)62 ChangeTrackedDeleteCommand::ChangeTrackedDeleteCommand(DeleteMode mode,
63                                                        QTextDocument *document,
64                                                        KoShapeController *shapeController,
65                                                        KUndo2Command *parent) :
66     KoTextCommandBase (parent),
67     m_document(document),
68     m_rdf(0),
69     m_shapeController(shapeController),
70     m_first(true),
71     m_undone(false),
72     m_canMerge(true),
73     m_mode(mode),
74     m_removedElements()
75 {
76       setText(kundo2_i18n("Delete"));
77       m_rdf = qobject_cast<KoDocumentRdfBase*>(shapeController->resourceManager()->resource(KoText::DocumentRdf).value<QObject*>());
78 }
79 
undo()80 void ChangeTrackedDeleteCommand::undo()
81 {
82     if (m_document.isNull()) return;
83 
84     KoTextCommandBase::undo();
85     UndoRedoFinalizer finalizer(this);
86 
87     KoTextDocument textDocument(m_document.data());
88     textDocument.changeTracker()->elementById(m_addedChangeElement)->setValid(false);
89     foreach (int changeId, m_removedElements) {
90         textDocument.changeTracker()->elementById(changeId)->setValid(true);
91     }
92     updateListChanges();
93     m_undone = true;
94 }
95 
redo()96 void ChangeTrackedDeleteCommand::redo()
97 {
98     if (!m_document.isNull()) return;
99 
100     m_undone = false;
101     KoTextDocument textDocument(m_document.data());
102 
103     if (!m_first) {
104         KoTextCommandBase::redo();
105         UndoRedoFinalizer finalizer(this);
106         textDocument.changeTracker()->elementById(m_addedChangeElement)->setValid(true);
107         foreach (int changeId, m_removedElements) {
108             textDocument.changeTracker()->elementById(changeId)->setValid(false);
109         }
110     } else {
111         m_first = false;
112         textDocument.textEditor()->beginEditBlock();
113         if(m_mode == PreviousChar)
114             deletePreviousChar();
115         else
116             deleteChar();
117         textDocument.textEditor()->endEditBlock();
118     }
119 }
120 
deleteChar()121 void ChangeTrackedDeleteCommand::deleteChar()
122 {
123     if (m_document.isNull()) return;
124 
125     KoTextEditor *editor = KoTextDocument(m_document).textEditor();
126 
127     if (editor->atEnd() && !editor->hasSelection())
128         return;
129 
130     if (!editor->hasSelection())
131         editor->movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
132 
133     deleteSelection(editor);
134 }
135 
deletePreviousChar()136 void ChangeTrackedDeleteCommand::deletePreviousChar()
137 {
138     if (m_document.isNull()) return;
139 
140     KoTextEditor *editor = KoTextDocument(m_document).textEditor();
141 
142     if (editor->atStart() && !editor->hasSelection())
143         return;
144 
145     if (!editor->hasSelection()
146             && editor->block().textList()
147             && (editor->position() == editor->block().position())) {
148         handleListItemDelete(editor);
149         return;
150     }
151 
152     if (!editor->hasSelection()) {
153         editor->movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
154     }
155     deleteSelection(editor);
156 }
157 
handleListItemDelete(KoTextEditor * editor)158 void ChangeTrackedDeleteCommand::handleListItemDelete(KoTextEditor *editor)
159 {
160     if (m_document.isNull()) return;
161 
162     m_canMerge = false;
163     bool numberedListItem = false;
164     if (!editor->blockFormat().boolProperty(KoParagraphStyle::UnnumberedListItem))
165          numberedListItem = true;
166 
167     // Mark the complete list-item
168     QTextBlock block = m_document.data()->findBlock(editor->position());
169     editor->movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, (block.length() - 1));
170 
171     // Copy the marked item
172     int from = editor->anchor();
173     int to = editor->position();
174     KoTextOdfSaveHelper saveHelper(m_document.data(), from, to);
175     KoTextDrag drag;
176 #ifdef SHOULD_BUILD_RDF
177     if (m_rdf) {
178         saveHelper.setRdfModel(m_rdf->model());
179     }
180 #endif
181     drag.setOdf(KoOdf::mimeType(KoOdf::Text), saveHelper);
182     QTextDocumentFragment fragment = editor->selection();
183     drag.setData("text/html", fragment.toHtml("utf-8").toUtf8());
184     drag.setData("text/plain", fragment.toPlainText().toUtf8());
185 
186     // Delete the marked section
187     editor->setPosition(editor->anchor() -1);
188     editor->movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, block.length());
189     deleteSelection(editor);
190     // Mark it as inserted content
191     QTextCharFormat format = editor->charFormat();
192     editor->registerTrackedChange(*editor->cursor(), KoGenChange::InsertChange, i18n("Key Press"), format, format, false);
193     //Paste the selected text from the clipboard... (XXX: is this really correct here?)
194     TextPasteCommand *pasteCommand =
195             new TextPasteCommand(drag.mimeData(),
196                                  m_document.data(),
197                                  m_shapeController,
198                                  this);
199 
200     pasteCommand->redo();
201 
202     // Convert it into a un-numbered list-item or a paragraph
203     if (numberedListItem) {
204         ListItemNumberingCommand *changeNumberingCommand = new ListItemNumberingCommand(editor->block(), false, this);
205         changeNumberingCommand->redo();
206     } else {
207         KoListLevelProperties llp;
208         llp.setStyle(KoListStyle::None);
209         llp.setLevel(0);
210         ChangeListCommand *changeListCommand = new ChangeListCommand(*editor->cursor(), llp,
211                                                                      KoTextEditor::ModifyExistingList | KoTextEditor::MergeWithAdjacentList,
212                                                                      this);
213         changeListCommand->redo();
214     }
215     editor->setPosition(editor->block().position());
216 }
217 
deleteSelection(KoTextEditor * editor)218 void ChangeTrackedDeleteCommand::deleteSelection(KoTextEditor *editor)
219 {
220     if (m_document.isNull()) return;
221 
222     // XXX: don't allow anyone to steal our cursor!
223     QTextCursor *selection = editor->cursor();
224     QTextCursor checker = QTextCursor(*editor->cursor());
225 
226     bool backwards = (checker.anchor() > checker.position());
227     int selectionBegin = qMin(checker.anchor(), checker.position());
228     int selectionEnd = qMax(checker.anchor(), checker.position());
229     int changeId;
230 
231     QList<KoShape *> shapesInSelection;
232 
233     checker.setPosition(selectionBegin);
234 
235     KoTextDocument textDocument(m_document.data());
236     KoInlineTextObjectManager *inlineTextObjectManager = textDocument.inlineTextObjectManager();
237 
238     while ((checker.position() < selectionEnd) && (!checker.atEnd())) {
239         QChar charAtPos = m_document.data()->characterAt(checker.position());
240         checker.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
241         if (inlineTextObjectManager->inlineTextObject(checker) && charAtPos == QChar::ObjectReplacementCharacter) {
242         /* This has changed but since this entire command is going away - let's not bother
243                 KoTextAnchor *anchor = dynamic_cast<KoTextAnchor *>(inlineTextObjectManager->inlineTextObject(checker));
244                 if (anchor)
245                     shapesInSelection.push_back(anchor->shape());
246         */
247         }
248         checker.setPosition(checker.position());
249     }
250 
251     checker.setPosition(selectionBegin);
252 
253     if (!KoTextDocument(m_document).changeTracker()->displayChanges()) {
254         QChar charAtPos = m_document.data()->characterAt(checker.position() - 1);
255     }
256 
257     if (KoTextDocument(m_document).changeTracker()->containsInlineChanges(checker.charFormat())) {
258         int changeId = checker.charFormat().property(KoCharacterStyle::ChangeTrackerId).toInt();
259         if (KoTextDocument(m_document).changeTracker()->elementById(changeId)->getChangeType() == KoGenChange::DeleteChange) {
260             QTextDocumentFragment prefix =  KoTextDocument(m_document).changeTracker()->elementById(changeId)->getDeleteData();
261             selectionBegin -= (KoChangeTracker::fragmentLength(prefix) + 1 );
262             KoTextDocument(m_document).changeTracker()->elementById(changeId)->setValid(false);
263             m_removedElements.push_back(changeId);
264         }
265     }
266 
267     checker.setPosition(selectionEnd);
268     if (!checker.atEnd()) {
269         QChar charAtPos = m_document.data()->characterAt(checker.position());
270         checker.movePosition(QTextCursor::NextCharacter);
271     }
272 
273     selection->setPosition(selectionBegin);
274     selection->setPosition(selectionEnd, QTextCursor::KeepAnchor);
275     QTextDocumentFragment deletedFragment;
276     changeId = KoTextDocument(m_document).changeTracker()->getDeleteChangeId(i18n("Delete"), deletedFragment, 0);
277     KoChangeTrackerElement *element = KoTextDocument(m_document).changeTracker()->elementById(changeId);
278 
279     QTextCharFormat charFormat;
280     charFormat.setProperty(KoCharacterStyle::ChangeTrackerId, changeId);
281     selection->mergeCharFormat(charFormat);
282 
283     deletedFragment = KoChangeTracker::generateDeleteFragment(*selection);
284     element->setDeleteData(deletedFragment);
285 
286     //Store the position and length. Will be used in updateListChanges()
287     m_position = (selection->anchor() < selection->position()) ? selection->anchor():selection->position();
288     m_length = qAbs(selection->anchor() - selection->position());
289 
290     updateListIds(*editor->cursor());
291 
292     m_addedChangeElement = changeId;
293 
294     //Insert the deleted data again after the marker with the charformat set to the change-id
295     if (KoTextDocument(m_document).changeTracker()->displayChanges()) {
296         int startPosition = selection->position();
297         KoChangeTracker::insertDeleteFragment(*selection);
298         QTextCursor tempCursor(*selection);
299         tempCursor.setPosition(startPosition);
300         tempCursor.setPosition(selection->position(), QTextCursor::KeepAnchor);
301         // XXX: why was this commented out?
302         //tempCursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, fragmentLength(deletedFragment));
303         updateListIds(tempCursor);
304         if (backwards) {
305             selection->movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor, KoChangeTracker::fragmentLength(deletedFragment) + 1);
306         }
307     } else {
308         if (backwards) {
309             selection->movePosition(QTextCursor::PreviousCharacter, QTextCursor::MoveAnchor,1);
310         }
311 
312         foreach (KoShape *shape, shapesInSelection) {
313             KUndo2Command *shapeDeleteCommand = m_shapeController->removeShape(shape, this);
314             shapeDeleteCommand->redo();
315             m_canMerge = false;
316         }
317     }
318 }
319 
id() const320 int ChangeTrackedDeleteCommand::id() const
321 {
322     return 98765;
323 }
324 
mergeWith(const KUndo2Command * command)325 bool ChangeTrackedDeleteCommand::mergeWith( const KUndo2Command *command)
326 {
327     class UndoTextCommand : public KUndo2Command
328     {
329     public:
330         UndoTextCommand(QTextDocument *document, KUndo2Command *parent = 0)
331             : KUndo2Command(kundo2_i18n("Text"), parent),
332               m_document(document)
333         {}
334 
335         void undo() {
336             QTextDocument *doc = const_cast<QTextDocument*>(m_document.data());
337             if (doc)
338                 doc->undo(KoTextDocument(doc).textEditor()->cursor());
339         }
340 
341         void redo() {
342             QTextDocument *doc = const_cast<QTextDocument*>(m_document.data());
343             if (doc)
344                 doc->redo(KoTextDocument(doc).textEditor()->cursor());
345         }
346 
347         QWeakPointer<QTextDocument> m_document;
348     };
349 
350     if (command->id() != id())
351         return false;
352 
353     ChangeTrackedDeleteCommand *other = const_cast<ChangeTrackedDeleteCommand *>(static_cast<const ChangeTrackedDeleteCommand *>(command));
354 
355     if (other->m_canMerge == false)
356         return false;
357 
358     if (other->m_removedElements.contains(m_addedChangeElement)) {
359         removeChangeElement(m_addedChangeElement);
360         other->m_removedElements.removeAll(m_addedChangeElement);
361         m_addedChangeElement = other->m_addedChangeElement;
362 
363         m_removedElements += other->m_removedElements;
364         other->m_removedElements.clear();
365 
366         m_newListIds = other->m_newListIds;
367 
368         m_position = other->m_position;
369         m_length = other->m_length;
370 
371         for(int i=0; i < command->childCount(); i++) {
372             new UndoTextCommand(m_document.data(), this);
373         }
374 
375         return true;
376     }
377     return false;
378 }
379 
updateListIds(QTextCursor & cursor)380 void ChangeTrackedDeleteCommand::updateListIds(QTextCursor &cursor)
381 {
382     if (m_document.isNull()) return;
383 
384     m_newListIds.clear();
385     QTextCursor tempCursor(m_document.data());
386     QTextBlock startBlock = m_document.data()->findBlock(cursor.anchor());
387     QTextBlock endBlock = m_document.data()->findBlock(cursor.position());
388     QTextList *currentList;
389 
390     for (QTextBlock currentBlock = startBlock; currentBlock != endBlock.next(); currentBlock = currentBlock.next()) {
391         tempCursor.setPosition(currentBlock.position());
392         currentList = tempCursor.currentList();
393         if (currentList) {
394             KoListStyle::ListIdType listId = ListId(currentList->format());
395             m_newListIds.push_back(listId);
396         }
397     }
398 }
updateListChanges()399 void ChangeTrackedDeleteCommand::updateListChanges()
400 {
401     if (m_document.isNull()) return;
402 
403     QTextCursor tempCursor(m_document.data());
404     QTextBlock startBlock = m_document.data()->findBlock(m_position);
405     QTextBlock endBlock = m_document.data()->findBlock(m_position + m_length);
406     QTextList *currentList;
407     int newListIdsCounter = 0;
408 
409     for (QTextBlock currentBlock = startBlock; currentBlock != endBlock.next(); currentBlock = currentBlock.next()) {
410         tempCursor.setPosition(currentBlock.position());
411         currentList = tempCursor.currentList();
412         if (currentList) {
413             KoListStyle::ListIdType listId = m_newListIds[newListIdsCounter];
414             if (!KoTextDocument(m_document).list(currentBlock)) {
415                 KoList *list = KoTextDocument(m_document).list(listId);
416                 if (list)
417                     list->updateStoredList(currentBlock);
418             }
419             newListIdsCounter++;
420         }
421     }
422 }
423 
~ChangeTrackedDeleteCommand()424 ChangeTrackedDeleteCommand::~ChangeTrackedDeleteCommand()
425 {
426     if (m_undone) {
427         removeChangeElement(m_addedChangeElement);
428     } else {
429         foreach (int changeId, m_removedElements) {
430            removeChangeElement(changeId);
431         }
432     }
433 }
434 
removeChangeElement(int changeId)435 void ChangeTrackedDeleteCommand::removeChangeElement(int changeId)
436 {
437     KoTextDocument textDocument(m_document);
438     KoChangeTrackerElement *element = textDocument.changeTracker()->elementById(changeId);
439     KoTextDocument(m_document).changeTracker()->removeById(changeId);
440 }
441