File indexing completed on 2024-04-28 11:44:59
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 ¤t, 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"