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