File indexing completed on 2024-04-28 07:46:16

0001 /*
0002     SPDX-FileCopyrightText: 2022 Eric Armbruster <eric1@armbruster-online.de>
0003     SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "clipboardhistorydialog.h"
0009 #include "kateconfig.h"
0010 #include "katedocument.h"
0011 #include "kateview.h"
0012 
0013 #include <QBoxLayout>
0014 #include <QCoreApplication>
0015 #include <QFont>
0016 #include <QGraphicsOpacityEffect>
0017 #include <QItemSelectionModel>
0018 #include <QKeyEvent>
0019 #include <QMimeDatabase>
0020 #include <QSortFilterProxyModel>
0021 #include <QStyledItemDelegate>
0022 #include <QVBoxLayout>
0023 
0024 #include <KLocalizedString>
0025 #include <KSyntaxHighlighting/Definition>
0026 #include <KSyntaxHighlighting/Repository>
0027 #include <KTextEditor/Editor>
0028 
0029 class ClipboardHistoryModel : public QAbstractTableModel
0030 {
0031 public:
0032     enum Role { HighlightingRole = Qt::UserRole + 1, OriginalSorting };
0033 
0034     explicit ClipboardHistoryModel(QObject *parent)
0035         : QAbstractTableModel(parent)
0036     {
0037     }
0038 
0039     int rowCount(const QModelIndex &parent) const override
0040     {
0041         if (parent.isValid()) {
0042             return 0;
0043         }
0044         return m_modelEntries.size();
0045     }
0046 
0047     int columnCount(const QModelIndex &parent) const override
0048     {
0049         Q_UNUSED(parent);
0050         return 1;
0051     }
0052 
0053     QVariant data(const QModelIndex &idx, int role) const override
0054     {
0055         if (!idx.isValid()) {
0056             return {};
0057         }
0058 
0059         const ClipboardEntry &clipboardEntry = m_modelEntries.at(idx.row());
0060         if (role == Qt::DisplayRole) {
0061             return clipboardEntry.text;
0062         } else if (role == Role::HighlightingRole) {
0063             return clipboardEntry.fileName;
0064         } else if (role == Qt::DecorationRole) {
0065             return clipboardEntry.icon;
0066         } else if (role == Role::OriginalSorting) {
0067             return clipboardEntry.dateSort;
0068         }
0069 
0070         return {};
0071     }
0072 
0073     void refresh(const QList<KTextEditor::EditorPrivate::ClipboardEntry> &clipboardEntry)
0074     {
0075         QList<ClipboardEntry> temp;
0076 
0077         for (int i = 0; i < clipboardEntry.size(); ++i) {
0078             const auto entry = clipboardEntry.at(i);
0079 
0080             auto icon = QIcon::fromTheme(QMimeDatabase().mimeTypeForFile(entry.fileName).iconName());
0081             if (icon.isNull()) {
0082                 icon = QIcon::fromTheme(QStringLiteral("text-plain"));
0083             }
0084 
0085             temp.append({entry.text, entry.fileName, icon, i});
0086         }
0087 
0088         beginResetModel();
0089         m_modelEntries = std::move(temp);
0090         endResetModel();
0091     }
0092 
0093     void clear()
0094     {
0095         beginResetModel();
0096         QList<ClipboardEntry>().swap(m_modelEntries);
0097         endResetModel();
0098     }
0099 
0100 private:
0101     struct ClipboardEntry {
0102         QString text;
0103         QString fileName;
0104         QIcon icon;
0105         int dateSort;
0106     };
0107 
0108     QList<ClipboardEntry> m_modelEntries;
0109 };
0110 
0111 class ClipboardHistoryFilterModel : public QSortFilterProxyModel
0112 {
0113 public:
0114     explicit ClipboardHistoryFilterModel(QObject *parent = nullptr)
0115         : QSortFilterProxyModel(parent)
0116     {
0117     }
0118 
0119 protected:
0120     bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override
0121     {
0122         const int l = sourceLeft.data(ClipboardHistoryModel::OriginalSorting).toInt();
0123         const int r = sourceRight.data(ClipboardHistoryModel::OriginalSorting).toInt();
0124         return l > r;
0125     }
0126 };
0127 
0128 class SingleLineDelegate : public QStyledItemDelegate
0129 {
0130 public:
0131     explicit SingleLineDelegate(const QFont &font)
0132         : QStyledItemDelegate(nullptr)
0133         , m_font(font)
0134         , m_newLineRegExp(QStringLiteral("\\n|\\r|\u2028"), QRegularExpression::UseUnicodePropertiesOption)
0135     {
0136     }
0137 
0138     void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override
0139     {
0140         QStyledItemDelegate::initStyleOption(option, index);
0141         option->font = m_font;
0142     }
0143 
0144     QString displayText(const QVariant &value, const QLocale &locale) const override
0145     {
0146         QString baseText = QStyledItemDelegate::displayText(value, locale).trimmed();
0147         auto endOfLine = baseText.indexOf(m_newLineRegExp, 0);
0148         if (endOfLine != -1) {
0149             baseText.truncate(endOfLine);
0150         }
0151 
0152         return baseText;
0153     }
0154 
0155 private:
0156     QFont m_font;
0157     QRegularExpression m_newLineRegExp;
0158 };
0159 
0160 ClipboardHistoryDialog::ClipboardHistoryDialog(QWidget *mainWindow, KTextEditor::ViewPrivate *viewPrivate)
0161     : QMenu(mainWindow)
0162     , m_mainWindow(mainWindow)
0163     , m_viewPrivate(viewPrivate)
0164     , m_model(new ClipboardHistoryModel(this))
0165     , m_proxyModel(new ClipboardHistoryFilterModel(this))
0166     , m_selectedDoc(new KTextEditor::DocumentPrivate)
0167 {
0168     // --------------------------------------------------
0169     // start of copy from Kate quickdialog.cpp (slight changes)
0170     // --------------------------------------------------
0171 
0172     QVBoxLayout *layout = new QVBoxLayout();
0173     layout->setSpacing(0);
0174     layout->setContentsMargins(4, 4, 4, 4);
0175     setLayout(layout);
0176 
0177     setFocusProxy(&m_lineEdit);
0178 
0179     layout->addWidget(&m_lineEdit);
0180 
0181     layout->addWidget(&m_treeView, 2);
0182     m_treeView.setTextElideMode(Qt::ElideLeft);
0183     m_treeView.setUniformRowHeights(true);
0184 
0185     connect(&m_lineEdit, &QLineEdit::returnPressed, this, &ClipboardHistoryDialog::slotReturnPressed);
0186     // user can add this as necessary
0187     //    connect(m_lineEdit, &QLineEdit::textChanged, delegate, &StyleDelegate::setFilterString);
0188     connect(&m_lineEdit, &QLineEdit::textChanged, this, [this]() {
0189         m_treeView.viewport()->update();
0190     });
0191     connect(&m_treeView, &QTreeView::doubleClicked, this, &ClipboardHistoryDialog::slotReturnPressed);
0192     m_treeView.setSortingEnabled(true);
0193 
0194     m_treeView.setHeaderHidden(true);
0195     m_treeView.setRootIsDecorated(false);
0196     m_treeView.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0197     m_treeView.setSelectionMode(QTreeView::SingleSelection);
0198 
0199     updateViewGeometry();
0200     setFocus();
0201 
0202     // --------------------------------------------------
0203     // end of copy from Kate quickdialog.cpp
0204     // --------------------------------------------------
0205 
0206     m_proxyModel->setSourceModel(m_model);
0207     m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
0208 
0209     const QFont font = viewPrivate->rendererConfig()->baseFont();
0210 
0211     m_treeView.setModel(m_proxyModel);
0212     m_treeView.setItemDelegate(new SingleLineDelegate(font));
0213     m_treeView.setTextElideMode(Qt::ElideRight);
0214 
0215     m_selectedDoc->setParent(this);
0216     m_selectedView = new KTextEditor::ViewPrivate(m_selectedDoc, this);
0217     m_selectedView->setStatusBarEnabled(false);
0218     m_selectedView->setLineNumbersOn(false);
0219     m_selectedView->setFoldingMarkersOn(false);
0220     m_selectedView->setIconBorder(false);
0221     m_selectedView->setScrollBarMarks(false);
0222     m_selectedView->setScrollBarMiniMap(false);
0223 
0224     layout->addWidget(m_selectedView, 3);
0225 
0226     m_lineEdit.setFont(font);
0227 
0228     connect(m_treeView.selectionModel(), &QItemSelectionModel::currentRowChanged, this, [this](const QModelIndex &current, const QModelIndex &previous) {
0229         Q_UNUSED(previous);
0230         showSelectedText(current);
0231     });
0232 
0233     connect(&m_lineEdit, &QLineEdit::textChanged, this, [this](const QString &s) {
0234         m_proxyModel->setFilterFixedString(s);
0235 
0236         const auto bestMatch = m_proxyModel->index(0, 0);
0237         m_treeView.setCurrentIndex(bestMatch);
0238         showSelectedText(bestMatch);
0239     });
0240 
0241     m_treeView.installEventFilter(this);
0242     m_lineEdit.installEventFilter(this);
0243     m_selectedView->installEventFilter(this);
0244 }
0245 
0246 void ClipboardHistoryDialog::showSelectedText(const QModelIndex &idx)
0247 {
0248     QString text = m_proxyModel->data(idx, Qt::DisplayRole).toString();
0249     if (m_selectedDoc->text().isEmpty() || text != m_selectedDoc->text()) {
0250         QString fileName = m_proxyModel->data(idx, ClipboardHistoryModel::Role::HighlightingRole).toString();
0251         m_selectedDoc->setReadWrite(true);
0252         m_selectedDoc->setText(text);
0253         m_selectedDoc->setReadWrite(false);
0254         const auto mode = KTextEditor::Editor::instance()->repository().definitionForFileName(fileName).name();
0255         m_selectedDoc->setHighlightingMode(mode);
0256     }
0257 }
0258 
0259 void ClipboardHistoryDialog::resetValues()
0260 {
0261     m_lineEdit.setPlaceholderText(i18n("Select text to paste."));
0262 }
0263 
0264 void ClipboardHistoryDialog::openDialog(const QList<KTextEditor::EditorPrivate::ClipboardEntry> &clipboardHistory)
0265 {
0266     m_model->refresh(clipboardHistory);
0267     resetValues();
0268 
0269     if (m_model->rowCount(m_model->index(-1, -1)) == 0) {
0270         showEmptyPlaceholder();
0271     } else {
0272         const auto first = m_proxyModel->index(0, 0);
0273         m_treeView.setCurrentIndex(first);
0274         showSelectedText(first);
0275     }
0276 
0277     exec();
0278 }
0279 
0280 void ClipboardHistoryDialog::showEmptyPlaceholder()
0281 {
0282     QVBoxLayout *noRecentsLayout = new QVBoxLayout(&m_treeView);
0283     m_treeView.setLayout(noRecentsLayout);
0284     m_noEntries = new QLabel(&m_treeView);
0285     QFont placeholderLabelFont;
0286     // To match the size of a level 2 Heading/KTitleWidget
0287     placeholderLabelFont.setPointSize(qRound(placeholderLabelFont.pointSize() * 1.3));
0288     noRecentsLayout->addWidget(m_noEntries);
0289     m_noEntries->setFont(placeholderLabelFont);
0290     m_noEntries->setTextInteractionFlags(Qt::NoTextInteraction);
0291     m_noEntries->setWordWrap(true);
0292     m_noEntries->setAlignment(Qt::AlignCenter);
0293     m_noEntries->setText(i18n("No entries in clipboard history"));
0294     // Match opacity of QML placeholder label component
0295     auto *effect = new QGraphicsOpacityEffect(m_noEntries);
0296     effect->setOpacity(0.5);
0297     m_noEntries->setGraphicsEffect(effect);
0298 }
0299 
0300 // --------------------------------------------------
0301 // start of copy from Kate quickdialog.cpp
0302 // --------------------------------------------------
0303 
0304 void ClipboardHistoryDialog::slotReturnPressed()
0305 {
0306     const QString text = m_proxyModel->data(m_treeView.currentIndex(), Qt::DisplayRole).toString();
0307     m_viewPrivate->paste(&text);
0308 
0309     clearLineEdit();
0310     hide();
0311 }
0312 
0313 bool ClipboardHistoryDialog::eventFilter(QObject *obj, QEvent *event)
0314 {
0315     // catch key presses + shortcut overrides to allow to have ESC as application wide shortcut, too, see bug 409856
0316     if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) {
0317         QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
0318         if (obj == &m_lineEdit) {
0319             const bool forward2list = (keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_PageUp)
0320                 || (keyEvent->key() == Qt::Key_PageDown);
0321             if (forward2list) {
0322                 QCoreApplication::sendEvent(&m_treeView, event);
0323                 return true;
0324             }
0325 
0326             if (keyEvent->key() == Qt::Key_Escape) {
0327                 clearLineEdit();
0328                 keyEvent->accept();
0329                 hide();
0330                 return true;
0331             }
0332         } else {
0333             const bool forward2input = (keyEvent->key() != Qt::Key_Up) && (keyEvent->key() != Qt::Key_Down) && (keyEvent->key() != Qt::Key_PageUp)
0334                 && (keyEvent->key() != Qt::Key_PageDown) && (keyEvent->key() != Qt::Key_Tab) && (keyEvent->key() != Qt::Key_Backtab);
0335             if (forward2input) {
0336                 QCoreApplication::sendEvent(&m_lineEdit, event);
0337                 return true;
0338             }
0339         }
0340     }
0341 
0342     // hide on focus out, if neither input field nor list have focus!
0343     else if (event->type() == QEvent::FocusOut && !(m_lineEdit.hasFocus() || m_treeView.hasFocus() || m_selectedView->hasFocus())) {
0344         clearLineEdit();
0345         hide();
0346         return true;
0347     }
0348 
0349     return QWidget::eventFilter(obj, event);
0350 }
0351 
0352 void ClipboardHistoryDialog::updateViewGeometry()
0353 {
0354     if (!m_mainWindow)
0355         return;
0356 
0357     const QSize centralSize = m_mainWindow->size();
0358 
0359     // width: 2.4 of editor, height: 1/2 of editor
0360     const QSize viewMaxSize(centralSize.width() / 2.4, centralSize.height() / 2);
0361 
0362     // Position should be central over window
0363     const int xPos = std::max(0, (centralSize.width() - viewMaxSize.width()) / 2);
0364     const int yPos = std::max(0, (centralSize.height() - viewMaxSize.height()) * 1 / 4);
0365     const QPoint p(xPos, yPos);
0366     move(p + m_mainWindow->pos());
0367 
0368     this->setFixedSize(viewMaxSize);
0369 }
0370 
0371 void ClipboardHistoryDialog::clearLineEdit()
0372 {
0373     const QSignalBlocker block(m_lineEdit);
0374     m_lineEdit.clear();
0375 }
0376 
0377 // --------------------------------------------------
0378 // end of copy from Kate quickdialog.cpp
0379 // --------------------------------------------------