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"