File indexing completed on 2024-05-12 05:10:42

0001 /*
0002    SPDX-FileCopyrightText: 2012 Sérgio Martins <iamsergio@gmail.com>
0003 
0004    SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "calendarclipboard_p.h"
0008 #include <KCalUtils/DndFactory>
0009 #include <KCalUtils/ICalDrag>
0010 
0011 #include <KLocalizedString>
0012 #include <KMessageBox>
0013 
0014 #include <QApplication>
0015 #include <QClipboard>
0016 
0017 using namespace Akonadi;
0018 
0019 CalendarClipboardPrivate::CalendarClipboardPrivate(const Akonadi::CalendarBase::Ptr &calendar, Akonadi::IncidenceChanger *changer, CalendarClipboard *qq)
0020     : QObject()
0021     , m_calendar(calendar)
0022     , m_changer(changer)
0023     , q(qq)
0024 {
0025     Q_ASSERT(m_calendar);
0026     if (!m_changer) {
0027         m_changer = new Akonadi::IncidenceChanger(this);
0028         m_changer->setHistoryEnabled(false);
0029         m_changer->setGroupwareCommunication(false);
0030     }
0031 
0032     m_dndfactory = new KCalUtils::DndFactory(m_calendar);
0033 
0034     connect(m_changer, &IncidenceChanger::modifyFinished, this, &CalendarClipboardPrivate::slotModifyFinished);
0035 
0036     connect(m_changer, &IncidenceChanger::deleteFinished, this, &CalendarClipboardPrivate::slotDeleteFinished);
0037 }
0038 
0039 CalendarClipboardPrivate::~CalendarClipboardPrivate()
0040 {
0041     delete m_dndfactory;
0042 }
0043 
0044 void CalendarClipboardPrivate::getIncidenceHierarchy(const KCalendarCore::Incidence::Ptr &incidence, QStringList &uids)
0045 {
0046     // protection against looping hierarchies
0047     if (incidence && !uids.contains(incidence->uid())) {
0048         const KCalendarCore::Incidence::List immediateChildren = m_calendar->childIncidences(incidence->uid());
0049 
0050         for (const KCalendarCore::Incidence::Ptr &child : immediateChildren) {
0051             getIncidenceHierarchy(child, uids);
0052         }
0053         uids.append(incidence->uid());
0054     }
0055 }
0056 
0057 void CalendarClipboardPrivate::cut(const KCalendarCore::Incidence::List &incidences)
0058 {
0059     const bool result = m_dndfactory->copyIncidences(incidences);
0060     m_pendingChangeIds.clear();
0061     // Note: Don't use DndFactory::cutIncidences(), it doesn't use IncidenceChanger for deletion
0062     // we would loose async error handling and redo/undo features
0063     if (result) {
0064         Akonadi::Item::List items = m_calendar->itemList(incidences);
0065         const int result = m_changer->deleteIncidences(items);
0066         if (result == -1) {
0067             Q_EMIT q->cutFinished(/**success=*/false, i18n("Error performing deletion."));
0068         } else {
0069             m_pendingChangeIds << result;
0070         }
0071     } else {
0072         Q_EMIT q->cutFinished(/**success=*/false, i18n("Error performing copy."));
0073     }
0074 }
0075 
0076 void CalendarClipboardPrivate::cut(const KCalendarCore::Incidence::Ptr &incidence)
0077 {
0078     KCalendarCore::Incidence::List incidences;
0079     incidences << incidence;
0080     cut(incidences);
0081 }
0082 
0083 void CalendarClipboardPrivate::makeChildsIndependent(const KCalendarCore::Incidence::Ptr &incidence)
0084 {
0085     Q_ASSERT(incidence);
0086     const KCalendarCore::Incidence::List children = m_calendar->childIncidences(incidence->uid());
0087 
0088     if (children.isEmpty()) {
0089         cut(incidence);
0090     } else {
0091         m_pendingChangeIds.clear();
0092         m_abortCurrentOperation = false;
0093         for (const KCalendarCore::Incidence::Ptr &child : children) {
0094             Akonadi::Item childItem = m_calendar->item(incidence);
0095             if (!childItem.isValid()) {
0096                 Q_EMIT q->cutFinished(/**success=*/false, i18n("Can't find item: %1", childItem.id()));
0097                 return;
0098             }
0099 
0100             KCalendarCore::Incidence::Ptr newIncidence(child->clone());
0101             newIncidence->setRelatedTo(QString());
0102             childItem.setPayload<KCalendarCore::Incidence::Ptr>(newIncidence);
0103             const int changeId = m_changer->modifyIncidence(childItem, /*originalPayload*/ child);
0104             if (changeId == -1) {
0105                 m_abortCurrentOperation = true;
0106                 break;
0107             } else {
0108                 m_pendingChangeIds << changeId;
0109             }
0110         }
0111         if (m_pendingChangeIds.isEmpty() && m_abortCurrentOperation) {
0112             Q_EMIT q->cutFinished(/**success=*/false, i18n("Error while removing relations."));
0113         } // if m_pendingChangeIds isn't empty, we wait for all jobs to finish first.
0114     }
0115 }
0116 
0117 void CalendarClipboardPrivate::slotModifyFinished(int changeId, const Akonadi::Item &item, IncidenceChanger::ResultCode resultCode, const QString &errorMessage)
0118 {
0119     if (!m_pendingChangeIds.contains(changeId)) {
0120         return; // Not ours, someone else deleted something, not our business.
0121     }
0122 
0123     m_pendingChangeIds.remove(changeId);
0124     const bool isLastChange = m_pendingChangeIds.isEmpty();
0125 
0126     Q_UNUSED(item)
0127     Q_UNUSED(errorMessage)
0128     if (m_abortCurrentOperation && isLastChange) {
0129         Q_EMIT q->cutFinished(/**success=*/false, i18n("Error while removing relations."));
0130     } else if (!m_abortCurrentOperation) {
0131         if (resultCode == IncidenceChanger::ResultCodeSuccess) {
0132             if (isLastChange) {
0133                 // All children are unparented, lets cut.
0134                 Q_ASSERT(item.isValid() && item.hasPayload());
0135                 cut(item.payload<KCalendarCore::Incidence::Ptr>());
0136             }
0137         } else {
0138             m_abortCurrentOperation = true;
0139         }
0140     }
0141 }
0142 
0143 void CalendarClipboardPrivate::slotDeleteFinished(int changeId,
0144                                                   const QList<Akonadi::Item::Id> &ids,
0145                                                   Akonadi::IncidenceChanger::ResultCode result,
0146                                                   const QString &errorMessage)
0147 {
0148     if (!m_pendingChangeIds.contains(changeId)) {
0149         return; // Not ours, someone else deleted something, not our business.
0150     }
0151 
0152     m_pendingChangeIds.remove(changeId);
0153 
0154     Q_UNUSED(ids)
0155     if (result == IncidenceChanger::ResultCodeSuccess) {
0156         Q_EMIT q->cutFinished(/**success=*/true, QString());
0157     } else {
0158         Q_EMIT q->cutFinished(/**success=*/false, i18n("Error while deleting incidences: %1", errorMessage));
0159     }
0160 }
0161 
0162 CalendarClipboard::CalendarClipboard(const Akonadi::CalendarBase::Ptr &calendar, Akonadi::IncidenceChanger *changer, QObject *parent)
0163     : QObject(parent)
0164     , d(new CalendarClipboardPrivate(calendar, changer, this))
0165 {
0166 }
0167 
0168 CalendarClipboard::~CalendarClipboard() = default;
0169 
0170 void CalendarClipboard::cutIncidence(const KCalendarCore::Incidence::Ptr &incidence, CalendarClipboard::Mode mode)
0171 {
0172     const bool hasChildren = !d->m_calendar->childIncidences(incidence->uid()).isEmpty();
0173     if (mode == AskMode && hasChildren) {
0174         const int km = KMessageBox::questionTwoActionsCancel(nullptr,
0175                                                              i18n("The item \"%1\" has sub-to-dos. "
0176                                                                   "Do you want to cut just this item and "
0177                                                                   "make all its sub-to-dos independent, or "
0178                                                                   "cut the to-do with all its sub-to-dos?",
0179                                                                   incidence->summary()),
0180                                                              i18nc("@title:window", "KOrganizer Confirmation"),
0181                                                              KGuiItem(i18n("Cut Only This")),
0182                                                              KGuiItem(i18n("Cut All")));
0183 
0184         if (km == KMessageBox::Cancel) {
0185             Q_EMIT cutFinished(/*success=*/true, QString());
0186             return;
0187         }
0188         mode = km == KMessageBox::ButtonCode::PrimaryAction ? SingleMode : RecursiveMode;
0189     } else if (mode == AskMode) {
0190         mode = SingleMode; // Doesn't have children, don't ask
0191     }
0192 
0193     if (mode == SingleMode) {
0194         d->makeChildsIndependent(incidence); // Will call d->cut(incidence) when it finishes.
0195     } else {
0196         QStringList uids;
0197         d->getIncidenceHierarchy(incidence, uids);
0198         Q_ASSERT(!uids.isEmpty());
0199         KCalendarCore::Incidence::List incidencesToCut;
0200         for (const QString &uid : std::as_const(uids)) {
0201             KCalendarCore::Incidence::Ptr child = d->m_calendar->incidence(uid);
0202             if (child) {
0203                 incidencesToCut << child;
0204             }
0205         }
0206         d->cut(incidencesToCut);
0207     }
0208 }
0209 
0210 bool CalendarClipboard::copyIncidence(const KCalendarCore::Incidence::Ptr &incidence, CalendarClipboard::Mode mode)
0211 {
0212     const bool hasChildren = !d->m_calendar->childIncidences(incidence->uid()).isEmpty();
0213     if (mode == AskMode && hasChildren) {
0214         const int km = KMessageBox::questionTwoActionsCancel(nullptr,
0215                                                              i18n("The item \"%1\" has sub-to-dos. "
0216                                                                   "Do you want to copy just this item or "
0217                                                                   "copy the to-do with all its sub-to-dos?",
0218                                                                   incidence->summary()),
0219                                                              i18nc("@title:window", "KOrganizer Confirmation"),
0220                                                              KGuiItem(i18n("Copy Only This")),
0221                                                              KGuiItem(i18n("Copy All")));
0222         if (km == KMessageBox::Cancel) {
0223             return true;
0224         }
0225         mode = km == KMessageBox::ButtonCode::PrimaryAction ? SingleMode : RecursiveMode;
0226     } else if (mode == AskMode) {
0227         mode = SingleMode; // Doesn't have children, don't ask
0228     }
0229 
0230     KCalendarCore::Incidence::List incidencesToCopy;
0231     if (mode == SingleMode) {
0232         incidencesToCopy << incidence;
0233     } else {
0234         QStringList uids;
0235         d->getIncidenceHierarchy(incidence, uids);
0236         Q_ASSERT(!uids.isEmpty());
0237         for (const QString &uid : std::as_const(uids)) {
0238             KCalendarCore::Incidence::Ptr child = d->m_calendar->incidence(uid);
0239             if (child) {
0240                 incidencesToCopy << child;
0241             }
0242         }
0243     }
0244 
0245     return d->m_dndfactory->copyIncidences(incidencesToCopy);
0246 }
0247 
0248 bool CalendarClipboard::pasteAvailable() const
0249 {
0250     return KCalUtils::ICalDrag::canDecode(QApplication::clipboard()->mimeData());
0251 }
0252 
0253 #include "moc_calendarclipboard.cpp"
0254 
0255 #include "moc_calendarclipboard_p.cpp"