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

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