File indexing completed on 2024-03-24 04:00:11
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 ¤t, 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 // --------------------------------------------------