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