1 // -*- indent-tabs-mode:nil -*-
2 // vim: set ts=4 sts=4 sw=4 et:
3 /* This file is part of the KDE project
4    Copyright (C) 2000 David Faure <faure@kde.org>
5    Copyright (C) 2002-2003 Alexander Kellett <lypanov@kde.org>
6 
7    This program is free software; you can redistribute it and/or
8    modify it under the terms of the GNU General Public
9    License version 2 or at your option version 3 as published by
10    the Free Software Foundation.
11 
12    This program 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    General Public License for more details.
16 
17    You should have received a copy of the GNU General Public License
18    along with this program; see the file COPYING.  If not, write to
19    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
20    Boston, MA 02110-1301, USA.
21 */
22 
23 #include "commands.h"
24 #include "commands_p.h"
25 #include "kinsertionsort_p.h"
26 #include "model.h"
27 
28 #include "keditbookmarks_debug.h"
29 #include <KBookmarkManager>
30 #include <KDesktopFile>
31 #include <KLocalizedString>
32 
affectedBookmarks() const33 QString KEBMacroCommand::affectedBookmarks() const
34 {
35     const int commandCount = childCount();
36     if (commandCount == 0) {
37         return QString();
38     }
39     // Need to use dynamic_cast here due to going cross-hierarchy, but it should never return 0.
40     int i = 0;
41     QString affectBook = dynamic_cast<const IKEBCommand *>(child(i))->affectedBookmarks();
42     ++i;
43     for (; i < commandCount; ++i) {
44         affectBook = KBookmark::commonParent(affectBook, dynamic_cast<const IKEBCommand *>(child(i))->affectedBookmarks());
45     }
46     return affectBook;
47 }
48 
DeleteManyCommand(KBookmarkModel * model,const QString & name,const QList<KBookmark> & bookmarks)49 DeleteManyCommand::DeleteManyCommand(KBookmarkModel *model, const QString &name, const QList<KBookmark> &bookmarks)
50     : KEBMacroCommand(name)
51 {
52     QList<KBookmark>::const_iterator it, begin;
53     begin = bookmarks.constBegin();
54     it = bookmarks.constEnd();
55     while (begin != it) {
56         --it;
57         new DeleteCommand(model, (*it).address(), false, this);
58     }
59 }
60 
61 ////
62 
CreateCommand(KBookmarkModel * model,const QString & address,QUndoCommand * parent)63 CreateCommand::CreateCommand(KBookmarkModel *model, const QString &address, QUndoCommand *parent)
64     : QUndoCommand(parent)
65     , m_model(model)
66     , m_to(address)
67     , m_group(false)
68     , m_separator(true)
69     , m_originalBookmark(QDomElement())
70 {
71     setText(i18nc("(qtundo-format)", "Insert Separator"));
72 }
73 
CreateCommand(KBookmarkModel * model,const QString & address,const QString & text,const QString & iconPath,const QUrl & url,QUndoCommand * parent)74 CreateCommand::CreateCommand(KBookmarkModel *model, const QString &address, const QString &text, const QString &iconPath, const QUrl &url, QUndoCommand *parent)
75     : QUndoCommand(parent)
76     , m_model(model)
77     , m_to(address)
78     , m_text(text)
79     , m_iconPath(iconPath)
80     , m_url(url)
81     , m_group(false)
82     , m_separator(false)
83     , m_originalBookmark(QDomElement())
84 {
85     setText(i18nc("(qtundo-format)", "Create Bookmark"));
86 }
87 
CreateCommand(KBookmarkModel * model,const QString & address,const QString & text,const QString & iconPath,bool open,QUndoCommand * parent)88 CreateCommand::CreateCommand(KBookmarkModel *model, const QString &address, const QString &text, const QString &iconPath, bool open, QUndoCommand *parent)
89     : QUndoCommand(parent)
90     , m_model(model)
91     , m_to(address)
92     , m_text(text)
93     , m_iconPath(iconPath)
94     , m_group(true)
95     , m_separator(false)
96     , m_open(open)
97     , m_originalBookmark(QDomElement())
98 {
99     setText(i18nc("(qtundo-format)", "Create Folder"));
100 }
101 
CreateCommand(KBookmarkModel * model,const QString & address,const KBookmark & original,const QString & name,QUndoCommand * parent)102 CreateCommand::CreateCommand(KBookmarkModel *model, const QString &address, const KBookmark &original, const QString &name, QUndoCommand *parent)
103     : QUndoCommand(parent)
104     , m_model(model)
105     , m_to(address)
106     , m_group(false)
107     , m_separator(false)
108     , m_open(false)
109     , m_originalBookmark(original)
110     , m_originalBookmarkDocRef(m_originalBookmark.internalElement().ownerDocument())
111 {
112     setText(i18nc("(qtundo-format)", "Copy %1", name));
113 }
114 
redo()115 void CreateCommand::redo()
116 {
117     QString parentAddress = KBookmark::parentAddress(m_to);
118     KBookmarkGroup parentGroup = m_model->bookmarkManager()->findByAddress(parentAddress).toGroup();
119 
120     QString previousSibling = KBookmark::previousAddress(m_to);
121 
122     // qCDebug(KEDITBOOKMARKS_LOG) << "previousSibling=" << previousSibling;
123     KBookmark prev = (previousSibling.isEmpty()) ? KBookmark(QDomElement()) : m_model->bookmarkManager()->findByAddress(previousSibling);
124 
125     KBookmark bk = KBookmark(QDomElement());
126     const int pos = KBookmark::positionInParent(m_to);
127     m_model->beginInsert(parentGroup, pos, pos);
128 
129     if (m_separator) {
130         bk = parentGroup.createNewSeparator();
131 
132     } else if (m_group) {
133         Q_ASSERT(!m_text.isEmpty());
134         bk = parentGroup.createNewFolder(m_text);
135         bk.internalElement().setAttribute(QStringLiteral("folded"), (m_open ? QLatin1String("no") : QLatin1String("yes")));
136         if (!m_iconPath.isEmpty()) {
137             bk.setIcon(m_iconPath);
138         }
139     } else if (!m_originalBookmark.isNull()) {
140         QDomElement element = m_originalBookmark.internalElement().cloneNode().toElement();
141         bk = KBookmark(element);
142         parentGroup.addBookmark(bk);
143     } else {
144         bk = parentGroup.addBookmark(m_text, m_url, m_iconPath);
145     }
146 
147     // move to right position
148     bool ok = parentGroup.moveBookmark(bk, prev);
149     Q_UNUSED(ok);
150     // TODO (requires KBookmarks >= 5.25)
151     // Q_ASSERT(ok);
152     if (!(text().isEmpty()) && !parentAddress.isEmpty()) {
153         // open the parent (useful if it was empty) - only for manual commands
154         Q_ASSERT(parentGroup.internalElement().tagName() != QLatin1String("xbel"));
155         parentGroup.internalElement().setAttribute(QStringLiteral("folded"), QStringLiteral("no"));
156     }
157 
158     Q_ASSERT(bk.address() == m_to);
159     m_model->endInsert();
160 }
161 
finalAddress() const162 QString CreateCommand::finalAddress() const
163 {
164     Q_ASSERT(!m_to.isEmpty());
165     return m_to;
166 }
167 
undo()168 void CreateCommand::undo()
169 {
170     KBookmark bk = m_model->bookmarkManager()->findByAddress(m_to);
171     Q_ASSERT(!bk.isNull() && !bk.parentGroup().isNull());
172 
173     m_model->removeBookmark(bk);
174 }
175 
affectedBookmarks() const176 QString CreateCommand::affectedBookmarks() const
177 {
178     return KBookmark::parentAddress(m_to);
179 }
180 
181 /* -------------------------------------- */
182 
EditCommand(KBookmarkModel * model,const QString & address,int col,const QString & newValue,QUndoCommand * parent)183 EditCommand::EditCommand(KBookmarkModel *model, const QString &address, int col, const QString &newValue, QUndoCommand *parent)
184     : QUndoCommand(parent)
185     , m_model(model)
186     , mAddress(address)
187     , mCol(col)
188 {
189     qCDebug(KEDITBOOKMARKS_LOG) << address << col << newValue;
190     if (mCol == 1) {
191         const QUrl u(newValue);
192         if (!(u.isEmpty() && !newValue.isEmpty())) // prevent emptied line if the currently entered url is invalid
193             mNewValue = u.toString();
194         else
195             mNewValue = newValue;
196     } else
197         mNewValue = newValue;
198 
199     // -2 is "toolbar" attribute change, but that's only used internally.
200     if (mCol == -1)
201         setText(i18nc("(qtundo-format)", "Icon Change"));
202     else if (mCol == 0)
203         setText(i18nc("(qtundo-format)", "Title Change"));
204     else if (mCol == 1)
205         setText(i18nc("(qtundo-format)", "URL Change"));
206     else if (mCol == 2)
207         setText(i18nc("(qtundo-format)", "Comment Change"));
208 }
209 
redo()210 void EditCommand::redo()
211 {
212     KBookmark bk = m_model->bookmarkManager()->findByAddress(mAddress);
213     if (mCol == -2) {
214         if (mOldValue.isEmpty())
215             mOldValue = bk.internalElement().attribute(QStringLiteral("toolbar"));
216         bk.internalElement().setAttribute(QStringLiteral("toolbar"), mNewValue);
217     } else if (mCol == -1) {
218         if (mOldValue.isEmpty())
219             mOldValue = bk.icon();
220         bk.setIcon(mNewValue);
221     } else if (mCol == 0) {
222         if (mOldValue.isEmpty()) // only the first time, not when compressing changes in modify()
223             mOldValue = bk.fullText();
224         qCDebug(KEDITBOOKMARKS_LOG) << "mOldValue=" << mOldValue;
225         bk.setFullText(mNewValue);
226     } else if (mCol == 1) {
227         if (mOldValue.isEmpty())
228             mOldValue = bk.url().toDisplayString();
229         const QUrl newUrl(mNewValue);
230         if (!(newUrl.isEmpty() && !mNewValue.isEmpty())) // prevent emptied line if the currently entered url is invalid
231             bk.setUrl(newUrl);
232     } else if (mCol == 2) {
233         if (mOldValue.isEmpty())
234             mOldValue = bk.description();
235         bk.setDescription(mNewValue);
236     }
237     m_model->emitDataChanged(bk);
238 }
239 
undo()240 void EditCommand::undo()
241 {
242     qCDebug(KEDITBOOKMARKS_LOG) << "Setting old value" << mOldValue << "in bk" << mAddress << "col" << mCol;
243     KBookmark bk = m_model->bookmarkManager()->findByAddress(mAddress);
244     if (mCol == -2) {
245         bk.internalElement().setAttribute(QStringLiteral("toolbar"), mOldValue);
246     } else if (mCol == -1) {
247         bk.setIcon(mOldValue);
248     } else if (mCol == 0) {
249         bk.setFullText(mOldValue);
250     } else if (mCol == 1) {
251         bk.setUrl(QUrl(mOldValue));
252     } else if (mCol == 2) {
253         bk.setDescription(mOldValue);
254     }
255     m_model->emitDataChanged(bk);
256 }
257 
modify(const QString & newValue)258 void EditCommand::modify(const QString &newValue)
259 {
260     if (mCol == 1) {
261         const QUrl u(newValue);
262         if (!(u.isEmpty() && !newValue.isEmpty())) // prevent emptied line if the currently entered url is invalid
263             mNewValue = u.toString();
264         else
265             mNewValue = newValue;
266     } else
267         mNewValue = newValue;
268 }
269 
270 /* -------------------------------------- */
271 
DeleteCommand(KBookmarkModel * model,const QString & from,bool contentOnly,QUndoCommand * parent)272 DeleteCommand::DeleteCommand(KBookmarkModel *model, const QString &from, bool contentOnly, QUndoCommand *parent)
273     : QUndoCommand(parent)
274     , m_model(model)
275     , m_from(from)
276     , m_cmd(nullptr)
277     , m_subCmd(nullptr)
278     , m_contentOnly(contentOnly)
279 {
280     // NOTE - DeleteCommand needs no text, it is always embedded in a macrocommand
281 }
282 
redo()283 void DeleteCommand::redo()
284 {
285     KBookmark bk = m_model->bookmarkManager()->findByAddress(m_from);
286     Q_ASSERT(!bk.isNull());
287 
288     if (m_contentOnly) {
289         QDomElement groupRoot = bk.internalElement();
290 
291         QDomNode n = groupRoot.firstChild();
292         while (!n.isNull()) {
293             QDomElement e = n.toElement();
294             if (!e.isNull()) {
295                 // qCDebug(KEDITBOOKMARKS_LOG) << e.tagName();
296             }
297             QDomNode next = n.nextSibling();
298             groupRoot.removeChild(n);
299             n = next;
300         }
301         return;
302     }
303 
304     // TODO - bug - unparsed xml is lost after undo,
305     //              we must store it all therefore
306 
307     // FIXME this removes the comments, that's bad!
308     if (!m_cmd) {
309         if (bk.isGroup()) {
310             m_cmd =
311                 new CreateCommand(m_model, m_from, bk.fullText(), bk.icon(), bk.internalElement().attribute(QStringLiteral("folded")) == QLatin1String("no"));
312             m_subCmd = deleteAll(m_model, bk.toGroup());
313             m_subCmd->redo();
314 
315         } else {
316             m_cmd = (bk.isSeparator()) ? new CreateCommand(m_model, m_from) : new CreateCommand(m_model, m_from, bk.fullText(), bk.icon(), bk.url());
317         }
318     }
319     m_cmd->undo();
320 }
321 
undo()322 void DeleteCommand::undo()
323 {
324     // qCDebug(KEDITBOOKMARKS_LOG) << "DeleteCommand::undo " << m_from;
325 
326     if (m_contentOnly) {
327         // TODO - recover saved metadata
328         return;
329     }
330 
331     m_cmd->redo();
332 
333     if (m_subCmd) {
334         m_subCmd->undo();
335     }
336 }
337 
affectedBookmarks() const338 QString DeleteCommand::affectedBookmarks() const
339 {
340     return KBookmark::parentAddress(m_from);
341 }
342 
deleteAll(KBookmarkModel * model,const KBookmarkGroup & parentGroup)343 KEBMacroCommand *DeleteCommand::deleteAll(KBookmarkModel *model, const KBookmarkGroup &parentGroup)
344 {
345     KEBMacroCommand *cmd = new KEBMacroCommand(QString());
346     QStringList lstToDelete;
347     // we need to delete from the end, to avoid index shifting
348     for (KBookmark bk = parentGroup.first(); !bk.isNull(); bk = parentGroup.next(bk))
349         lstToDelete.prepend(bk.address());
350     for (QStringList::const_iterator it = lstToDelete.constBegin(); it != lstToDelete.constEnd(); ++it) {
351         new DeleteCommand(model, (*it), false, cmd);
352     }
353     return cmd;
354 }
355 
356 /* -------------------------------------- */
357 
MoveCommand(KBookmarkModel * model,const QString & from,const QString & to,const QString & name,QUndoCommand * parent)358 MoveCommand::MoveCommand(KBookmarkModel *model, const QString &from, const QString &to, const QString &name, QUndoCommand *parent)
359     : QUndoCommand(parent)
360     , m_model(model)
361     , m_from(from)
362     , m_to(to)
363     , m_cc(nullptr)
364     , m_dc(nullptr)
365 {
366     setText(i18nc("(qtundo-format)", "Move %1", name));
367 }
368 
redo()369 void MoveCommand::redo()
370 {
371     // qCDebug(KEDITBOOKMARKS_LOG) << "Moving from=" << m_from << "to=" << m_to;
372 
373     KBookmark fromBk = m_model->bookmarkManager()->findByAddress(m_from);
374     Q_ASSERT(fromBk.address() == m_from);
375 
376     // qCDebug(KEDITBOOKMARKS_LOG) << "  1) creating" << m_to;
377     m_cc = new CreateCommand(m_model, m_to, fromBk, QString());
378     m_cc->redo();
379 
380     // qCDebug(KEDITBOOKMARKS_LOG) << "  2) deleting" << fromBk.address();
381     m_dc = new DeleteCommand(m_model, fromBk.address());
382     m_dc->redo();
383 }
384 
finalAddress() const385 QString MoveCommand::finalAddress() const
386 {
387     Q_ASSERT(!m_to.isEmpty());
388     return m_to;
389 }
390 
undo()391 void MoveCommand::undo()
392 {
393     m_dc->undo();
394     m_cc->undo();
395 }
396 
affectedBookmarks() const397 QString MoveCommand::affectedBookmarks() const
398 {
399     return KBookmark::commonParent(KBookmark::parentAddress(m_from), KBookmark::parentAddress(m_to));
400 }
401 
402 /* -------------------------------------- */
403 
404 class SortItem
405 {
406 public:
SortItem(const KBookmark & bk)407     SortItem(const KBookmark &bk)
408         : m_bk(bk)
409     {
410     }
411 
operator ==(const SortItem & s)412     bool operator==(const SortItem &s)
413     {
414         return (m_bk.internalElement() == s.m_bk.internalElement());
415     }
416 
isNull() const417     bool isNull() const
418     {
419         return m_bk.isNull();
420     }
421 
previousSibling() const422     SortItem previousSibling() const
423     {
424         return m_bk.parentGroup().previous(m_bk);
425     }
426 
nextSibling() const427     SortItem nextSibling() const
428     {
429         return m_bk.parentGroup().next(m_bk);
430     }
431 
bookmark() const432     const KBookmark &bookmark() const
433     {
434         return m_bk;
435     }
436 
437 private:
438     KBookmark m_bk;
439 };
440 
441 class SortByName
442 {
443 public:
key(const SortItem & item)444     static QString key(const SortItem &item)
445     {
446         return (item.bookmark().isGroup() ? QStringLiteral("a") : QStringLiteral("b")) + (item.bookmark().fullText().toLower());
447     }
448 };
449 
450 /* -------------------------------------- */
451 
SortCommand(KBookmarkModel * model,const QString & name,const QString & groupAddress,QUndoCommand * parent)452 SortCommand::SortCommand(KBookmarkModel *model, const QString &name, const QString &groupAddress, QUndoCommand *parent)
453     : KEBMacroCommand(name, parent)
454     , m_model(model)
455     , m_groupAddress(groupAddress)
456 {
457 }
458 
redo()459 void SortCommand::redo()
460 {
461     if (childCount() == 0) {
462         KBookmarkGroup grp = m_model->bookmarkManager()->findByAddress(m_groupAddress).toGroup();
463         Q_ASSERT(!grp.isNull());
464         SortItem firstChild(grp.first());
465         // this will call moveAfter, which will add
466         // the subcommands for moving the items
467         kInsertionSort<SortItem, SortByName, QString, SortCommand>(firstChild, (*this));
468 
469     } else {
470         // don't redo for second time on addCommand(cmd)
471         KEBMacroCommand::redo();
472     }
473 }
474 
moveAfter(const SortItem & moveMe,const SortItem & afterMe)475 void SortCommand::moveAfter(const SortItem &moveMe, const SortItem &afterMe)
476 {
477     const QString destAddress = afterMe.isNull()
478         // move as first child
479         ? KBookmark::parentAddress(moveMe.bookmark().address()) + QStringLiteral("/0")
480         // move after "afterMe"
481         : KBookmark::nextAddress(afterMe.bookmark().address());
482 
483     MoveCommand *cmd = new MoveCommand(m_model, moveMe.bookmark().address(), destAddress, QString(), this);
484     cmd->redo();
485 }
486 
undo()487 void SortCommand::undo()
488 {
489     KEBMacroCommand::undo();
490 }
491 
affectedBookmarks() const492 QString SortCommand::affectedBookmarks() const
493 {
494     return m_groupAddress;
495 }
496 
497 /* -------------------------------------- */
498 
setAsToolbar(KBookmarkModel * model,const KBookmark & bk)499 KEBMacroCommand *CmdGen::setAsToolbar(KBookmarkModel *model, const KBookmark &bk)
500 {
501     KEBMacroCommand *mcmd = new KEBMacroCommand(i18nc("(qtundo-format)", "Set as Bookmark Toolbar"));
502 
503     KBookmarkGroup oldToolbar = model->bookmarkManager()->toolbar();
504     if (!oldToolbar.isNull()) {
505         new EditCommand(model, oldToolbar.address(), -2, QStringLiteral("no"), mcmd); // toolbar
506         new EditCommand(model, oldToolbar.address(), -1, QLatin1String(""), mcmd); // icon
507     }
508 
509     new EditCommand(model, bk.address(), -2, QStringLiteral("yes"), mcmd);
510     new EditCommand(model, bk.address(), -1, QStringLiteral("bookmark-toolbar"), mcmd);
511 
512     return mcmd;
513 }
514 
insertMimeSource(KBookmarkModel * model,const QString & cmdName,const QMimeData * data,const QString & addr)515 KEBMacroCommand *CmdGen::insertMimeSource(KBookmarkModel *model, const QString &cmdName, const QMimeData *data, const QString &addr)
516 {
517     KEBMacroCommand *mcmd = new KEBMacroCommand(cmdName);
518     QString currentAddress = addr;
519     QDomDocument doc;
520     const auto bookmarks = KBookmark::List::fromMimeData(data, doc);
521     for (const KBookmark &bk : bookmarks) {
522         new CreateCommand(model, currentAddress, bk, QString(), mcmd);
523         currentAddress = KBookmark::nextAddress(currentAddress);
524     }
525     return mcmd;
526 }
527 
itemsMoved(KBookmarkModel * model,const QList<KBookmark> & items,const QString & newAddress,bool copy)528 KEBMacroCommand *CmdGen::itemsMoved(KBookmarkModel *model, const QList<KBookmark> &items, const QString &newAddress, bool copy)
529 {
530     Q_ASSERT(!copy); // always called for a move, never for a copy (that's what insertMimeSource is about)
531     Q_UNUSED(copy); // TODO: remove
532 
533     KEBMacroCommand *mcmd = new KEBMacroCommand(copy ? i18nc("(qtundo-format)", "Copy Items") : i18nc("(qtundo-format)", "Move Items"));
534     QString bkInsertAddr = newAddress;
535     for (const KBookmark &bk : items) {
536         new CreateCommand(model, bkInsertAddr, KBookmark(bk.internalElement().cloneNode(true).toElement()), bk.text(), mcmd);
537         bkInsertAddr = KBookmark::nextAddress(bkInsertAddr);
538     }
539 
540     // Do the copying, and get the updated addresses of the bookmarks to remove.
541     mcmd->redo();
542     QStringList addresses;
543     for (const KBookmark &bk : items) {
544         addresses.append(bk.address());
545     }
546     mcmd->undo();
547 
548     for (const auto &address : std::as_const(addresses)) {
549         new DeleteCommand(model, address, false, mcmd);
550     }
551 
552     return mcmd;
553 }
554