File indexing completed on 2025-01-05 04:49:46

0001 /*
0002    SPDX-FileCopyrightText: 2014-2024 Laurent Montel <montel@kde.org>
0003 
0004    SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "todoedit.h"
0008 #include "createtodoplugin_debug.h"
0009 #include <CalendarSupport/KCalPrefs>
0010 #include <CalendarSupport/Utils>
0011 #include <messageviewer/globalsettings_messageviewer.h>
0012 
0013 #include <KLocalizedString>
0014 #include <QIcon>
0015 #include <QLineEdit>
0016 
0017 #include <KMessageWidget>
0018 #include <QEvent>
0019 #include <QHBoxLayout>
0020 #include <QKeyEvent>
0021 #include <QLabel>
0022 #include <QPushButton>
0023 
0024 #include <Akonadi/CollectionComboBox>
0025 #include <Akonadi/SpecialMailCollections>
0026 #include <IncidenceEditor/IncidenceDialog>
0027 #include <IncidenceEditor/IncidenceDialogFactory>
0028 #include <KGuiItem>
0029 #include <KStandardGuiItem>
0030 
0031 namespace MessageViewer
0032 {
0033 QAbstractItemModel *_k_todoEditStubModel = nullptr;
0034 }
0035 
0036 using namespace MessageViewer;
0037 
0038 TodoEdit::TodoEdit(QWidget *parent)
0039     : QWidget(parent)
0040     , mNoteEdit(new QLineEdit(this))
0041     , mMsgWidget(new KMessageWidget(this))
0042     , mSaveButton(new QPushButton(QIcon::fromTheme(QStringLiteral("task-new")), i18n("&Save"), this))
0043     , mOpenEditorButton(new QPushButton(i18n("Open &Editor..."), this))
0044 {
0045     auto vbox = new QVBoxLayout(this);
0046     vbox->setContentsMargins(5, 5, 5, 5);
0047     vbox->setSpacing(2);
0048 
0049     mMsgWidget->setCloseButtonVisible(true);
0050     mMsgWidget->setMessageType(KMessageWidget::Positive);
0051     mMsgWidget->setObjectName(QLatin1StringView("msgwidget"));
0052     mMsgWidget->setWordWrap(true);
0053     mMsgWidget->setVisible(false);
0054     vbox->addWidget(mMsgWidget);
0055 
0056     auto hbox = new QHBoxLayout;
0057     hbox->setContentsMargins({});
0058     hbox->setSpacing(2);
0059     vbox->addLayout(hbox);
0060 
0061     auto lab = new QLabel(i18n("To-do:"), this);
0062     hbox->addWidget(lab);
0063 
0064     mNoteEdit->setClearButtonEnabled(true);
0065     mNoteEdit->setObjectName(QLatin1StringView("noteedit"));
0066     mNoteEdit->setFocus();
0067     connect(mNoteEdit, &QLineEdit::textChanged, this, &TodoEdit::slotTextEdited);
0068     connect(mNoteEdit, &QLineEdit::returnPressed, this, &TodoEdit::slotReturnPressed);
0069     hbox->addWidget(mNoteEdit, 1);
0070 
0071     hbox->addSpacing(5);
0072 
0073     mCollectionCombobox = new Akonadi::CollectionComboBox(_k_todoEditStubModel, this);
0074     mCollectionCombobox->setAccessRightsFilter(Akonadi::Collection::CanCreateItem);
0075     mCollectionCombobox->setMinimumWidth(250);
0076     mCollectionCombobox->setMimeTypeFilter(QStringList() << KCalendarCore::Todo::todoMimeType());
0077     mCollectionCombobox->setObjectName(QLatin1StringView("akonadicombobox"));
0078     connect(mCollectionCombobox->model(), &QAbstractItemModel::rowsInserted, this, &TodoEdit::comboboxRowInserted);
0079 #ifndef QT_NO_ACCESSIBILITY
0080     mCollectionCombobox->setAccessibleDescription(i18n("Todo list where the new task will be stored."));
0081 #endif
0082     mCollectionCombobox->setToolTip(i18n("Todo list where the new task will be stored"));
0083     connect(mCollectionCombobox, &Akonadi::CollectionComboBox::currentIndexChanged, this, &TodoEdit::slotCollectionChanged);
0084     connect(mCollectionCombobox, &Akonadi::CollectionComboBox::activated, this, &TodoEdit::slotCollectionChanged);
0085     hbox->addWidget(mCollectionCombobox);
0086 
0087     hbox = new QHBoxLayout;
0088     hbox->setContentsMargins({});
0089     hbox->setSpacing(2);
0090     vbox->addLayout(hbox);
0091 
0092     hbox->addStretch(1);
0093     mSaveButton->setObjectName(QLatin1StringView("save-button"));
0094     mSaveButton->setEnabled(false);
0095 #ifndef QT_NO_ACCESSIBILITY
0096     mSaveButton->setAccessibleDescription(i18n("Create new todo and close this widget."));
0097 #endif
0098     connect(mSaveButton, &QPushButton::clicked, this, &TodoEdit::slotReturnPressed);
0099     hbox->addWidget(mSaveButton);
0100 
0101     mOpenEditorButton->setObjectName(QLatin1StringView("open-editor-button"));
0102 #ifndef QT_NO_ACCESSIBILITY
0103     mOpenEditorButton->setAccessibleDescription(i18n("Open todo editor, where more details can be changed."));
0104 #endif
0105     mOpenEditorButton->setEnabled(false);
0106     connect(mOpenEditorButton, &QPushButton::clicked, this, &TodoEdit::slotOpenEditor);
0107     hbox->addWidget(mOpenEditorButton);
0108 
0109     auto btn = new QPushButton(this);
0110     KGuiItem::assign(btn, KStandardGuiItem::cancel());
0111     btn->setObjectName(QLatin1StringView("close-button"));
0112 #ifndef QT_NO_ACCESSIBILITY
0113     btn->setAccessibleDescription(i18n("Close the widget for creating new todos."));
0114 #endif
0115     connect(btn, &QPushButton::clicked, this, &TodoEdit::slotCloseWidget);
0116     hbox->addWidget(btn);
0117 
0118     readConfig();
0119     setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed));
0120     mCollectionCombobox->installEventFilter(this);
0121     installEventFilter(this);
0122 }
0123 
0124 TodoEdit::~TodoEdit()
0125 {
0126     writeConfig();
0127 }
0128 
0129 void TodoEdit::comboboxRowInserted()
0130 {
0131     updateButtons(mNoteEdit->text());
0132 }
0133 
0134 void TodoEdit::updateButtons(const QString &subject)
0135 {
0136     const bool subjectIsNotEmpty = !subject.trimmed().isEmpty();
0137     const bool collectionComboboxEmpty = (mCollectionCombobox->count() < 1);
0138     mSaveButton->setEnabled(subjectIsNotEmpty && !collectionComboboxEmpty);
0139     mOpenEditorButton->setEnabled(subjectIsNotEmpty && !collectionComboboxEmpty);
0140 }
0141 
0142 void TodoEdit::showToDoWidget()
0143 {
0144     const KMime::Headers::Subject *const subject = mMessage ? mMessage->subject(false) : nullptr;
0145     if (subject) {
0146         bool isSentFolder = false;
0147         if (mCurrentCollection.isValid()) {
0148             isSentFolder = (Akonadi::SpecialMailCollections::self()->defaultCollection(Akonadi::SpecialMailCollections::SentMail) == mCurrentCollection);
0149         }
0150         mNoteEdit->setText(isSentFolder ? i18n("Check I received a reply about \"%1\"", subject->asUnicodeString())
0151                                         : i18n("Reply to \"%1\"", subject->asUnicodeString()));
0152         mNoteEdit->selectAll();
0153         mNoteEdit->setFocus();
0154     } else {
0155         mNoteEdit->clear();
0156     }
0157     mNoteEdit->setFocus();
0158     show();
0159 }
0160 
0161 void TodoEdit::setCurrentCollection(const Akonadi::Collection &col)
0162 {
0163     mCurrentCollection = col;
0164 }
0165 
0166 void TodoEdit::writeConfig()
0167 {
0168     const Akonadi::Collection col = mCollectionCombobox->currentCollection();
0169     // col might not be valid if the collection wasn't found yet (the combo is async), skip saving in that case
0170     if (col.isValid() && col.id() != MessageViewer::MessageViewerSettingsBase::self()->lastSelectedFolder()) {
0171         MessageViewer::MessageViewerSettingsBase::self()->setLastSelectedFolder(col.id());
0172         MessageViewer::MessageViewerSettingsBase::self()->save();
0173     }
0174 }
0175 
0176 void TodoEdit::readConfig()
0177 {
0178     const qint64 id = MessageViewer::MessageViewerSettingsBase::self()->lastSelectedFolder();
0179     if (id != -1) {
0180         mCollectionCombobox->setDefaultCollection(Akonadi::Collection(id));
0181     }
0182 }
0183 
0184 Akonadi::Collection TodoEdit::collection() const
0185 {
0186     return mCollection;
0187 }
0188 
0189 void TodoEdit::slotCollectionChanged(int /*index*/)
0190 {
0191     setCollection(mCollectionCombobox->currentCollection());
0192 }
0193 
0194 void TodoEdit::setCollection(const Akonadi::Collection &value)
0195 {
0196     if (mCollection != value) {
0197         mCollection = value;
0198         Q_EMIT collectionChanged(mCollection);
0199     }
0200 }
0201 
0202 KMime::Message::Ptr TodoEdit::message() const
0203 {
0204     return mMessage;
0205 }
0206 
0207 void TodoEdit::setMessage(const KMime::Message::Ptr &value)
0208 {
0209     if (mMessage != value) {
0210         mMessage = value;
0211         Q_EMIT messageChanged(mMessage);
0212     }
0213 }
0214 
0215 void TodoEdit::slotCloseWidget()
0216 {
0217     if (isVisible()) {
0218         writeConfig();
0219         mNoteEdit->clear();
0220         mMessage = KMime::Message::Ptr();
0221         mMsgWidget->hide();
0222         hide();
0223     }
0224 }
0225 
0226 void TodoEdit::slotReturnPressed()
0227 {
0228     if (!mMessage) {
0229         qCDebug(CREATETODOPLUGIN_LOG) << " Message is null";
0230         return;
0231     }
0232     const Akonadi::Collection collection = mCollectionCombobox->currentCollection();
0233     if (!collection.isValid()) {
0234         qCDebug(CREATETODOPLUGIN_LOG) << " Collection is not valid";
0235         return;
0236     }
0237 
0238     if (!mNoteEdit->text().trimmed().isEmpty()) {
0239         mMsgWidget->setText(i18nc("%1 is summary of the todo, %2 is name of the folder in which it is stored",
0240                                   "New todo '%1' was added to task list '%2'",
0241                                   mNoteEdit->text(),
0242                                   collection.displayName()));
0243         KCalendarCore::Todo::Ptr todo = createTodoItem();
0244 
0245         // We don't hide the widget here, so that multiple todo's can be added
0246         Q_EMIT createTodo(todo, collection);
0247 
0248         mMsgWidget->animatedShow();
0249     }
0250 }
0251 
0252 KCalendarCore::Todo::Ptr TodoEdit::createTodoItem()
0253 {
0254     if (mMessage) {
0255         KCalendarCore::Todo::Ptr todo(new KCalendarCore::Todo);
0256         todo->setSummary(mNoteEdit->text());
0257         KCalendarCore::Attachment attachment(mMessage->encodedContent().toBase64(), KMime::Message::mimeType());
0258         const KMime::Headers::Subject *const subject = mMessage->subject(false);
0259         if (subject) {
0260             attachment.setLabel(subject->asUnicodeString());
0261         }
0262         if (CalendarSupport::KCalPrefs::instance()->defaultTodoReminders()) {
0263             KCalendarCore::Alarm::Ptr alm = todo->newAlarm();
0264             CalendarSupport::createAlarmReminder(alm, todo->type());
0265         }
0266 
0267         todo->addAttachment(attachment);
0268         return todo;
0269     }
0270     return {};
0271 }
0272 
0273 bool TodoEdit::eventFilter(QObject *object, QEvent *e)
0274 {
0275     // Close the bar when pressing Escape.
0276     // Not using a QShortcut for this because it could conflict with
0277     // window-global actions (e.g. Emil Sedgh binds Esc to "close tab").
0278     // With a shortcut override we can catch this before it gets to kactions.
0279     const bool shortCutOverride = (e->type() == QEvent::ShortcutOverride);
0280     if (shortCutOverride || e->type() == QEvent::KeyPress) {
0281         auto kev = static_cast<QKeyEvent *>(e);
0282         if (kev->key() == Qt::Key_Escape) {
0283             e->accept();
0284             slotCloseWidget();
0285             return true;
0286         } else if (kev->key() == Qt::Key_Enter || kev->key() == Qt::Key_Return || kev->key() == Qt::Key_Space) {
0287             e->accept();
0288             if (shortCutOverride) {
0289                 return true;
0290             }
0291             if (object == mCollectionCombobox) {
0292                 mCollectionCombobox->showPopup();
0293                 return true;
0294             }
0295         }
0296     }
0297     return QWidget::eventFilter(object, e);
0298 }
0299 
0300 void TodoEdit::slotOpenEditor()
0301 {
0302     KCalendarCore::Todo::Ptr event = createTodoItem();
0303     if (event) {
0304         Akonadi::Item item;
0305         item.setPayload<KCalendarCore::Todo::Ptr>(event);
0306         item.setMimeType(KCalendarCore::Todo::todoMimeType());
0307 
0308         IncidenceEditorNG::IncidenceDialog *dlg =
0309             IncidenceEditorNG::IncidenceDialogFactory::create(true, KCalendarCore::IncidenceBase::TypeTodo, nullptr, this);
0310         dlg->selectCollection(mCollectionCombobox->currentCollection());
0311         connect(dlg, &IncidenceEditorNG::IncidenceDialog::finished, this, &TodoEdit::slotCloseWidget);
0312         dlg->load(item);
0313         dlg->open();
0314     }
0315 }
0316 
0317 void TodoEdit::slotTextEdited(const QString &subject)
0318 {
0319     updateButtons(subject);
0320     if (mMsgWidget->isVisible()) {
0321         mMsgWidget->hide();
0322     }
0323 }
0324 
0325 #include "moc_todoedit.cpp"