File indexing completed on 2024-04-28 15:30:22

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