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

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-2009 Stephen Kelly <steveire@gmail.com>
0009                   2020 Igor Poboiko <igor.poboiko@gmail.com>
0010 
0011     SPDX-License-Identifier: LGPL-2.0-or-later
0012 */
0013 
0014 #include "kjotswidget.h"
0015 
0016 // Qt
0017 #include <QHBoxLayout>
0018 #include <QSplitter>
0019 #include <QStackedWidget>
0020 #include <QTextCursor>
0021 #include <QTextDocument>
0022 #include <QTextDocumentFragment>
0023 #include <QTimer>
0024 #include <QPrintDialog>
0025 #include <QPrinter>
0026 #include <QPrintPreviewDialog>
0027 #include <QDBusConnection>
0028 #include <QMenu>
0029 #include <QFileDialog>
0030 #include <QAction>
0031 #include <QIcon>
0032 #include <QItemDelegate>
0033 #include <QHeaderView>
0034 #include <QDebug>
0035 #include <QActionGroup>
0036 
0037 // Akonadi
0038 #include <akonadi_version.h>
0039 #include <Akonadi/NoteUtils>
0040 #include <Akonadi/AttributeFactory>
0041 #include <Akonadi/CollectionCreateJob>
0042 #include <Akonadi/CollectionDeleteJob>
0043 #include <Akonadi/ChangeRecorder>
0044 #include <Akonadi/EntityDisplayAttribute>
0045 #include <Akonadi/EntityMimeTypeFilterModel>
0046 #include <Akonadi/Item>
0047 #include <Akonadi/ItemCreateJob>
0048 #include <Akonadi/ItemModifyJob>
0049 #include <Akonadi/ItemDeleteJob>
0050 #include <Akonadi/ItemFetchScope>
0051 #include <Akonadi/EntityOrderProxyModel>
0052 #include <Akonadi/EntityTreeView>
0053 #include <Akonadi/ETMViewStateSaver>
0054 #include <Akonadi/ControlGui>
0055 
0056 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0057 #include <grantlee/template.h>
0058 #include <grantlee/engine.h>
0059 #include <grantlee/context.h>
0060 #else
0061 #include <KTextTemplate/Engine>
0062 #include <KTextTemplate/Template>
0063 #include <KTextTemplate/Context>
0064 #endif
0065 
0066 // KDE
0067 #include <KActionCollection>
0068 #include <KBookmarkMenu>
0069 #include <KLocalizedString>
0070 #include <KMessageBox>
0071 #include <KSelectionProxyModel>
0072 #include <KXMLGUIClient>
0073 #include <KActionMenu>
0074 #include <KRandom>
0075 #include <KSharedConfig>
0076 #include <KConfigDialog>
0077 #include <KIO/OpenUrlJob>
0078 
0079 #include <KPIMTextEdit/RichTextComposerActions>
0080 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0081 #include <KPIMTextEdit/RichTextEditorWidget>
0082 #else
0083 #include <TextCustomEditor/RichTextEditorWidget>
0084 #endif
0085 
0086 
0087 // KMime
0088 #include <KMime/Message>
0089 
0090 // KJots
0091 #include "uistatesaver.h"
0092 #include "kjotsbookmarks.h"
0093 #include "kjotsmodel.h"
0094 #include "kjotsedit.h"
0095 #include "kjotsconfigdlg.h"
0096 #include "KJotsSettings.h"
0097 #include "kjotsbrowser.h"
0098 #include "noteshared/notelockattribute.h"
0099 #include "noteshared/notepinattribute.h"
0100 #include "noteshared/standardnoteactionmanager.h"
0101 #include "notesortproxymodel.h"
0102 
0103 #include <memory>
0104 
0105 using namespace Akonadi;
0106 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0107 using namespace Grantlee;
0108 #else
0109 using namespace KTextTemplate;
0110 #endif
0111 KJotsWidget::KJotsWidget(QWidget *parent, KXMLGUIClient *xmlGuiClient, Qt::WindowFlags f)
0112     : QWidget(parent, f)
0113     , m_xmlGuiClient(xmlGuiClient)
0114 {
0115     ControlGui::widgetNeedsAkonadi(this);
0116 
0117     Akonadi::AttributeFactory::registerAttribute<NoteShared::NoteLockAttribute>();
0118     Akonadi::AttributeFactory::registerAttribute<NoteShared::NotePinAttribute>();
0119 
0120     // Grantlee
0121     m_loader = QSharedPointer<FileSystemTemplateLoader>(new FileSystemTemplateLoader());
0122     m_loader->setTemplateDirs(QStandardPaths::locateAll(QStandardPaths::GenericDataLocation,
0123                                                         QStringLiteral("kjots/themes"),
0124                                                         QStandardPaths::LocateDirectory));
0125     m_loader->setTheme(QStringLiteral("default"));
0126     m_templateEngine = new Engine(this);
0127     m_templateEngine->addTemplateLoader(m_loader);
0128 
0129     // GUI & Actions
0130     setupGui();
0131     setupActions();
0132     // Models
0133     ItemFetchScope scope;
0134     scope.fetchFullPayload(true);   // Need to have full item when adding it to the internal data structure
0135     scope.fetchAttribute<EntityDisplayAttribute>();
0136     scope.fetchAttribute<NoteShared::NoteLockAttribute>();
0137     scope.fetchAttribute<NoteShared::NotePinAttribute>();
0138 
0139     auto monitor = new ChangeRecorder(this);
0140     monitor->fetchCollection(true);
0141     monitor->setItemFetchScope(scope);
0142     monitor->setCollectionMonitored(Collection::root());
0143     monitor->setMimeTypeMonitored(NoteUtils::noteMimeType());
0144 
0145     m_kjotsModel = new KJotsModel(monitor, this);
0146 
0147     m_browserWidget->browser()->setModel(m_kjotsModel);
0148 
0149     m_collectionModel = new EntityMimeTypeFilterModel(this);
0150     m_collectionModel->setSourceModel(m_kjotsModel);
0151     m_collectionModel->addMimeTypeInclusionFilter(Collection::mimeType());
0152     m_collectionModel->setHeaderGroup(EntityTreeModel::CollectionTreeHeaders);
0153     m_collectionModel->setDynamicSortFilter(true);
0154     m_collectionModel->setSortCaseSensitivity(Qt::CaseInsensitive);
0155 
0156     m_orderProxy = new EntityOrderProxyModel(this);
0157     m_orderProxy->setSourceModel(m_collectionModel);
0158     KConfigGroup cfg(KSharedConfig::openConfig(), QStringLiteral("KJotsEntityOrder"));
0159     m_orderProxy->setOrderConfig(cfg);
0160 
0161     m_collectionView->setModel(m_orderProxy);
0162 
0163     m_collectionSelectionProxyModel = new KSelectionProxyModel(m_collectionView->selectionModel(), this);
0164     m_collectionSelectionProxyModel->setSourceModel(m_kjotsModel);
0165     m_collectionSelectionProxyModel->setFilterBehavior(KSelectionProxyModel::ChildrenOfExactSelection);
0166 
0167     m_itemModel = new EntityMimeTypeFilterModel(this);
0168     m_itemModel->setSourceModel(m_collectionSelectionProxyModel);
0169     m_itemModel->addMimeTypeExclusionFilter(Collection::mimeType());
0170     m_itemModel->setHeaderGroup(EntityTreeModel::ItemListHeaders);
0171 
0172     m_itemSortModel = new NoteSortProxyModel(this);
0173     m_itemSortModel->setSourceModel(m_itemModel);
0174     m_itemSortModel->setSortRole(Qt::EditRole);
0175 
0176     m_itemView->setModel(m_itemSortModel);
0177 
0178     m_actionManager->setCollectionSelectionModel(m_collectionView->selectionModel());
0179     m_actionManager->setItemSelectionModel(m_itemView->selectionModel());
0180 
0181     connect(m_kjotsModel, &EntityTreeModel::modelAboutToBeReset, this, &KJotsWidget::saveState);
0182     connect(m_kjotsModel, &EntityTreeModel::modelReset, this, &KJotsWidget::restoreState);
0183 
0184     // TODO: handle dataChanged properly, i.e. if item was changed from outside
0185     connect(m_collectionView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &KJotsWidget::renderSelection);
0186     connect(m_collectionView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &KJotsWidget::updateMenu);
0187     connect(m_collectionView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &KJotsWidget::updateCaption);
0188     connect(m_collectionView->model(), &QAbstractItemModel::dataChanged, this, &KJotsWidget::renderSelection);
0189     connect(m_collectionView->model(), &QAbstractItemModel::dataChanged, this, &KJotsWidget::updateCaption);
0190 
0191     connect(m_itemView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &KJotsWidget::renderSelection);
0192     connect(m_itemView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &KJotsWidget::updateMenu);
0193     connect(m_itemView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &KJotsWidget::updateCaption);
0194     connect(m_itemView->model(), &QAbstractItemModel::dataChanged, this, &KJotsWidget::renderSelection);
0195     connect(m_itemView->model(), &QAbstractItemModel::dataChanged, this, &KJotsWidget::updateCaption);
0196 
0197     connect(m_editor, &KJotsEdit::documentModified, this, &KJotsWidget::updateCaption);
0198 
0199     QTimer::singleShot(0, this, &KJotsWidget::delayedInitialization);
0200 
0201     // Autosave timer
0202     m_autosaveTimer = new QTimer(this);
0203     updateConfiguration();
0204     connect(m_autosaveTimer, &QTimer::timeout, m_editor, &KJotsEdit::savePage);
0205     connect(m_collectionView->selectionModel(), &QItemSelectionModel::selectionChanged, m_autosaveTimer, qOverload<>(&QTimer::start));
0206 
0207     restoreState();
0208 
0209     QDBusConnection::sessionBus().registerObject(QStringLiteral("/KJotsWidget"), this, QDBusConnection::ExportScriptableContents);
0210 }
0211 
0212 KJotsWidget::~KJotsWidget()
0213 {
0214     saveState();
0215 }
0216 
0217 void KJotsWidget::setupGui()
0218 {
0219     // Main horizontal layout
0220     auto layout = new QHBoxLayout(this);
0221     layout->setContentsMargins(0, 0, 0, 0);
0222 
0223     // Splitter between (collection view) and (item view + editor)
0224     m_splitter1 = new QSplitter(this);
0225     m_splitter1->setObjectName(QStringLiteral("CollectionSplitter"));
0226     m_splitter1->setStretchFactor(1, 1);
0227     layout->addWidget(m_splitter1);
0228 
0229     // Collection view
0230     m_collectionView = new EntityTreeView(m_xmlGuiClient, m_splitter1);
0231     m_collectionView->setObjectName(QStringLiteral("CollectionView"));
0232     m_collectionView->setSelectionMode(QAbstractItemView::ExtendedSelection);
0233     m_collectionView->setEditTriggers(QAbstractItemView::DoubleClicked);
0234     m_collectionView->setManualSortingActive(true);
0235     m_collectionView->header()->setDefaultAlignment(Qt::AlignCenter);
0236 
0237     // Splitter between item view and editor
0238     m_splitter2 = new QSplitter(m_splitter1);
0239     m_splitter2->setObjectName(QStringLiteral("EditorSplitter"));
0240 
0241     // Item view
0242     m_itemView = new EntityTreeView(m_xmlGuiClient, m_splitter2);
0243     m_itemView->setObjectName(QStringLiteral("ItemView"));
0244     m_itemView->setSelectionMode(QAbstractItemView::ExtendedSelection);
0245     m_itemView->setSelectionBehavior(QAbstractItemView::SelectRows);
0246     m_itemView->setEditTriggers(QAbstractItemView::DoubleClicked);
0247     m_itemView->setRootIsDecorated(false);
0248     m_itemView->header()->setDefaultAlignment(Qt::AlignCenter);
0249 
0250     // Stacked widget containing editor & browser
0251     m_stackedWidget = new QStackedWidget(m_splitter2);
0252 
0253     // Editor
0254     m_editor = new KJotsEdit(m_stackedWidget, m_xmlGuiClient->actionCollection());
0255 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0256     m_editorWidget = new KPIMTextEdit::RichTextEditorWidget(m_editor, m_stackedWidget);
0257 #else
0258     m_editorWidget = new TextCustomEditor::RichTextEditorWidget(m_editor, m_stackedWidget);
0259 #endif
0260     m_editor->setParent(m_editorWidget);
0261     m_stackedWidget->addWidget(m_editorWidget);
0262     connect(m_editor, &KJotsEdit::linkClicked, this, &KJotsWidget::openLink);
0263     // Browser
0264     m_browserWidget = new KJotsBrowserWidget(std::make_unique<KJotsBrowser>(m_xmlGuiClient->actionCollection()),
0265                                              m_stackedWidget);
0266     m_stackedWidget->addWidget(m_browserWidget);
0267     m_stackedWidget->setCurrentWidget(m_browserWidget);
0268     connect(m_browserWidget->browser(), &KJotsBrowser::linkClicked, this, &KJotsWidget::openLink);
0269 
0270     // Make sure the editor gets focus again after naming a new book/page.
0271     connect(m_collectionView->itemDelegate(), &QItemDelegate::closeEditor, this, [this](){
0272             activeEditor()->setFocus();
0273         });
0274 }
0275 
0276 void KJotsWidget::restoreState()
0277 {
0278     {
0279         auto saver = new ETMViewStateSaver;
0280         saver->setView(m_collectionView);
0281         KConfigGroup cfg(KSharedConfig::openConfig(), QStringLiteral("CollectionViewState"));
0282         saver->restoreState(cfg);
0283     }
0284     {
0285         auto saver = new ETMViewStateSaver;
0286         saver->setView(m_itemView);
0287         KConfigGroup cfg(KSharedConfig::openConfig(), QStringLiteral("ItemViewState"));
0288         saver->restoreState(cfg);
0289     }
0290 }
0291 
0292 void KJotsWidget::saveState()
0293 {
0294     {
0295         ETMViewStateSaver saver;
0296         saver.setView(m_collectionView);
0297         KConfigGroup cfg(KSharedConfig::openConfig(), QStringLiteral("CollectionViewState"));
0298         saver.saveState(cfg);
0299         cfg.sync();
0300     }
0301     {
0302         ETMViewStateSaver saver;
0303         saver.setView(m_itemView);
0304         KConfigGroup cfg(KSharedConfig::openConfig(), QStringLiteral("ItemViewState"));
0305         saver.saveState(cfg);
0306         cfg.sync();
0307     }
0308 }
0309 
0310 void KJotsWidget::saveUIStates() const
0311 {
0312     const QString groupName = QStringLiteral("UiState_MainWidget_%1").arg(KJotsSettings::viewMode());
0313     KConfigGroup group(KSharedConfig::openConfig(), groupName);
0314     KJots::UiStateSaver::saveState(m_splitter1, group);
0315     KJots::UiStateSaver::saveState(m_splitter2, group);
0316     KJots::UiStateSaver::saveState(m_collectionView, group);
0317     KJots::UiStateSaver::saveState(m_itemView, group);
0318     group.sync();
0319 }
0320 
0321 void KJotsWidget::restoreUIStates()
0322 {
0323     const QString groupName = QStringLiteral("UiState_MainWidget_%1").arg(KJotsSettings::viewMode());
0324     KConfigGroup group(KSharedConfig::openConfig(), groupName);
0325     KJots::UiStateSaver::restoreState(m_splitter1, group);
0326     KJots::UiStateSaver::restoreState(m_splitter2, group);
0327     KJots::UiStateSaver::restoreState(m_collectionView, group);
0328     KJots::UiStateSaver::restoreState(m_itemView, group);
0329     group.sync();
0330 }
0331 
0332 void KJotsWidget::setViewMode(int mode)
0333 {
0334     const int newMode = (mode == 0) ? KJotsSettings::viewMode() : mode;
0335     m_splitter2->setOrientation(newMode == 1 ? Qt::Vertical : Qt::Horizontal);
0336     if (mode != 0) {
0337         KJotsSettings::setViewMode(mode);
0338         saveUIStates();
0339     }
0340     restoreUIStates();
0341     m_viewModeGroup->actions().at(newMode-1)->setChecked(true);
0342 }
0343 
0344 void KJotsWidget::setupActions()
0345 {
0346     KActionCollection *actionCollection = m_xmlGuiClient->actionCollection();
0347     // Default Akonadi Notes actions
0348     m_actionManager = new StandardNoteActionManager(actionCollection, this);
0349     m_actionManager->createAllActions();
0350 
0351     actionCollection->setDefaultShortcut(m_actionManager->action(StandardActionManager::CreateCollection),
0352                                          QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_N));
0353 
0354     QAction *action;
0355     // Standard actions
0356     KStandardAction::preferences(this, &KJotsWidget::configure, actionCollection);
0357 
0358     action = KStandardAction::next(this, [this](){
0359             m_itemView->selectionModel()->select(previousNextEntity(m_itemView, +1), QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
0360         }, actionCollection);
0361     actionCollection->setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::Key_PageDown));
0362     connect(this, &KJotsWidget::canGoNextPageChanged, action, &QAction::setEnabled);
0363 
0364     action = KStandardAction::prior(this, [this](){
0365             m_itemView->selectionModel()->select(previousNextEntity(m_itemView, -1), QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
0366         }, actionCollection);
0367     actionCollection->setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::Key_PageUp));
0368     connect(this, &KJotsWidget::canGoPreviousPageChanged, action, &QAction::setEnabled);
0369 
0370     KStandardAction::renameFile(this, [this](){
0371             EntityTreeView *activeView;
0372             if (m_collectionView->hasFocus()) {
0373                 activeView = m_collectionView;
0374             } else {
0375                 activeView = m_itemView;
0376             }
0377 
0378             const QModelIndexList rows = activeView->selectionModel()->selectedRows();
0379             if (rows.size() != 1) {
0380                 return;
0381             }
0382             activeView->edit(rows.first());
0383         }, actionCollection);
0384 
0385     action = KStandardAction::deleteFile(this, [this](){
0386             if (m_collectionView->hasFocus()) {
0387                 m_actionManager->action(StandardActionManager::DeleteCollections)->trigger();
0388             } else {
0389                 m_actionManager->action(StandardActionManager::DeleteItems)->trigger();
0390             }
0391         }, actionCollection);
0392     // Default Shift+Delete is ambiguous and used both for cut and delete
0393     actionCollection->setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::Key_Delete));
0394 
0395     action = KStandardAction::copy(this, [this](){
0396             activeEditor()->copy();
0397         }, actionCollection);
0398     connect(m_editor, &KJotsEdit::copyAvailable, action, &QAction::setEnabled);
0399     connect(m_browserWidget->browser(), &KJotsBrowser::copyAvailable, action, &QAction::setEnabled);
0400     action->setEnabled(false);
0401 
0402     action = KStandardAction::selectAll(this, [this](){
0403             activeEditor()->selectAll();
0404         }, actionCollection);
0405     KStandardAction::find(this, [this](){
0406             if (m_editorWidget->isVisible()) {
0407                 m_editorWidget->slotFind();
0408             } else {
0409                 m_browserWidget->slotFind();
0410             }
0411         }, actionCollection);
0412     KStandardAction::findNext(this, [this](){
0413             if (m_editorWidget->isVisible()) {
0414                 m_editorWidget->slotFindNext();
0415             } else {
0416                 m_browserWidget->slotFindNext();
0417             }
0418         }, actionCollection);
0419 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0420     action = KStandardAction::replace(m_editorWidget, &KPIMTextEdit::RichTextEditorWidget::slotReplace, actionCollection);
0421 #else
0422     action = KStandardAction::replace(m_editorWidget, &TextCustomEditor::RichTextEditorWidget::slotReplace, actionCollection);
0423 #endif
0424     connect(m_stackedWidget, &QStackedWidget::currentChanged, this, [this, action](int index){
0425             action->setEnabled(m_stackedWidget->widget(index) == m_editorWidget);
0426         });
0427 
0428     KStandardAction::print(this, &KJotsWidget::printSelection, actionCollection);
0429     KStandardAction::printPreview(this, &KJotsWidget::printPreviewSelection, actionCollection);
0430 
0431     // Bookmarks actions
0432     auto *bookmarkMenu = actionCollection->add<KActionMenu>(QStringLiteral("bookmarks"));
0433     bookmarkMenu->setText(i18n("&Bookmarks"));
0434     auto bookmarks = new KJotsBookmarks(m_collectionView->selectionModel(), this);
0435     connect(bookmarks, &KJotsBookmarks::openLink, this, &KJotsWidget::openLink);
0436 #if QT_VERSION_MAJOR < 6
0437     m_bookmarkManager = KBookmarkManager::managerForFile(
0438         QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) +
0439             QStringLiteral("/kjots/bookmarks.xml"), QStringLiteral("kjots"));
0440         auto bmm = new KBookmarkMenu(m_bookmarkManager,
0441                                      bookmarks, bookmarkMenu->menu());
0442 #else
0443     m_bookmarkManager = new KBookmarkManager(
0444         QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) +
0445         QStringLiteral("/kjots/bookmarks.xml"), this);
0446     auto bmm = new KBookmarkMenu(m_bookmarkManager, bookmarks,
0447                                  bookmarkMenu->menu());
0448 #endif
0449 
0450     // "Add bookmark" and "make text bold" actions have conflicting shortcuts
0451     // (ctrl + b) Make add_bookmark use ctrl+shift+b to resolve that.
0452     action = bmm->addBookmarkAction();
0453     actionCollection->addAction(QStringLiteral("add_bookmark"), action);
0454     actionCollection->setDefaultShortcut(action, Qt::CTRL | Qt::SHIFT | Qt::Key_B);
0455     actionCollection->addAction(QStringLiteral("edit_bookmark"), bmm->editBookmarksAction());
0456     actionCollection->addAction(QStringLiteral("add_bookmarks_list"), bmm->bookmarkTabsAsFolderAction());
0457 
0458     // Export actions
0459     auto *exportMenu = actionCollection->add<KActionMenu>(QStringLiteral("save_to"));
0460     exportMenu->setText(i18n("Export"));
0461     exportMenu->setIcon(QIcon::fromTheme(QStringLiteral("document-export")));
0462 
0463     action = actionCollection->addAction(QStringLiteral("save_to_ascii"));
0464     action->setText(i18n("To Text File..."));
0465     action->setIcon(QIcon::fromTheme(QStringLiteral("text-plain")));
0466     connect(action, &QAction::triggered, this, [this](){
0467             exportSelection(QStringLiteral("plain_text"), QStringLiteral("template.txt"));
0468         });
0469     exportMenu->menu()->addAction(action);
0470 
0471     action = actionCollection->addAction(QStringLiteral("save_to_html"));
0472     action->setText(i18n("To HTML File..."));
0473     action->setIcon(QIcon::fromTheme(QStringLiteral("text-html")));
0474     connect(action, &QAction::triggered, this, [this](){
0475             exportSelection(QStringLiteral("default"), QStringLiteral("template.html"));
0476         });
0477     exportMenu->menu()->addAction(action);
0478 
0479     // Move actions
0480     action = actionCollection->addAction(QStringLiteral("go_next_book"));
0481     action->setText(i18n("Next Book"));
0482     action->setIcon(QIcon::fromTheme(QStringLiteral("go-down")));
0483     actionCollection->setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_PageDown));
0484     connect(action, &QAction::triggered, this, [this](){
0485             const QModelIndex idx = previousNextEntity(m_collectionView, +1);
0486             m_collectionView->selectionModel()->select(idx, QItemSelectionModel::SelectCurrent);
0487             m_collectionView->expand(idx);
0488         });
0489     connect(this, &KJotsWidget::canGoNextBookChanged, action, &QAction::setEnabled);
0490 
0491     action = actionCollection->addAction(QStringLiteral("go_prev_book"));
0492     action->setText(i18n("Previous Book"));
0493     action->setIcon(QIcon::fromTheme(QStringLiteral("go-up")));
0494     actionCollection->setDefaultShortcut(action, QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_PageUp));
0495     connect(action, &QAction::triggered, this, [this](){
0496             const QModelIndex idx = previousNextEntity(m_collectionView, -1);
0497             m_collectionView->selectionModel()->select(idx, QItemSelectionModel::SelectCurrent);
0498             m_collectionView->expand(idx);
0499         });
0500     connect(this, &KJotsWidget::canGoPreviousBookChanged, action, &QAction::setEnabled);
0501 
0502     // View mode actions
0503     m_viewModeGroup = new QActionGroup(this);
0504 
0505     action = new QAction(i18nc("@action:inmenu", "Two Columns"), m_viewModeGroup);
0506     action->setCheckable(true);
0507     action->setData(1);
0508     actionCollection->addAction(QStringLiteral("view_mode_two_columns"), action);
0509 
0510     action = new QAction(i18nc("@action:inmenu", "Three Columns"), m_viewModeGroup);
0511     action->setCheckable(true);
0512     action->setData(2);
0513     actionCollection->addAction(QStringLiteral("view_mode_three_columns"), action);
0514 
0515     connect(m_viewModeGroup, &QActionGroup::triggered, this, [this](QAction *action){
0516             setViewMode(action->data().toInt());
0517         });
0518 }
0519 
0520 void KJotsWidget::delayedInitialization()
0521 {
0522     // anySelectionActions are available when at least something is selected (i.e. editor/browser are not empty
0523 
0524     KActionCollection *actionCollection = m_xmlGuiClient->actionCollection();
0525 
0526     // Actions for a single item selection.
0527 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0528     anySelectionActions = { actionCollection->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Find))),
0529                             actionCollection->action(QString::fromLatin1(KStandardAction::name(KStandardAction::Print))),
0530 #else
0531     anySelectionActions = { actionCollection->action(KStandardAction::name(KStandardAction::Find)),
0532                             actionCollection->action(KStandardAction::name(KStandardAction::Print)),
0533 #endif
0534                             actionCollection->action(QStringLiteral("save_to")) };
0535 
0536     updateMenu();
0537 
0538     // Load view mode and splitters
0539     setViewMode(0);
0540 }
0541 
0542 inline QTextEdit *KJotsWidget::activeEditor()
0543 {
0544     if (m_browserWidget->isVisible()) {
0545         return m_browserWidget->browser();
0546     } else {
0547         return m_editor;
0548     }
0549 }
0550 
0551 void KJotsWidget::updateMenu()
0552 {
0553     const int collectionsSelected = m_collectionView->selectionModel()->selectedRows().count();
0554     const int itemsSelected = m_itemView->selectionModel()->selectedRows().count();
0555     const int selectionSize = itemsSelected + collectionsSelected;
0556 
0557     // Actions available only when editor is shown
0558     m_editor->setEnableActions(itemsSelected == 1 && !m_editor->locked());
0559 
0560     // Rename is available only when single something is selected
0561     m_xmlGuiClient->actionCollection()
0562 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0563             ->action(QString::fromLatin1(KStandardAction::name(KStandardAction::RenameFile)))
0564 #else
0565             ->action(KStandardAction::name(KStandardAction::RenameFile))
0566 #endif
0567             ->setEnabled((itemsSelected == 1) || (m_collectionView->hasFocus() && collectionsSelected == 1));
0568 
0569     // Actions available when at least something is shown
0570     for (QAction *action : std::as_const(anySelectionActions)) {
0571         action->setEnabled(selectionSize >= 1);
0572     }
0573 }
0574 
0575 void KJotsWidget::configure()
0576 {
0577     if (KConfigDialog::showDialog(QStringLiteral("kjotssettings"))) {
0578         return;
0579     }
0580     auto* dialog = new KConfigDialog(this, QStringLiteral("kjotssettings"), KJotsSettings::self());
0581     auto configMisc = new KJotsConfigMisc(dialog);
0582 #if KCMUTILS_VERSION < QT_VERSION_CHECK(5, 240, 0)
0583     dialog->addPage(configMisc, i18nc("@title:window config dialog page", "Misc"), QStringLiteral("preferences-other"));
0584 #else
0585     dialog->addPage(configMisc->widget(), i18nc("@title:window config dialog page", "Misc"), QStringLiteral("preferences-other"));
0586 #endif
0587     connect(dialog, &KConfigDialog::settingsChanged, this, &KJotsWidget::updateConfiguration);
0588     dialog->show();
0589 }
0590 
0591 void KJotsWidget::updateConfiguration()
0592 {
0593     if (KJotsSettings::autoSave()) {
0594         m_autosaveTimer->setInterval(KJotsSettings::autoSaveInterval() * 1000 * 60);
0595         m_autosaveTimer->start();
0596     } else {
0597         m_autosaveTimer->stop();
0598     }
0599 }
0600 
0601 QString KJotsWidget::renderSelectionTo(const QString &theme, const QString &templ)
0602 {
0603     QList<QVariant> objectList;
0604 
0605     const QModelIndexList selectedItems = m_itemView->selectionModel()->selectedRows();
0606     if (selectedItems.count() > 0) {
0607         objectList.reserve(selectedItems.size());
0608         std::transform(selectedItems.cbegin(), selectedItems.cend(), std::back_inserter(objectList),
0609             [](const QModelIndex &idx){
0610                 return idx.data(KJotsModel::GrantleeObjectRole);
0611             });
0612     } else {
0613         const QModelIndexList selectedCollections = m_collectionView->selectionModel()->selectedRows();
0614         objectList.reserve(selectedCollections.size());
0615         std::transform(selectedCollections.cbegin(), selectedCollections.cend(), std::back_inserter(objectList),
0616             [](const QModelIndex &idx){
0617                 return idx.data(KJotsModel::GrantleeObjectRole);
0618             });
0619     }
0620     QHash<QString, QVariant> hash = {{QStringLiteral("entities"), objectList},
0621                                      {QStringLiteral("i18n_TABLE_OF_CONTENTS"), i18nc("Header for 'Table of contents' section of rendered output", "Table of contents")}};
0622 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0623     Context c(hash);
0624 #else
0625     KTextTemplate::Context c(hash);
0626 #endif
0627 
0628     const QString currentTheme = m_loader->themeName();
0629     m_loader->setTheme(theme);
0630 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0631     Template t = m_templateEngine->loadByName(templ);
0632 #else
0633     KTextTemplate::Template t = m_templateEngine->loadByName(templ);
0634 #endif
0635     const QString result = t->render(&c);
0636     m_loader->setTheme(currentTheme);
0637     return result;
0638 }
0639 
0640 QString KJotsWidget::renderSelectionToHtml()
0641 {
0642     return renderSelectionTo(QStringLiteral("default"), QStringLiteral("template.html"));
0643 }
0644 
0645 void KJotsWidget::exportSelection(const QString &theme, const QString &templ)
0646 {
0647     // TODO: dialog captions & etc
0648     QString filename = QFileDialog::getSaveFileName();
0649     if (filename.isEmpty()) {
0650         return;
0651     }
0652     QFile exportFile(filename);
0653     if (!exportFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
0654         KMessageBox::error(this, i18n("<qt>Could not open \"%1\" for writing</qt>", filename));
0655         return;
0656     }
0657     exportFile.write(renderSelectionTo(theme, templ).toUtf8());
0658     exportFile.close();
0659 }
0660 
0661 std::unique_ptr<QPrinter> KJotsWidget::setupPrinter(QPrinter::PrinterMode mode)
0662 {
0663     auto printer = std::make_unique<QPrinter>(mode);
0664     printer->setDocName(QStringLiteral("KJots_Print"));
0665     printer->setCreator(QStringLiteral("KJots"));
0666     if (!activeEditor()->textCursor().selection().isEmpty()) {
0667         printer->setPrintRange(QPrinter::Selection);
0668     }
0669     return printer;
0670 }
0671 
0672 void KJotsWidget::printPreviewSelection()
0673 {
0674     auto printer = setupPrinter(QPrinter::ScreenResolution);
0675     QPrintPreviewDialog previewdlg(printer.get(), this);
0676     connect(&previewdlg, &QPrintPreviewDialog::paintRequested, this, &KJotsWidget::print);
0677     previewdlg.exec();
0678 }
0679 
0680 void KJotsWidget::printSelection()
0681 {
0682     auto printer = setupPrinter(QPrinter::HighResolution);
0683     QPrintDialog printDialog(printer.get(), this);
0684     if (printDialog.exec() == QDialog::Accepted) {
0685         print(printer.get());
0686     }
0687 }
0688 
0689 void KJotsWidget::print(QPrinter *printer)
0690 {
0691     QTextDocument printDocument;
0692     if (printer->printRange() == QPrinter::Selection) {
0693         printDocument.setHtml(activeEditor()->textCursor().selection().toHtml());
0694     } else {
0695         QString currentTheme = m_loader->themeName();
0696         m_loader->setTheme(QStringLiteral("default"));
0697         printDocument.setHtml(renderSelectionToHtml());
0698         m_loader->setTheme(currentTheme);
0699     }
0700     printDocument.print(printer);
0701 }
0702 
0703 QModelIndex KJotsWidget::previousNextEntity(QTreeView *view, int step)
0704 {
0705     const QModelIndexList selection = view->selectionModel()->selectedRows();
0706     if (selection.size() == 0) {
0707         return step > 0 ? view->model()->index(0, 0) : view->model()->index(view->model()->rowCount()-1, 0);
0708     }
0709     if (selection.size() != 1) {
0710         return {};
0711     }
0712     return step > 0 ? view->indexBelow(selection.first()) : view->indexAbove(selection.first());
0713 }
0714 
0715 void KJotsWidget::renderSelection()
0716 {
0717     Q_EMIT canGoNextBookChanged(previousNextEntity(m_collectionView, +1).isValid());
0718     Q_EMIT canGoNextPageChanged(previousNextEntity(m_itemView, +1).isValid());
0719     Q_EMIT canGoPreviousBookChanged(previousNextEntity(m_collectionView, -1).isValid());
0720     Q_EMIT canGoPreviousPageChanged(previousNextEntity(m_itemView, -1).isValid());
0721 
0722     const QModelIndexList selectedItems = m_itemView->selectionModel()->selectedRows();
0723 
0724     // If the selection is a single note, present it for editing...
0725     if (selectedItems.count() == 1) {
0726         if (m_editor->setModelIndex(selectedItems.first())) {
0727             m_stackedWidget->setCurrentWidget(m_editorWidget);
0728             return;
0729         }
0730         // If something went wrong, we show user the browser
0731     }
0732     // ... Otherwise, render the selection read-only.
0733     m_browserWidget->browser()->setHtml(renderSelectionToHtml());
0734     m_stackedWidget->setCurrentWidget(m_browserWidget);
0735 }
0736 
0737 void KJotsWidget::updateCaption()
0738 {
0739     QString caption;
0740     const QModelIndexList itemSelection = m_itemView->selectionModel()->selectedRows();
0741     const QModelIndexList collectionSelection = m_collectionView->selectionModel()->selectedRows();
0742     if (itemSelection.size() == 1) {
0743         caption = KJotsModel::itemPath(KJotsModel::etmIndex(itemSelection.first()));
0744         if (m_editor->modified()) {
0745             caption.append(QStringLiteral(" *"));
0746         }
0747     } else if (itemSelection.size() == 0 && collectionSelection.size() == 1) {
0748         caption = KJotsModel::itemPath(collectionSelection.first());
0749     } else if (itemSelection.size() > 1 || collectionSelection.size() > 1) {
0750         caption = i18nc("@title:window", "Multiple selection");
0751     }
0752 
0753     Q_EMIT captionChanged(caption);
0754 }
0755 
0756 bool KJotsWidget::queryClose()
0757 {
0758     // Saving the current note
0759     // We cannot use async interface (i.e. ETM) here
0760     // because we need to abort the close if something went wrong
0761     const QModelIndexList selection = m_itemView->selectionModel()->selectedRows();
0762     if ((selection.size() == 1) && (m_editor->document()->isModified())) {
0763         QModelIndex idx = selection.first();
0764         m_editor->prepareDocumentForSaving();
0765         auto job = new ItemModifyJob(KJotsModel::updateItem(idx.data(EntityTreeModel::ItemRole).value<Item>(), m_editor->document()));
0766         if (!job->exec()) {
0767             int res = KMessageBox::warningContinueCancelDetailed(this,
0768                                                                  i18n("Unable to save the note.\n"
0769                                                                       "You can save your note to a local file using the \"File - Export\" menu,\n"
0770                                                                       "otherwise you will lose your changes!\n"
0771                                                                       "Do you want to close anyways?"),
0772                                                                  i18n("Close Document"),
0773                                                                  KStandardGuiItem::quit(),
0774                                                                  KStandardGuiItem::cancel(),
0775                                                                  QString(),
0776                                                                  KMessageBox::Notify,
0777                                                                  i18n("Error message:\n"
0778                                                                       "%1", job->errorString()));
0779             if (res == KMessageBox::Cancel) {
0780                 return false;
0781             }
0782         }
0783     }
0784 
0785     saveUIStates();
0786     KJotsSettings::self()->save();
0787     m_orderProxy->saveOrder();
0788 
0789     return true;
0790 }
0791 
0792 void KJotsWidget::openLink(const QUrl &url)
0793 {
0794     if (url.scheme() == QStringLiteral("akonadi")) {
0795         QModelIndex idx = KJotsModel::modelIndexForUrl(m_kjotsModel, url);
0796 
0797         // Trying to map it to collection view model
0798         QModelIndex colIdx = m_collectionModel->mapFromSource(idx);
0799         if (colIdx.isValid()) {
0800             colIdx = m_orderProxy->mapFromSource(colIdx);
0801             m_collectionView->selectionModel()->select(colIdx, QItemSelectionModel::SelectCurrent);
0802             m_itemView->selectionModel()->clearSelection();
0803         } else {
0804             // Selecting parent collection
0805             QModelIndex parentCollectionIdx = EntityTreeModel::modelIndexForCollection(m_collectionView->model(),
0806                                                                                        idx.data(EntityTreeModel::ParentCollectionRole).value<Collection>());
0807             m_collectionView->selectionModel()->select(parentCollectionIdx, QItemSelectionModel::SelectCurrent);
0808 
0809             // Mapping idx to item view model
0810             idx = m_collectionSelectionProxyModel->mapFromSource(idx);
0811             idx = m_itemModel->mapFromSource(idx);
0812             m_itemView->selectionModel()->select(idx, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
0813         }
0814     } else {
0815         auto job = new KIO::OpenUrlJob(url, this);
0816         job->start();
0817     }
0818 }
0819 
0820 #include "moc_kjotswidget.cpp"