File indexing completed on 2024-05-19 05:19:20

0001 /*
0002     This file is part of KJots.
0003 
0004     SPDX-FileCopyrightText: 1997 Christoph Neerfeld <Christoph.Neerfeld@home.ivm.de>
0005                   2002, 2003 Aaron J. Seigo <aseigo@kde.org>
0006                   2003 Stanislav Kljuhhin <crz@hot.ee>
0007                   2005-2006 Jaison Lee <lee.jaison@gmail.com>
0008                   2007-2008 Stephen Kelly <steveire@gmail.com>
0009                   2020 Igor Poboiko <igor.poboiko@gmail.com>
0010 
0011     SPDX-License-Identifier: GPL-2.0-or-later
0012 */
0013 
0014 //Own Header
0015 #include "kjotsedit.h"
0016 
0017 #include <QAction>
0018 #include <QApplication>
0019 #include <QClipboard>
0020 #include <QMenu>
0021 #include <QMimeData>
0022 #include <QTextCursor>
0023 #include <QTextDocumentFragment>
0024 #include <QToolTip>
0025 #include <QUrl>
0026 #include <QAbstractItemModel>
0027 
0028 #include <KActionCollection>
0029 #include <KStandardGuiItem>
0030 #include <KLocalizedString>
0031 #include <KMime/Message>
0032 #include <KPIMTextEdit/RichTextComposerControler>
0033 #include <KPIMTextEdit/RichTextComposerActions>
0034 #include <KPIMTextEdit/RichTextComposerImages>
0035 
0036 #include <akonadi_version.h>
0037 #include <Akonadi/Item>
0038 
0039 #include "kjotslinkdialog.h"
0040 #include "kjotsmodel.h"
0041 #include "noteshared/notelockattribute.h"
0042 
0043 Q_DECLARE_METATYPE(QTextCursor)
0044 Q_DECLARE_METATYPE(KPIMTextEdit::ImageList)
0045 
0046 using namespace Akonadi;
0047 using namespace KPIMTextEdit;
0048 
0049 class Q_DECL_HIDDEN KJotsEdit::Private {
0050 public:
0051     Private() = default;
0052     ~Private() = default;
0053 
0054     QPersistentModelIndex index;
0055     QAbstractItemModel *model = nullptr;
0056 
0057     QAction *action_copy_into_title = nullptr;
0058     QAction *action_manage_link = nullptr;
0059     QAction *action_auto_bullet = nullptr;
0060     QAction *action_auto_decimal = nullptr;
0061     QAction *action_insert_date = nullptr;
0062     // KStandardActions
0063     QAction *action_save = nullptr;
0064     QAction *action_cut = nullptr;
0065     QAction *action_paste = nullptr;
0066     QAction *action_undo = nullptr;
0067     QAction *action_redo = nullptr;
0068 
0069     QVector<QAction *> editorActionList;
0070 };
0071 
0072 KJotsEdit::KJotsEdit(QWidget *parent, KActionCollection *actionCollection)
0073     : RichTextComposer(parent)
0074     , d(new Private)
0075     , m_actionCollection(actionCollection)
0076     , allowAutoDecimal(false)
0077 {
0078     setMouseTracking(true);
0079     setAcceptRichText(true);
0080     setWordWrapMode(QTextOption::WordWrap);
0081     setCheckSpellingEnabled(true);
0082     setFocusPolicy(Qt::StrongFocus);
0083 
0084     createActions(m_actionCollection);
0085     activateRichText();
0086 }
0087 
0088 KJotsEdit::~KJotsEdit() = default;
0089 
0090 void KJotsEdit::createActions(KActionCollection *ac)
0091 {
0092     RichTextComposer::createActions(ac);
0093 
0094     d->action_copy_into_title = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")),
0095                                             i18nc("@action", "Copy &Into Page Title"), this);
0096     connect(d->action_copy_into_title, &QAction::triggered, this, &KJotsEdit::copySelectionIntoTitle);
0097     connect(this, &KJotsEdit::copyAvailable, d->action_copy_into_title, &QAction::setEnabled);
0098     d->action_copy_into_title->setEnabled(false);
0099     d->editorActionList.append(d->action_copy_into_title);
0100     if (ac) {
0101         ac->addAction(QStringLiteral("copy_into_title"), d->action_copy_into_title);
0102     }
0103 
0104     d->action_manage_link = new QAction(QIcon::fromTheme(QStringLiteral("insert-link")),
0105                                         i18nc("@action creates and manages hyperlinks", "Link"), this);
0106     connect(d->action_manage_link, &QAction::triggered, this, &KJotsEdit::onLinkify);
0107     d->editorActionList.append(d->action_manage_link);
0108     if (ac) {
0109         ac->addAction(QStringLiteral("manage_note_link"), d->action_manage_link);
0110     }
0111 
0112     d->action_auto_bullet = new QAction(QIcon::fromTheme(QStringLiteral("format-list-unordered")),
0113                                         i18nc("@action", "Auto Bullet List"), this);
0114     d->action_auto_bullet->setCheckable(true);
0115     connect(d->action_auto_bullet, &QAction::triggered, this, &KJotsEdit::onAutoBullet);
0116     d->editorActionList.append(d->action_auto_bullet);
0117     if (ac) {
0118         ac->addAction(QStringLiteral("auto_bullet"), d->action_auto_bullet);
0119     }
0120 
0121     d->action_auto_decimal = new QAction(QIcon::fromTheme(QStringLiteral("format-list-ordered")),
0122                                          i18nc("@action", "Auto Decimal List"), this);
0123     d->action_auto_decimal->setCheckable(true);
0124     connect(d->action_auto_decimal, &QAction::triggered, this, &KJotsEdit::onAutoDecimal);
0125     d->editorActionList.append(d->action_auto_decimal);
0126     if (ac) {
0127         ac->addAction(QStringLiteral("auto_decimal"), d->action_auto_decimal);
0128     }
0129 
0130     d->action_insert_date = new QAction(QIcon::fromTheme(QStringLiteral("view-calendar-time-spent")),
0131                                         i18nc("@action", "Insert Date"), this);
0132     connect(d->action_insert_date, &QAction::triggered, this, [this](){
0133             insertPlainText(QLocale().toString(QDateTime::currentDateTime(), QLocale::ShortFormat));
0134         });
0135     d->editorActionList.append(d->action_insert_date);
0136     if (ac) {
0137         ac->addAction(QStringLiteral("insert_date"), d->action_insert_date);
0138         ac->setDefaultShortcut(d->action_insert_date, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_I));
0139     }
0140 
0141     d->action_save = KStandardAction::save(this, &KJotsEdit::savePage, ac);
0142     d->editorActionList.append(d->action_save);
0143 
0144     d->action_cut = KStandardAction::cut(this, &KJotsEdit::cut, ac);
0145     connect(this, &KJotsEdit::copyAvailable, d->action_cut, &QAction::setEnabled);
0146     d->action_cut->setEnabled(false);
0147     d->editorActionList.append(d->action_cut);
0148 
0149     d->action_paste = KStandardAction::paste(this, &KJotsEdit::paste, ac);
0150     d->editorActionList.append(d->action_paste);
0151 
0152     d->action_undo = KStandardAction::undo(this, &KJotsEdit::undo, ac);
0153     d->editorActionList.append(d->action_undo);
0154 
0155     d->action_redo = KStandardAction::redo(this, &KJotsEdit::redo, ac);
0156     d->editorActionList.append(d->action_redo);
0157 }
0158 
0159 void KJotsEdit::setEnableActions(bool enable)
0160 {
0161     // FIXME: RichTextComposer::setEnableActions(enable) messes with indent actions
0162     // due to bug in KPIMTextEdit (should be fixed in 20.08?)
0163     composerActions()->setActionsEnabled(enable);
0164     for (QAction *action : std::as_const(d->editorActionList)) {
0165         action->setEnabled(enable);
0166     }
0167 }
0168 
0169 void KJotsEdit::contextMenuEvent(QContextMenuEvent *event)
0170 {
0171     QMenu *popup = mousePopupMenu(event->pos());
0172     if (popup) {
0173         const QList<QAction*> actionList = popup->actions();
0174         if (!qApp->clipboard()->text().isEmpty()) {
0175             QAction *act = m_actionCollection->action(QStringLiteral("paste_without_formatting"));
0176             act->setIcon(QIcon::fromTheme(QStringLiteral("edit-paste")));
0177             act->setEnabled(!isReadOnly());
0178             // HACK: menu actions are following: Undo, Redo, Separator, Cut, Copy, Paste, Delete, Clear
0179             // We want to insert "Paste Without Formatting" right after standard Paste (which is at pos 6)
0180             // Let's hope QTextEdit and KPIMTextEdit::RichTextEditor doesn't break it
0181             // (and we don't break anything either)
0182             const int pasteActionPosition = 6;
0183             if (actionList.count() >= pasteActionPosition) {
0184                 popup->insertAction(popup->actions().at(pasteActionPosition), act);
0185             } else {
0186                 popup->addAction(act);
0187             }
0188         }
0189         popup->addSeparator();
0190         popup->addAction(d->action_copy_into_title);
0191 
0192         if (!anchorAt(event->pos()).isNull()) {
0193             popup->addAction(d->action_manage_link);
0194         }
0195 
0196         popup->exec(event->globalPos());
0197         delete popup;
0198     }
0199 }
0200 
0201 bool KJotsEdit::modified()
0202 {
0203     return document()->isModified();
0204 }
0205 
0206 bool KJotsEdit::locked()
0207 {
0208     return d->index.data(EntityTreeModel::ItemRole).value<Item>().hasAttribute<NoteShared::NoteLockAttribute>();
0209 }
0210 
0211 bool KJotsEdit::setModelIndex(const QModelIndex &index)
0212 {
0213     // Mapping index to ETM
0214     QModelIndex etmIndex = KJotsModel::etmIndex(index);
0215 
0216     // Saving the old document, if it was changed
0217     bool newDocument = d->index.isValid() && (d->index != etmIndex);
0218     if (newDocument) {
0219         savePage();
0220     }
0221 
0222     d->model = const_cast<QAbstractItemModel *>(etmIndex.model());
0223     d->index = QPersistentModelIndex(etmIndex);
0224     // Loading document
0225     auto *doc = d->index.data(KJotsModel::DocumentRole).value<QTextDocument *>();
0226     if (!doc) {
0227         setReadOnly(true);
0228         return false;
0229     }
0230 
0231     disconnect(document(), &QTextDocument::modificationChanged, this, &KJotsEdit::documentModified);
0232     setDocument(doc);
0233     connect(doc, &QTextDocument::modificationChanged, this, &KJotsEdit::documentModified);
0234 
0235     // Setting cursor
0236     auto cursor = doc->property("textCursor").value<QTextCursor>();
0237     if (!cursor.isNull()) {
0238         setTextCursor(cursor);
0239     } else {
0240         // This is a work-around for QTextEdit bug. If the first letter of the document is formatted,
0241         // QTextCursor doesn't follow this format. One can either move the cursor 1 symbol to the right
0242         // and then 1 symbol to the left as a workaround, or just explicitly move it to the start.
0243         // Submitted to qt-bugs, id 192886.
0244         //  -- (don't know the fate of this bug, as for April 2020 it is inaccessible)
0245         moveCursor(QTextCursor::Start);
0246     }
0247     // Setting focus if document was changed
0248     if (newDocument) {
0249         setFocus();
0250     }
0251     // Setting ReadOnly
0252     auto item = d->index.data(EntityTreeModel::ItemRole).value<Item>();
0253     if (!item.isValid()) {
0254         setReadOnly(true);
0255         return false;
0256     } else if (item.hasAttribute<NoteShared::NoteLockAttribute>()) {
0257         setReadOnly(true);
0258         return true;
0259     } else {
0260         setReadOnly(false);
0261         return true;
0262     }
0263 }
0264 
0265 void KJotsEdit::onAutoBullet()
0266 {
0267     KTextEdit::AutoFormatting currentFormatting = autoFormatting();
0268 
0269     //TODO: set line spacing properly.
0270 
0271     if (currentFormatting == KTextEdit::AutoBulletList) {
0272         setAutoFormatting(KTextEdit::AutoNone);
0273         d->action_auto_bullet->setChecked(false);
0274     } else {
0275         setAutoFormatting(KTextEdit::AutoBulletList);
0276         d->action_auto_bullet->setChecked(true);
0277     }
0278 }
0279 
0280 void KJotsEdit::createAutoDecimalList()
0281 {
0282     //this is an adaptation of Qt's createAutoBulletList() function for creating a bulleted list, except in this case I use it to create a decimal list.
0283     QTextCursor cursor = textCursor();
0284     cursor.beginEditBlock();
0285 
0286     QTextBlockFormat blockFmt = cursor.blockFormat();
0287 
0288     QTextListFormat listFmt;
0289     listFmt.setStyle(QTextListFormat::ListDecimal);
0290     listFmt.setIndent(blockFmt.indent() + 1);
0291 
0292     blockFmt.setIndent(0);
0293     cursor.setBlockFormat(blockFmt);
0294 
0295     cursor.createList(listFmt);
0296 
0297     cursor.endEditBlock();
0298     setTextCursor(cursor);
0299 }
0300 
0301 void KJotsEdit::DecimalList()
0302 {
0303     QTextCursor cursor = textCursor();
0304 
0305     if (cursor.currentList()) {
0306         return;
0307     }
0308 
0309     QString blockText = cursor.block().text();
0310 
0311     if (blockText.length() == 2 && blockText == QLatin1String("1.")) {
0312         cursor.movePosition(QTextCursor::StartOfLine, QTextCursor::KeepAnchor);
0313         cursor.removeSelectedText();
0314         createAutoDecimalList();
0315     }
0316 }
0317 
0318 void KJotsEdit::onAutoDecimal()
0319 {
0320     if (allowAutoDecimal) {
0321         allowAutoDecimal = false;
0322         disconnect(this, &KJotsEdit::textChanged, this, &KJotsEdit::DecimalList);
0323         d->action_auto_decimal->setChecked(false);
0324     } else {
0325         allowAutoDecimal = true;
0326         connect(this, &KJotsEdit::textChanged, this, &KJotsEdit::DecimalList);
0327         d->action_auto_decimal->setChecked(true);
0328     }
0329 }
0330 
0331 void KJotsEdit::onLinkify()
0332 {
0333     // Nothing is yet opened, ignoring
0334     if (!d->index.isValid()) {
0335         return;
0336     }
0337     composerControler()->selectLinkText();
0338     auto linkDialog = std::make_unique<KJotsLinkDialog>(const_cast<QAbstractItemModel*>(d->index.model()), this);
0339     linkDialog->setLinkText(composerControler()->currentLinkText());
0340     linkDialog->setLinkUrl(composerControler()->currentLinkUrl());
0341 
0342     if (linkDialog->exec()) {
0343         composerControler()->updateLink(linkDialog->linkUrl(), linkDialog->linkText());
0344     }
0345 }
0346 
0347 void KJotsEdit::copySelectionIntoTitle()
0348 {
0349     if (!d->index.isValid()) {
0350         return;
0351     }
0352     const QString newTitle(textCursor().selectedText());
0353     d->model->setData(d->index, newTitle);
0354 }
0355 
0356 bool KJotsEdit::canInsertFromMimeData(const QMimeData *source) const
0357 {
0358     if (source->hasUrls()) {
0359         return true;
0360     } else {
0361         return RichTextComposer::canInsertFromMimeData(source);
0362     }
0363 }
0364 
0365 void KJotsEdit::insertFromMimeData(const QMimeData *source)
0366 {
0367     // Nothing is opened, ignoring
0368     if (!d->index.isValid()) {
0369         return;
0370     }
0371     if (source->hasUrls()) {
0372         const QList<QUrl> urls = source->urls();
0373         for (const QUrl &url : urls) {
0374             if (url.scheme() == QStringLiteral("akonadi")) {
0375                 QModelIndex idx = KJotsModel::modelIndexForUrl(d->model, url);
0376                 if (idx.isValid()) {
0377                     insertHtml(QStringLiteral("<a href=\"%1\">%2</a>").arg(idx.data(KJotsModel::EntityUrlRole).toString(),
0378                                                                            idx.data().toString()));
0379                 }
0380             } else {
0381                 QString text = source->hasText() ? source->text() : url.toString(QUrl::RemovePassword);
0382                 insertHtml(QStringLiteral("<a href=\"%1\">%2</a>").arg(QString::fromUtf8(url.toEncoded()), text));
0383             }
0384         }
0385     } else if (source->hasHtml()) {
0386         // Don't have an action to set top and bottom margins on paragraphs yet.
0387         // Remove the margins for all inserted html.
0388         QTextDocument dummy;
0389         dummy.setHtml(source->html());
0390         QTextCursor c(&dummy);
0391         QTextBlockFormat fmt = c.blockFormat();
0392         fmt.setTopMargin(0);
0393         fmt.setBottomMargin(0);
0394         fmt.setLeftMargin(0);
0395         fmt.setRightMargin(0);
0396         c.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
0397         c.mergeBlockFormat(fmt);
0398 
0399         textCursor().insertFragment(QTextDocumentFragment(c));
0400         ensureCursorVisible();
0401     } else {
0402         RichTextComposer::insertFromMimeData(source);
0403     }
0404 }
0405 
0406 void KJotsEdit::mouseMoveEvent(QMouseEvent *event)
0407 {
0408     if ((event->modifiers() & Qt::ControlModifier) && !anchorAt(event->pos()).isEmpty()) {
0409         if (!m_cursorChanged) {
0410             QApplication::setOverrideCursor(Qt::PointingHandCursor);
0411             m_cursorChanged = true;
0412         }
0413     } else {
0414         if (m_cursorChanged) {
0415             QApplication::restoreOverrideCursor();
0416             m_cursorChanged = false;
0417         }
0418     }
0419     RichTextComposer::mouseMoveEvent(event);
0420 }
0421 
0422 void KJotsEdit::leaveEvent(QEvent *event)
0423 {
0424     if (m_cursorChanged) {
0425         QApplication::restoreOverrideCursor();
0426         m_cursorChanged = false;
0427     }
0428     RichTextComposer::leaveEvent(event);
0429 }
0430 
0431 void KJotsEdit::mousePressEvent(QMouseEvent *event)
0432 {
0433     const QUrl url = QUrl(anchorAt(event->pos()));
0434     if ((event->modifiers() & Qt::ControlModifier) && (event->button() & Qt::LeftButton) && !url.isEmpty()) {
0435         Q_EMIT linkClicked(url);
0436     } else {
0437         RichTextComposer::mousePressEvent(event);
0438     }
0439 }
0440 
0441 bool KJotsEdit::event(QEvent *event)
0442 {
0443     if (event->type() == QEvent::WindowDeactivate) {
0444         savePage();
0445     } else if (event->type() == QEvent::ToolTip) {
0446         tooltipEvent(static_cast<QHelpEvent *>(event));
0447     }
0448     return RichTextComposer::event(event);
0449 }
0450 
0451 void KJotsEdit::tooltipEvent(QHelpEvent *event)
0452 {
0453     // Nothing is opened, ignoring
0454     if (!d->index.isValid()) {
0455         return;
0456     }
0457     QUrl url(anchorAt(event->pos()));
0458     QString message;
0459 
0460     if (url.isValid()) {
0461         if (url.scheme() == QStringLiteral("akonadi")) {
0462             const QModelIndex idx = KJotsModel::modelIndexForUrl(d->model, url);
0463             if (idx.data(EntityTreeModel::ItemRole).value<Item>().isValid()) {
0464                 message = i18nc("@info:tooltip %1 is a full path to note (i.e. Notes / Notebook / Note)", "Ctrl+click to open note: %1", KJotsModel::itemPath(idx));
0465             } else if (idx.data(EntityTreeModel::CollectionRole).value<Collection>().isValid()) {
0466                 message = i18nc("@info:tooltip %1 is a full path to book (i.e. Notes / Notebook)", "Ctrl+click to open book: %1", KJotsModel::itemPath(idx));
0467             }
0468         } else {
0469             message = i18nc("@info:tooltip %1 is hyperlink address", "Ctrl+click to follow the hyperlink: %1", url.toString(QUrl::RemovePassword));
0470         }
0471     }
0472 
0473     if (!message.isEmpty()) {
0474         QToolTip::showText(event->globalPos(), message);
0475     } else {
0476         QToolTip::hideText();
0477     }
0478 }
0479 
0480 void KJotsEdit::focusOutEvent(QFocusEvent *event)
0481 {
0482     savePage();
0483     RichTextComposer::focusOutEvent(event);
0484 }
0485 
0486 void KJotsEdit::prepareDocumentForSaving()
0487 {
0488     document()->setModified(false);
0489     document()->setProperty("textCursor", QVariant::fromValue(textCursor()));
0490     document()->setProperty("images", QVariant::fromValue(composerControler()->composerImages()->embeddedImages()));
0491 }
0492 
0493 void KJotsEdit::savePage()
0494 {
0495     if (!document()->isModified() || !d->index.isValid()) {
0496         return;
0497     }
0498 
0499     prepareDocumentForSaving();
0500     d->model->setData(d->index, QVariant::fromValue(document()), KJotsModel::DocumentRole);
0501 }
0502 
0503 /* ex: set tabstop=4 softtabstop=4 shiftwidth=4 expandtab: */
0504 
0505 #include "moc_kjotsedit.cpp"