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 | |
29 | class ClipboardHistoryModel : public QAbstractTableModel |
30 | { |
31 | public: |
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 | |
100 | private: |
101 | struct ClipboardEntry { |
102 | QString text; |
103 | QString fileName; |
104 | QIcon icon; |
105 | int dateSort; |
106 | }; |
107 | |
108 | QList<ClipboardEntry> m_modelEntries; |
109 | }; |
110 | |
111 | class ClipboardHistoryFilterModel : public QSortFilterProxyModel |
112 | { |
113 | public: |
114 | explicit ClipboardHistoryFilterModel(QObject *parent = nullptr) |
115 | : QSortFilterProxyModel(parent) |
116 | { |
117 | } |
118 | |
119 | protected: |
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 | |
128 | class SingleLineDelegate : public QStyledItemDelegate |
129 | { |
130 | public: |
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 | |
155 | private: |
156 | QFont m_font; |
157 | QRegularExpression m_newLineRegExp; |
158 | }; |
159 | |
160 | ClipboardHistoryDialog::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 ¤t, 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 | |
246 | void 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 | |
259 | void ClipboardHistoryDialog::resetValues() |
260 | { |
261 | m_lineEdit.setPlaceholderText(i18n("Select text to paste." )); |
262 | } |
263 | |
264 | void 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 | |
280 | void 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 | |
304 | void 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 | |
313 | bool 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 | |
352 | void 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 | |
371 | void 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 | |