1/*
2 SPDX-FileCopyrightText: 2022 Eric Armbruster <eric1@armbruster-online.de>
3 SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "clipboardhistorydialog.h"
9#include "kateconfig.h"
10#include "katedocument.h"
11#include "kateview.h"
12
13#include <QBoxLayout>
14#include <QCoreApplication>
15#include <QFont>
16#include <QGraphicsOpacityEffect>
17#include <QItemSelectionModel>
18#include <QKeyEvent>
19#include <QMimeDatabase>
20#include <QSortFilterProxyModel>
21#include <QStyledItemDelegate>
22#include <QVBoxLayout>
23
24#include <KLocalizedString>
25#include <KSyntaxHighlighting/Definition>
26#include <KSyntaxHighlighting/Repository>
27#include <KTextEditor/Editor>
28
29class ClipboardHistoryModel : public QAbstractTableModel
30{
31public:
32 enum Role {
33 HighlightingRole = Qt::UserRole + 1,
34 OriginalSorting
35 };
36
37 explicit ClipboardHistoryModel(QObject *parent)
38 : QAbstractTableModel(parent)
39 {
40 }
41
42 int rowCount(const QModelIndex &parent) const override
43 {
44 if (parent.isValid()) {
45 return 0;
46 }
47 return m_modelEntries.size();
48 }
49
50 int columnCount(const QModelIndex &parent) const override
51 {
52 Q_UNUSED(parent);
53 return 1;
54 }
55
56 QVariant data(const QModelIndex &idx, int role) const override
57 {
58 if (!idx.isValid()) {
59 return {};
60 }
61
62 const ClipboardEntry &clipboardEntry = m_modelEntries.at(i: idx.row());
63 if (role == Qt::DisplayRole) {
64 return clipboardEntry.text;
65 } else if (role == Role::HighlightingRole) {
66 return clipboardEntry.fileName;
67 } else if (role == Qt::DecorationRole) {
68 return clipboardEntry.icon;
69 } else if (role == Role::OriginalSorting) {
70 return clipboardEntry.dateSort;
71 }
72
73 return {};
74 }
75
76 void refresh(const QList<KTextEditor::EditorPrivate::ClipboardEntry> &clipboardEntry)
77 {
78 QList<ClipboardEntry> temp;
79
80 for (int i = 0; i < clipboardEntry.size(); ++i) {
81 const auto entry = clipboardEntry.at(i);
82
83 auto icon = QIcon::fromTheme(name: QMimeDatabase().mimeTypeForFile(fileName: entry.fileName).iconName());
84 if (icon.isNull()) {
85 icon = QIcon::fromTheme(QStringLiteral("text-plain"));
86 }
87
88 temp.append(t: {.text = entry.text, .fileName = entry.fileName, .icon = icon, .dateSort = i});
89 }
90
91 beginResetModel();
92 m_modelEntries = std::move(temp);
93 endResetModel();
94 }
95
96 void clear()
97 {
98 beginResetModel();
99 QList<ClipboardEntry>().swap(other&: m_modelEntries);
100 endResetModel();
101 }
102
103private:
104 struct ClipboardEntry {
105 QString text;
106 QString fileName;
107 QIcon icon;
108 int dateSort;
109 };
110
111 QList<ClipboardEntry> m_modelEntries;
112};
113
114class ClipboardHistoryFilterModel : public QSortFilterProxyModel
115{
116public:
117 explicit ClipboardHistoryFilterModel(QObject *parent = nullptr)
118 : QSortFilterProxyModel(parent)
119 {
120 }
121
122protected:
123 bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override
124 {
125 const int l = sourceLeft.data(arole: ClipboardHistoryModel::OriginalSorting).toInt();
126 const int r = sourceRight.data(arole: ClipboardHistoryModel::OriginalSorting).toInt();
127 return l > r;
128 }
129};
130
131class SingleLineDelegate : public QStyledItemDelegate
132{
133public:
134 explicit SingleLineDelegate(const QFont &font)
135 : QStyledItemDelegate(nullptr)
136 , m_font(font)
137 , m_newLineRegExp(QStringLiteral("\\n|\\r|\u2028"), QRegularExpression::UseUnicodePropertiesOption)
138 {
139 }
140
141 void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override
142 {
143 QStyledItemDelegate::initStyleOption(option, index);
144 option->font = m_font;
145 }
146
147 QString displayText(const QVariant &value, const QLocale &locale) const override
148 {
149 QString baseText = QStyledItemDelegate::displayText(value, locale).trimmed();
150 auto endOfLine = baseText.indexOf(re: m_newLineRegExp, from: 0);
151 if (endOfLine != -1) {
152 baseText.truncate(pos: endOfLine);
153 }
154
155 return baseText;
156 }
157
158private:
159 QFont m_font;
160 QRegularExpression m_newLineRegExp;
161};
162
163ClipboardHistoryDialog::ClipboardHistoryDialog(QWidget *mainWindow, KTextEditor::ViewPrivate *viewPrivate)
164 : QMenu(mainWindow)
165 , m_mainWindow(mainWindow)
166 , m_viewPrivate(viewPrivate)
167 , m_model(new ClipboardHistoryModel(this))
168 , m_proxyModel(new ClipboardHistoryFilterModel(this))
169 , m_selectedDoc(new KTextEditor::DocumentPrivate)
170{
171 // --------------------------------------------------
172 // start of copy from Kate quickdialog.cpp (slight changes)
173 // --------------------------------------------------
174
175 QVBoxLayout *layout = new QVBoxLayout();
176 layout->setSpacing(0);
177 layout->setContentsMargins(left: 4, top: 4, right: 4, bottom: 4);
178 setLayout(layout);
179
180 setFocusProxy(&m_lineEdit);
181
182 layout->addWidget(&m_lineEdit);
183
184 layout->addWidget(&m_treeView, stretch: 2);
185 m_treeView.setTextElideMode(Qt::ElideLeft);
186 m_treeView.setUniformRowHeights(true);
187
188 connect(sender: &m_lineEdit, signal: &QLineEdit::returnPressed, context: this, slot: &ClipboardHistoryDialog::slotReturnPressed);
189 // user can add this as necessary
190 // connect(m_lineEdit, &QLineEdit::textChanged, delegate, &StyleDelegate::setFilterString);
191 connect(sender: &m_lineEdit, signal: &QLineEdit::textChanged, context: this, slot: [this]() {
192 m_treeView.viewport()->update();
193 });
194 connect(sender: &m_treeView, signal: &QTreeView::doubleClicked, context: this, slot: &ClipboardHistoryDialog::slotReturnPressed);
195 m_treeView.setSortingEnabled(true);
196
197 m_treeView.setHeaderHidden(true);
198 m_treeView.setRootIsDecorated(false);
199 m_treeView.setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
200 m_treeView.setSelectionMode(QTreeView::SingleSelection);
201
202 updateViewGeometry();
203 setFocus();
204
205 // --------------------------------------------------
206 // end of copy from Kate quickdialog.cpp
207 // --------------------------------------------------
208
209 m_proxyModel->setSourceModel(m_model);
210 m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
211
212 const QFont font = viewPrivate->rendererConfig()->baseFont();
213
214 m_treeView.setModel(m_proxyModel);
215 m_treeView.setItemDelegate(new SingleLineDelegate(font));
216 m_treeView.setTextElideMode(Qt::ElideRight);
217
218 m_selectedDoc->setParent(this);
219 m_selectedView = new KTextEditor::ViewPrivate(m_selectedDoc, this);
220 m_selectedView->setStatusBarEnabled(false);
221 m_selectedView->setLineNumbersOn(false);
222 m_selectedView->setFoldingMarkersOn(false);
223 m_selectedView->setIconBorder(false);
224 m_selectedView->setScrollBarMarks(false);
225 m_selectedView->setScrollBarMiniMap(false);
226
227 layout->addWidget(m_selectedView, stretch: 3);
228
229 m_lineEdit.setFont(font);
230
231 connect(sender: m_treeView.selectionModel(), signal: &QItemSelectionModel::currentRowChanged, context: this, slot: [this](const QModelIndex &current, const QModelIndex &previous) {
232 Q_UNUSED(previous);
233 showSelectedText(idx: current);
234 });
235
236 connect(sender: &m_lineEdit, signal: &QLineEdit::textChanged, context: this, slot: [this](const QString &s) {
237 m_proxyModel->setFilterFixedString(s);
238
239 const auto bestMatch = m_proxyModel->index(row: 0, column: 0);
240 m_treeView.setCurrentIndex(bestMatch);
241 showSelectedText(idx: bestMatch);
242 });
243
244 m_treeView.installEventFilter(filterObj: this);
245 m_lineEdit.installEventFilter(filterObj: this);
246 m_selectedView->installEventFilter(filterObj: this);
247}
248
249void ClipboardHistoryDialog::showSelectedText(const QModelIndex &idx)
250{
251 QString text = m_proxyModel->data(index: idx, role: Qt::DisplayRole).toString();
252 if (m_selectedDoc->text().isEmpty() || text != m_selectedDoc->text()) {
253 QString fileName = m_proxyModel->data(index: idx, role: ClipboardHistoryModel::Role::HighlightingRole).toString();
254 m_selectedDoc->setReadWrite(true);
255 m_selectedDoc->setText(text);
256 m_selectedDoc->setReadWrite(false);
257 const auto mode = KTextEditor::Editor::instance()->repository().definitionForFileName(fileName).name();
258 m_selectedDoc->setHighlightingMode(mode);
259 }
260}
261
262void ClipboardHistoryDialog::resetValues()
263{
264 m_lineEdit.setPlaceholderText(i18n("Select text to paste."));
265}
266
267void ClipboardHistoryDialog::openDialog(const QList<KTextEditor::EditorPrivate::ClipboardEntry> &clipboardHistory)
268{
269 m_model->refresh(clipboardEntry: clipboardHistory);
270 resetValues();
271
272 if (m_model->rowCount(parent: m_model->index(row: -1, column: -1)) == 0) {
273 showEmptyPlaceholder();
274 } else {
275 const auto first = m_proxyModel->index(row: 0, column: 0);
276 m_treeView.setCurrentIndex(first);
277 showSelectedText(idx: first);
278 }
279
280 exec();
281}
282
283void ClipboardHistoryDialog::showEmptyPlaceholder()
284{
285 QVBoxLayout *noRecentsLayout = new QVBoxLayout(&m_treeView);
286 m_treeView.setLayout(noRecentsLayout);
287 m_noEntries = new QLabel(&m_treeView);
288 QFont placeholderLabelFont;
289 // To match the size of a level 2 Heading/KTitleWidget
290 placeholderLabelFont.setPointSize(qRound(d: placeholderLabelFont.pointSize() * 1.3));
291 noRecentsLayout->addWidget(m_noEntries);
292 m_noEntries->setFont(placeholderLabelFont);
293 m_noEntries->setTextInteractionFlags(Qt::NoTextInteraction);
294 m_noEntries->setWordWrap(true);
295 m_noEntries->setAlignment(Qt::AlignCenter);
296 m_noEntries->setText(i18n("No entries in clipboard history"));
297 // Match opacity of QML placeholder label component
298 auto *effect = new QGraphicsOpacityEffect(m_noEntries);
299 effect->setOpacity(0.5);
300 m_noEntries->setGraphicsEffect(effect);
301}
302
303// --------------------------------------------------
304// start of copy from Kate quickdialog.cpp
305// --------------------------------------------------
306
307void ClipboardHistoryDialog::slotReturnPressed()
308{
309 const QString text = m_proxyModel->data(index: m_treeView.currentIndex(), role: Qt::DisplayRole).toString();
310 m_viewPrivate->paste(textToPaste: &text);
311
312 clearLineEdit();
313 hide();
314}
315
316bool ClipboardHistoryDialog::eventFilter(QObject *obj, QEvent *event)
317{
318 // catch key presses + shortcut overrides to allow to have ESC as application wide shortcut, too, see bug 409856
319 if (event->type() == QEvent::KeyPress || event->type() == QEvent::ShortcutOverride) {
320 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
321 if (obj == &m_lineEdit) {
322 const bool forward2list = (keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down) || (keyEvent->key() == Qt::Key_PageUp)
323 || (keyEvent->key() == Qt::Key_PageDown);
324 if (forward2list) {
325 QCoreApplication::sendEvent(receiver: &m_treeView, event);
326 return true;
327 }
328
329 if (keyEvent->key() == Qt::Key_Escape) {
330 clearLineEdit();
331 keyEvent->accept();
332 hide();
333 return true;
334 }
335 } else {
336 const bool forward2input = (keyEvent->key() != Qt::Key_Up) && (keyEvent->key() != Qt::Key_Down) && (keyEvent->key() != Qt::Key_PageUp)
337 && (keyEvent->key() != Qt::Key_PageDown) && (keyEvent->key() != Qt::Key_Tab) && (keyEvent->key() != Qt::Key_Backtab);
338 if (forward2input) {
339 QCoreApplication::sendEvent(receiver: &m_lineEdit, event);
340 return true;
341 }
342 }
343 }
344
345 // hide on focus out, if neither input field nor list have focus!
346 else if (event->type() == QEvent::FocusOut && !(m_lineEdit.hasFocus() || m_treeView.hasFocus() || m_selectedView->hasFocus())) {
347 clearLineEdit();
348 hide();
349 return true;
350 }
351
352 return QWidget::eventFilter(watched: obj, event);
353}
354
355void ClipboardHistoryDialog::updateViewGeometry()
356{
357 if (!m_mainWindow)
358 return;
359
360 const QSize centralSize = m_mainWindow->size();
361
362 // width: 2.4 of editor, height: 1/2 of editor
363 const QSize viewMaxSize(centralSize.width() / 2.4, centralSize.height() / 2);
364
365 // Position should be central over window
366 const int xPos = std::max(a: 0, b: (centralSize.width() - viewMaxSize.width()) / 2);
367 const int yPos = std::max(a: 0, b: (centralSize.height() - viewMaxSize.height()) * 1 / 4);
368 const QPoint p(xPos, yPos);
369 move(p + m_mainWindow->pos());
370
371 this->setFixedSize(viewMaxSize);
372}
373
374void ClipboardHistoryDialog::clearLineEdit()
375{
376 const QSignalBlocker block(m_lineEdit);
377 m_lineEdit.clear();
378}
379
380// --------------------------------------------------
381// end of copy from Kate quickdialog.cpp
382// --------------------------------------------------
383

source code of ktexteditor/src/dialogs/clipboardhistorydialog.cpp