1 /*
2 SPDX-FileCopyrightText: 2021 Waqar Ahmed <waqar.17a@gmail.com>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 #include "branchesdialog.h"
7 #include "branchesdialogmodel.h"
8 #include "kateprojectpluginview.h"
9
10 #include <QCoreApplication>
11 #include <QKeyEvent>
12 #include <QLineEdit>
13 #include <QPainter>
14 #include <QSortFilterProxyModel>
15 #include <QStyledItemDelegate>
16 #include <QTextDocument>
17 #include <QTreeView>
18 #include <QVBoxLayout>
19 #include <QWidget>
20 #include <QtConcurrentRun>
21
22 #include <KTextEditor/MainWindow>
23 #include <KTextEditor/Message>
24 #include <KTextEditor/View>
25
26 #include <KLocalizedString>
27
28 #include <kfts_fuzzy_match.h>
29
30 class BranchFilterModel : public QSortFilterProxyModel
31 {
32 public:
BranchFilterModel(QObject * parent=nullptr)33 BranchFilterModel(QObject *parent = nullptr)
34 : QSortFilterProxyModel(parent)
35 {
36 }
37
setFilterString(const QString & string)38 Q_SLOT void setFilterString(const QString &string)
39 {
40 beginResetModel();
41 m_pattern = string;
42 endResetModel();
43 }
44
45 protected:
lessThan(const QModelIndex & sourceLeft,const QModelIndex & sourceRight) const46 bool lessThan(const QModelIndex &sourceLeft, const QModelIndex &sourceRight) const override
47 {
48 if (m_pattern.isEmpty()) {
49 const int l = sourceLeft.data(BranchesDialogModel::OriginalSorting).toInt();
50 const int r = sourceRight.data(BranchesDialogModel::OriginalSorting).toInt();
51 return l > r;
52 }
53 const int l = sourceLeft.data(BranchesDialogModel::FuzzyScore).toInt();
54 const int r = sourceRight.data(BranchesDialogModel::FuzzyScore).toInt();
55 return l < r;
56 }
57
filterAcceptsRow(int sourceRow,const QModelIndex & sourceParent) const58 bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
59 {
60 if (m_pattern.isEmpty()) {
61 return true;
62 }
63
64 int score = 0;
65 const auto idx = sourceModel()->index(sourceRow, 0, sourceParent);
66 const QString string = idx.data().toString();
67 const bool res = kfts::fuzzy_match(m_pattern, string, score);
68 sourceModel()->setData(idx, score, BranchesDialogModel::FuzzyScore);
69 return res;
70 }
71
72 private:
73 QString m_pattern;
74 };
75
76 class StyleDelegate : public QStyledItemDelegate
77 {
78 public:
StyleDelegate(QObject * parent=nullptr)79 StyleDelegate(QObject *parent = nullptr)
80 : QStyledItemDelegate(parent)
81 {
82 }
83
paint(QPainter * painter,const QStyleOptionViewItem & option,const QModelIndex & index) const84 void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
85 {
86 QStyleOptionViewItem options = option;
87 initStyleOption(&options, index);
88
89 auto name = index.data().toString();
90
91 QVector<QTextLayout::FormatRange> formats;
92 QTextCharFormat fmt;
93 fmt.setForeground(options.palette.link());
94 fmt.setFontWeight(QFont::Bold);
95
96 const auto itemType = (BranchesDialogModel::ItemType)index.data(BranchesDialogModel::ItemTypeRole).toInt();
97 const bool branchItem = itemType == BranchesDialogModel::BranchItem;
98 const int offset = branchItem ? 0 : 2;
99
100 formats = kfts::get_fuzzy_match_formats(m_filterString, name, offset, fmt);
101
102 if (!branchItem) {
103 name = QStringLiteral("+ ") + name;
104 }
105
106 const int nameLen = name.length();
107 int len = 6;
108 if (branchItem) {
109 const auto refType = (GitUtils::RefType)index.data(BranchesDialogModel::RefType).toInt();
110 using RefType = GitUtils::RefType;
111 if (refType == RefType::Head) {
112 name.append(QStringLiteral(" local"));
113 } else if (refType == RefType::Remote) {
114 name.append(QStringLiteral(" remote"));
115 len = 7;
116 }
117 }
118 QTextCharFormat lf;
119 lf.setFontItalic(true);
120 lf.setForeground(Qt::gray);
121 formats.append({nameLen, len, lf});
122
123 painter->save();
124
125 // paint background
126 if (option.state & QStyle::State_Selected) {
127 painter->fillRect(option.rect, option.palette.highlight());
128 } else {
129 painter->fillRect(option.rect, option.palette.base());
130 }
131
132 options.text = QString(); // clear old text
133 options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter, options.widget);
134
135 // leave space for icon
136 if (itemType == BranchesDialogModel::BranchItem) {
137 painter->translate(25, 0);
138 }
139 kfts::paintItemViewText(painter, name, options, formats);
140
141 painter->restore();
142 }
143
144 public Q_SLOTS:
setFilterString(const QString & text)145 void setFilterString(const QString &text)
146 {
147 m_filterString = text;
148 }
149
150 private:
151 QString m_filterString;
152 };
153
BranchesDialog(QWidget * window,KateProjectPluginView * pluginView,QString projectPath)154 BranchesDialog::BranchesDialog(QWidget *window, KateProjectPluginView *pluginView, QString projectPath)
155 : QuickDialog(nullptr, window)
156 , m_projectPath(projectPath)
157 , m_pluginView(pluginView)
158 {
159 m_model = new BranchesDialogModel(this);
160 m_proxyModel = new BranchFilterModel(this);
161 m_proxyModel->setSourceModel(m_model);
162 m_treeView.setModel(m_proxyModel);
163
164 auto delegate = new StyleDelegate(this);
165
166 connect(&m_lineEdit, &QLineEdit::textChanged, this, [this, delegate](const QString &s) {
167 static_cast<BranchFilterModel *>(m_proxyModel)->setFilterString(s);
168 delegate->setFilterString(s);
169 });
170 }
171
openDialog(GitUtils::RefType r)172 void BranchesDialog::openDialog(GitUtils::RefType r)
173 {
174 m_lineEdit.setPlaceholderText(i18n("Select Branch..."));
175
176 QVector<GitUtils::Branch> branches = GitUtils::getAllBranchesAndTags(m_projectPath, r);
177 m_model->refresh(branches);
178
179 reselectFirst();
180 exec();
181 }
182
slotReturnPressed()183 void BranchesDialog::slotReturnPressed()
184 {
185 /** We want display role here */
186 const auto branch = m_proxyModel->data(m_treeView.currentIndex(), Qt::DisplayRole).toString();
187 const auto itemType = (BranchesDialogModel::ItemType)m_proxyModel->data(m_treeView.currentIndex(), BranchesDialogModel::ItemTypeRole).toInt();
188 Q_ASSERT(itemType == BranchesDialogModel::BranchItem);
189
190 m_branch = branch;
191 Q_EMIT branchSelected(branch);
192
193 clearLineEdit();
194 hide();
195 }
196
reselectFirst()197 void BranchesDialog::reselectFirst()
198 {
199 QModelIndex index = m_proxyModel->index(0, 0);
200 m_treeView.setCurrentIndex(index);
201 }
202
sendMessage(const QString & plainText,bool warn)203 void BranchesDialog::sendMessage(const QString &plainText, bool warn)
204 {
205 // use generic output view
206 QVariantMap genericMessage;
207 genericMessage.insert(QStringLiteral("type"), warn ? QStringLiteral("Error") : QStringLiteral("Info"));
208 genericMessage.insert(QStringLiteral("category"), i18n("Git"));
209 genericMessage.insert(QStringLiteral("categoryIcon"), QIcon(QStringLiteral(":/icons/icons/sc-apps-git.svg")));
210 genericMessage.insert(QStringLiteral("text"), plainText);
211 Q_EMIT m_pluginView->message(genericMessage);
212 }
213