File indexing completed on 2022-11-29 15:26:22

0001 // -*- indent-tabs-mode:nil -*-
0002 // vim: set ts=4 sts=4 sw=4 et:
0003 /* This file is part of the KDE project
0004    Copyright (C) 2000 David Faure <faure@kde.org>
0005    Copyright (C) 2002-2003 Alexander Kellett <lypanov@kde.org>
0006 
0007    This program is free software; you can redistribute it and/or
0008    modify it under the terms of the GNU General Public
0009    License version 2 or at your option version 3 as published by
0010    the Free Software Foundation.
0011 
0012    This program is distributed in the hope that it will be useful,
0013    but WITHOUT ANY WARRANTY; without even the implied warranty of
0014    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
0015    General Public License for more details.
0016 
0017    You should have received a copy of the GNU General Public License
0018    along with this program; see the file COPYING.  If not, write to
0019    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
0020    Boston, MA 02110-1301, USA.
0021 */
0022 
0023 #include "commands.h"
0024 #include "commands_p.h"
0025 #include "kinsertionsort_p.h"
0026 #include "model.h"
0027 
0028 #include "keditbookmarks_debug.h"
0029 #include <KBookmarkManager>
0030 #include <KLocalizedString>
0031 
0032 QString KEBMacroCommand::affectedBookmarks() const
0033 {
0034     const int commandCount = childCount();
0035     if (commandCount == 0) {
0036         return QString();
0037     }
0038     // Need to use dynamic_cast here due to going cross-hierarchy, but it should never return 0.
0039     int i = 0;
0040     QString affectBook = dynamic_cast<const IKEBCommand *>(child(i))->affectedBookmarks();
0041     ++i;
0042     for (; i < commandCount; ++i) {
0043         affectBook = KBookmark::commonParent(affectBook, dynamic_cast<const IKEBCommand *>(child(i))->affectedBookmarks());
0044     }
0045     return affectBook;
0046 }
0047 
0048 DeleteManyCommand::DeleteManyCommand(KBookmarkModel *model, const QString &name, const QList<KBookmark> &bookmarks)
0049     : KEBMacroCommand(name)
0050 {
0051     QList<KBookmark>::const_iterator it, begin;
0052     begin = bookmarks.constBegin();
0053     it = bookmarks.constEnd();
0054     while (begin != it) {
0055         --it;
0056         new DeleteCommand(model, (*it).address(), false, this);
0057     }
0058 }
0059 
0060 ////
0061 
0062 CreateCommand::CreateCommand(KBookmarkModel *model, const QString &address, QUndoCommand *parent)
0063     : QUndoCommand(parent)
0064     , m_model(model)
0065     , m_to(address)
0066     , m_group(false)
0067     , m_separator(true)
0068     , m_originalBookmark(QDomElement())
0069 {
0070     setText(i18nc("(qtundo-format)", "Insert Separator"));
0071 }
0072 
0073 CreateCommand::CreateCommand(KBookmarkModel *model, const QString &address, const QString &text, const QString &iconPath, const QUrl &url, QUndoCommand *parent)
0074     : QUndoCommand(parent)
0075     , m_model(model)
0076     , m_to(address)
0077     , m_text(text)
0078     , m_iconPath(iconPath)
0079     , m_url(url)
0080     , m_group(false)
0081     , m_separator(false)
0082     , m_originalBookmark(QDomElement())
0083 {
0084     setText(i18nc("(qtundo-format)", "Create Bookmark"));
0085 }
0086 
0087 CreateCommand::CreateCommand(KBookmarkModel *model, const QString &address, const QString &text, const QString &iconPath, bool open, QUndoCommand *parent)
0088     : QUndoCommand(parent)
0089     , m_model(model)
0090     , m_to(address)
0091     , m_text(text)
0092     , m_iconPath(iconPath)
0093     , m_group(true)
0094     , m_separator(false)
0095     , m_open(open)
0096     , m_originalBookmark(QDomElement())
0097 {
0098     setText(i18nc("(qtundo-format)", "Create Folder"));
0099 }
0100 
0101 CreateCommand::CreateCommand(KBookmarkModel *model, const QString &address, const KBookmark &original, const QString &name, QUndoCommand *parent)
0102     : QUndoCommand(parent)
0103     , m_model(model)
0104     , m_to(address)
0105     , m_group(false)
0106     , m_separator(false)
0107     , m_open(false)
0108     , m_originalBookmark(original)
0109     , m_originalBookmarkDocRef(m_originalBookmark.internalElement().ownerDocument())
0110 {
0111     setText(i18nc("(qtundo-format)", "Copy %1", name));
0112 }
0113 
0114 void CreateCommand::redo()
0115 {
0116     QString parentAddress = KBookmark::parentAddress(m_to);
0117     KBookmarkGroup parentGroup = m_model->bookmarkManager()->findByAddress(parentAddress).toGroup();
0118 
0119     QString previousSibling = KBookmark::previousAddress(m_to);
0120 
0121     // qCDebug(KEDITBOOKMARKS_LOG) << "previousSibling=" << previousSibling;
0122     KBookmark prev = (previousSibling.isEmpty()) ? KBookmark(QDomElement()) : m_model->bookmarkManager()->findByAddress(previousSibling);
0123 
0124     KBookmark bk = KBookmark(QDomElement());
0125     const int pos = KBookmark::positionInParent(m_to);
0126     m_model->beginInsert(parentGroup, pos, pos);
0127 
0128     if (m_separator) {
0129         bk = parentGroup.createNewSeparator();
0130 
0131     } else if (m_group) {
0132         Q_ASSERT(!m_text.isEmpty());
0133         bk = parentGroup.createNewFolder(m_text);
0134         bk.internalElement().setAttribute(QStringLiteral("folded"), (m_open ? QLatin1String("no") : QLatin1String("yes")));
0135         if (!m_iconPath.isEmpty()) {
0136             bk.setIcon(m_iconPath);
0137         }
0138     } else if (!m_originalBookmark.isNull()) {
0139         QDomElement element = m_originalBookmark.internalElement().cloneNode().toElement();
0140         bk = KBookmark(element);
0141         parentGroup.addBookmark(bk);
0142     } else {
0143         bk = parentGroup.addBookmark(m_text, m_url, m_iconPath);
0144     }
0145 
0146     // move to right position
0147     bool ok = parentGroup.moveBookmark(bk, prev);
0148     Q_UNUSED(ok);
0149     // TODO (requires KBookmarks >= 5.25)
0150     // Q_ASSERT(ok);
0151     if (!(text().isEmpty()) && !parentAddress.isEmpty()) {
0152         // open the parent (useful if it was empty) - only for manual commands
0153         Q_ASSERT(parentGroup.internalElement().tagName() != QLatin1String("xbel"));
0154         parentGroup.internalElement().setAttribute(QStringLiteral("folded"), QStringLiteral("no"));
0155     }
0156 
0157     Q_ASSERT(bk.address() == m_to);
0158     m_model->endInsert();
0159 }
0160 
0161 QString CreateCommand::finalAddress() const
0162 {
0163     Q_ASSERT(!m_to.isEmpty());
0164     return m_to;
0165 }
0166 
0167 void CreateCommand::undo()
0168 {
0169     KBookmark bk = m_model->bookmarkManager()->findByAddress(m_to);
0170     Q_ASSERT(!bk.isNull() && !bk.parentGroup().isNull());
0171 
0172     m_model->removeBookmark(bk);
0173 }
0174 
0175 QString CreateCommand::affectedBookmarks() const
0176 {
0177     return KBookmark::parentAddress(m_to);
0178 }
0179 
0180 /* -------------------------------------- */
0181 
0182 EditCommand::EditCommand(KBookmarkModel *model, const QString &address, int col, const QString &newValue, QUndoCommand *parent)
0183     : QUndoCommand(parent)
0184     , m_model(model)
0185     , mAddress(address)
0186     , mCol(col)
0187 {
0188     qCDebug(KEDITBOOKMARKS_LOG) << address << col << newValue;
0189     if (mCol == 1) {
0190         const QUrl u(newValue);
0191         if (!(u.isEmpty() && !newValue.isEmpty())) // prevent emptied line if the currently entered url is invalid
0192             mNewValue = u.toString();
0193         else
0194             mNewValue = newValue;
0195     } else
0196         mNewValue = newValue;
0197 
0198     // -2 is "toolbar" attribute change, but that's only used internally.
0199     if (mCol == -1)
0200         setText(i18nc("(qtundo-format)", "Icon Change"));
0201     else if (mCol == 0)
0202         setText(i18nc("(qtundo-format)", "Title Change"));
0203     else if (mCol == 1)
0204         setText(i18nc("(qtundo-format)", "URL Change"));
0205     else if (mCol == 2)
0206         setText(i18nc("(qtundo-format)", "Comment Change"));
0207 }
0208 
0209 void EditCommand::redo()
0210 {
0211     KBookmark bk = m_model->bookmarkManager()->findByAddress(mAddress);
0212     if (mCol == -2) {
0213         if (mOldValue.isEmpty())
0214             mOldValue = bk.internalElement().attribute(QStringLiteral("toolbar"));
0215         bk.internalElement().setAttribute(QStringLiteral("toolbar"), mNewValue);
0216     } else if (mCol == -1) {
0217         if (mOldValue.isEmpty())
0218             mOldValue = bk.icon();
0219         bk.setIcon(mNewValue);
0220     } else if (mCol == 0) {
0221         if (mOldValue.isEmpty()) // only the first time, not when compressing changes in modify()
0222             mOldValue = bk.fullText();
0223         qCDebug(KEDITBOOKMARKS_LOG) << "mOldValue=" << mOldValue;
0224         bk.setFullText(mNewValue);
0225     } else if (mCol == 1) {
0226         if (mOldValue.isEmpty())
0227             mOldValue = bk.url().toDisplayString();
0228         const QUrl newUrl(mNewValue);
0229         if (!(newUrl.isEmpty() && !mNewValue.isEmpty())) // prevent emptied line if the currently entered url is invalid
0230             bk.setUrl(newUrl);
0231     } else if (mCol == 2) {
0232         if (mOldValue.isEmpty())
0233             mOldValue = bk.description();
0234         bk.setDescription(mNewValue);
0235     }
0236     m_model->emitDataChanged(bk);
0237 }
0238 
0239 void EditCommand::undo()
0240 {
0241     qCDebug(KEDITBOOKMARKS_LOG) << "Setting old value" << mOldValue << "in bk" << mAddress << "col" << mCol;
0242     KBookmark bk = m_model->bookmarkManager()->findByAddress(mAddress);
0243     if (mCol == -2) {
0244         bk.internalElement().setAttribute(QStringLiteral("toolbar"), mOldValue);
0245     } else if (mCol == -1) {
0246         bk.setIcon(mOldValue);
0247     } else if (mCol == 0) {
0248         bk.setFullText(mOldValue);
0249     } else if (mCol == 1) {
0250         bk.setUrl(QUrl(mOldValue));
0251     } else if (mCol == 2) {
0252         bk.setDescription(mOldValue);
0253     }
0254     m_model->emitDataChanged(bk);
0255 }
0256 
0257 void EditCommand::modify(const QString &newValue)
0258 {
0259     if (mCol == 1) {
0260         const QUrl u(newValue);
0261         if (!(u.isEmpty() && !newValue.isEmpty())) // prevent emptied line if the currently entered url is invalid
0262             mNewValue = u.toString();
0263         else
0264             mNewValue = newValue;
0265     } else
0266         mNewValue = newValue;
0267 }
0268 
0269 /* -------------------------------------- */
0270 
0271 DeleteCommand::DeleteCommand(KBookmarkModel *model, const QString &from, bool contentOnly, QUndoCommand *parent)
0272     : QUndoCommand(parent)
0273     , m_model(model)
0274     , m_from(from)
0275     , m_cmd(nullptr)
0276     , m_subCmd(nullptr)
0277     , m_contentOnly(contentOnly)
0278 {
0279     // NOTE - DeleteCommand needs no text, it is always embedded in a macrocommand
0280 }
0281 
0282 void DeleteCommand::redo()
0283 {
0284     KBookmark bk = m_model->bookmarkManager()->findByAddress(m_from);
0285     Q_ASSERT(!bk.isNull());
0286 
0287     if (m_contentOnly) {
0288         QDomElement groupRoot = bk.internalElement();
0289 
0290         QDomNode n = groupRoot.firstChild();
0291         while (!n.isNull()) {
0292             QDomElement e = n.toElement();
0293             if (!e.isNull()) {
0294                 // qCDebug(KEDITBOOKMARKS_LOG) << e.tagName();
0295             }
0296             QDomNode next = n.nextSibling();
0297             groupRoot.removeChild(n);
0298             n = next;
0299         }
0300         return;
0301     }
0302 
0303     // TODO - bug - unparsed xml is lost after undo,
0304     //              we must store it all therefore
0305 
0306     // FIXME this removes the comments, that's bad!
0307     if (!m_cmd) {
0308         if (bk.isGroup()) {
0309             m_cmd =
0310                 new CreateCommand(m_model, m_from, bk.fullText(), bk.icon(), bk.internalElement().attribute(QStringLiteral("folded")) == QLatin1String("no"));
0311             m_subCmd = deleteAll(m_model, bk.toGroup());
0312             m_subCmd->redo();
0313 
0314         } else {
0315             m_cmd = (bk.isSeparator()) ? new CreateCommand(m_model, m_from) : new CreateCommand(m_model, m_from, bk.fullText(), bk.icon(), bk.url());
0316         }
0317     }
0318     m_cmd->undo();
0319 }
0320 
0321 void DeleteCommand::undo()
0322 {
0323     // qCDebug(KEDITBOOKMARKS_LOG) << "DeleteCommand::undo " << m_from;
0324 
0325     if (m_contentOnly) {
0326         // TODO - recover saved metadata
0327         return;
0328     }
0329 
0330     m_cmd->redo();
0331 
0332     if (m_subCmd) {
0333         m_subCmd->undo();
0334     }
0335 }
0336 
0337 QString DeleteCommand::affectedBookmarks() const
0338 {
0339     return KBookmark::parentAddress(m_from);
0340 }
0341 
0342 KEBMacroCommand *DeleteCommand::deleteAll(KBookmarkModel *model, const KBookmarkGroup &parentGroup)
0343 {
0344     KEBMacroCommand *cmd = new KEBMacroCommand(QString());
0345     QStringList lstToDelete;
0346     // we need to delete from the end, to avoid index shifting
0347     for (KBookmark bk = parentGroup.first(); !bk.isNull(); bk = parentGroup.next(bk))
0348         lstToDelete.prepend(bk.address());
0349     for (QStringList::const_iterator it = lstToDelete.constBegin(); it != lstToDelete.constEnd(); ++it) {
0350         new DeleteCommand(model, (*it), false, cmd);
0351     }
0352     return cmd;
0353 }
0354 
0355 /* -------------------------------------- */
0356 
0357 MoveCommand::MoveCommand(KBookmarkModel *model, const QString &from, const QString &to, const QString &name, QUndoCommand *parent)
0358     : QUndoCommand(parent)
0359     , m_model(model)
0360     , m_from(from)
0361     , m_to(to)
0362     , m_cc(nullptr)
0363     , m_dc(nullptr)
0364 {
0365     setText(i18nc("(qtundo-format)", "Move %1", name));
0366 }
0367 
0368 void MoveCommand::redo()
0369 {
0370     // qCDebug(KEDITBOOKMARKS_LOG) << "Moving from=" << m_from << "to=" << m_to;
0371 
0372     KBookmark fromBk = m_model->bookmarkManager()->findByAddress(m_from);
0373     Q_ASSERT(fromBk.address() == m_from);
0374 
0375     // qCDebug(KEDITBOOKMARKS_LOG) << "  1) creating" << m_to;
0376     m_cc = new CreateCommand(m_model, m_to, fromBk, QString());
0377     m_cc->redo();
0378 
0379     // qCDebug(KEDITBOOKMARKS_LOG) << "  2) deleting" << fromBk.address();
0380     m_dc = new DeleteCommand(m_model, fromBk.address());
0381     m_dc->redo();
0382 }
0383 
0384 QString MoveCommand::finalAddress() const
0385 {
0386     Q_ASSERT(!m_to.isEmpty());
0387     return m_to;
0388 }
0389 
0390 void MoveCommand::undo()
0391 {
0392     m_dc->undo();
0393     m_cc->undo();
0394 }
0395 
0396 QString MoveCommand::affectedBookmarks() const
0397 {
0398     return KBookmark::commonParent(KBookmark::parentAddress(m_from), KBookmark::parentAddress(m_to));
0399 }
0400 
0401 /* -------------------------------------- */
0402 
0403 class SortItem
0404 {
0405 public:
0406     SortItem(const KBookmark &bk)
0407         : m_bk(bk)
0408     {
0409     }
0410 
0411     bool operator==(const SortItem &s)
0412     {
0413         return (m_bk.internalElement() == s.m_bk.internalElement());
0414     }
0415 
0416     bool isNull() const
0417     {
0418         return m_bk.isNull();
0419     }
0420 
0421     SortItem previousSibling() const
0422     {
0423         return m_bk.parentGroup().previous(m_bk);
0424     }
0425 
0426     SortItem nextSibling() const
0427     {
0428         return m_bk.parentGroup().next(m_bk);
0429     }
0430 
0431     const KBookmark &bookmark() const
0432     {
0433         return m_bk;
0434     }
0435 
0436 private:
0437     KBookmark m_bk;
0438 };
0439 
0440 class SortByName
0441 {
0442 public:
0443     static QString key(const SortItem &item)
0444     {
0445         return (item.bookmark().isGroup() ? QStringLiteral("a") : QStringLiteral("b")) + (item.bookmark().fullText().toLower());
0446     }
0447 };
0448 
0449 /* -------------------------------------- */
0450 
0451 SortCommand::SortCommand(KBookmarkModel *model, const QString &name, const QString &groupAddress, QUndoCommand *parent)
0452     : KEBMacroCommand(name, parent)
0453     , m_model(model)
0454     , m_groupAddress(groupAddress)
0455 {
0456 }
0457 
0458 void SortCommand::redo()
0459 {
0460     if (childCount() == 0) {
0461         KBookmarkGroup grp = m_model->bookmarkManager()->findByAddress(m_groupAddress).toGroup();
0462         Q_ASSERT(!grp.isNull());
0463         SortItem firstChild(grp.first());
0464         // this will call moveAfter, which will add
0465         // the subcommands for moving the items
0466         kInsertionSort<SortItem, SortByName, QString, SortCommand>(firstChild, (*this));
0467 
0468     } else {
0469         // don't redo for second time on addCommand(cmd)
0470         KEBMacroCommand::redo();
0471     }
0472 }
0473 
0474 void SortCommand::moveAfter(const SortItem &moveMe, const SortItem &afterMe)
0475 {
0476     const QString destAddress = afterMe.isNull()
0477         // move as first child
0478         ? KBookmark::parentAddress(moveMe.bookmark().address()) + QStringLiteral("/0")
0479         // move after "afterMe"
0480         : KBookmark::nextAddress(afterMe.bookmark().address());
0481 
0482     MoveCommand *cmd = new MoveCommand(m_model, moveMe.bookmark().address(), destAddress, QString(), this);
0483     cmd->redo();
0484 }
0485 
0486 void SortCommand::undo()
0487 {
0488     KEBMacroCommand::undo();
0489 }
0490 
0491 QString SortCommand::affectedBookmarks() const
0492 {
0493     return m_groupAddress;
0494 }
0495 
0496 /* -------------------------------------- */
0497 
0498 KEBMacroCommand *CmdGen::setAsToolbar(KBookmarkModel *model, const KBookmark &bk)
0499 {
0500     KEBMacroCommand *mcmd = new KEBMacroCommand(i18nc("(qtundo-format)", "Set as Bookmark Toolbar"));
0501 
0502     KBookmarkGroup oldToolbar = model->bookmarkManager()->toolbar();
0503     if (!oldToolbar.isNull()) {
0504         new EditCommand(model, oldToolbar.address(), -2, QStringLiteral("no"), mcmd); // toolbar
0505         new EditCommand(model, oldToolbar.address(), -1, QLatin1String(""), mcmd); // icon
0506     }
0507 
0508     new EditCommand(model, bk.address(), -2, QStringLiteral("yes"), mcmd);
0509     new EditCommand(model, bk.address(), -1, QStringLiteral("bookmark-toolbar"), mcmd);
0510 
0511     return mcmd;
0512 }
0513 
0514 KEBMacroCommand *CmdGen::insertMimeSource(KBookmarkModel *model, const QString &cmdName, const QMimeData *data, const QString &addr)
0515 {
0516     KEBMacroCommand *mcmd = new KEBMacroCommand(cmdName);
0517     QString currentAddress = addr;
0518     QDomDocument doc;
0519     const auto bookmarks = KBookmark::List::fromMimeData(data, doc);
0520     for (const KBookmark &bk : bookmarks) {
0521         new CreateCommand(model, currentAddress, bk, QString(), mcmd);
0522         currentAddress = KBookmark::nextAddress(currentAddress);
0523     }
0524     return mcmd;
0525 }
0526 
0527 KEBMacroCommand *CmdGen::itemsMoved(KBookmarkModel *model, const QList<KBookmark> &items, const QString &newAddress, bool copy)
0528 {
0529     Q_ASSERT(!copy); // always called for a move, never for a copy (that's what insertMimeSource is about)
0530     Q_UNUSED(copy); // TODO: remove
0531 
0532     KEBMacroCommand *mcmd = new KEBMacroCommand(copy ? i18nc("(qtundo-format)", "Copy Items") : i18nc("(qtundo-format)", "Move Items"));
0533     QString bkInsertAddr = newAddress;
0534     for (const KBookmark &bk : items) {
0535         new CreateCommand(model, bkInsertAddr, KBookmark(bk.internalElement().cloneNode(true).toElement()), bk.text(), mcmd);
0536         bkInsertAddr = KBookmark::nextAddress(bkInsertAddr);
0537     }
0538 
0539     // Do the copying, and get the updated addresses of the bookmarks to remove.
0540     mcmd->redo();
0541     QStringList addresses;
0542     for (const KBookmark &bk : items) {
0543         addresses.append(bk.address());
0544     }
0545     mcmd->undo();
0546 
0547     for (const auto &address : std::as_const(addresses)) {
0548         new DeleteCommand(model, address, false, mcmd);
0549     }
0550 
0551     return mcmd;
0552 }