1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Copyright (C) 2020 Francesc M.
5** Contact: https://www.qt.io/licensing/
6**
7** This file is part of the examples of the Qt Toolkit.
8**
9** $QT_BEGIN_LICENSE:BSD$
10** Commercial License Usage
11** Licensees holding valid commercial Qt licenses may use this file in
12** accordance with the commercial license agreement provided with the
13** Software or, alternatively, in accordance with the terms contained in
14** a written agreement between you and The Qt Company. For licensing terms
15** and conditions see https://www.qt.io/terms-conditions. For further
16** information use the contact form at https://www.qt.io/contact-us.
17**
18** BSD License Usage
19** Alternatively, you may use this file under the terms of the BSD license
20** as follows:
21**
22** "Redistribution and use in source and binary forms, with or without
23** modification, are permitted provided that the following conditions are
24** met:
25** * Redistributions of source code must retain the above copyright
26** notice, this list of conditions and the following disclaimer.
27** * Redistributions in binary form must reproduce the above copyright
28** notice, this list of conditions and the following disclaimer in
29** the documentation and/or other materials provided with the
30** distribution.
31** * Neither the name of The Qt Company Ltd nor the names of its
32** contributors may be used to endorse or promote products derived
33** from this software without specific prior written permission.
34**
35**
36** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
37** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
38** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
39** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
40** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
41** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
42** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
43** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
44** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
45** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
46** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
47**
48** $QT_END_LICENSE$
49**
50****************************************************************************/
51
52#include "FileDiffView.h"
53
54#include <FileDiffHighlighter.h>
55#include <LineNumberArea.h>
56
57#include <QLogger.h>
58
59#include <QScrollBar>
60#include <QMenu>
61
62using namespace QLogger;
63
64FileDiffView::FileDiffView(QWidget *parent)
65 : QPlainTextEdit(parent)
66 , mDiffHighlighter(new FileDiffHighlighter(document()))
67{
68 setAttribute(Qt::WA_DeleteOnClose);
69 setReadOnly(true);
70 setContextMenuPolicy(Qt::CustomContextMenu);
71
72 connect(this, &FileDiffView::customContextMenuRequested, this, &FileDiffView::showStagingMenu);
73 connect(this, &FileDiffView::blockCountChanged, this, &FileDiffView::updateLineNumberAreaWidth);
74 connect(this, &FileDiffView::updateRequest, this, &FileDiffView::updateLineNumberArea);
75 connect(verticalScrollBar(), &QScrollBar::valueChanged, this, &FileDiffView::signalScrollChanged);
76}
77
78FileDiffView::~FileDiffView()
79{
80 delete mDiffHighlighter;
81}
82
83void FileDiffView::addNumberArea(LineNumberArea *numberArea)
84{
85 mLineNumberArea = numberArea;
86
87 if (mLineNumberArea->commentsAllowed())
88 {
89 installEventFilter(this);
90 setMouseTracking(true);
91 }
92}
93
94void FileDiffView::loadDiff(const QString &text, const QVector<ChunkDiffInfo::ChunkInfo> &fileDiffInfo)
95{
96 QLog_Trace("UI",
97 QString("FileDiffView::loadDiff - {%1} move scroll to pos {%2}")
98 .arg(objectName(), QString::number(verticalScrollBar()->value())));
99
100 mFileDiffInfo = fileDiffInfo;
101
102 mDiffHighlighter->setDiffInfo(mFileDiffInfo);
103
104 const auto pos = verticalScrollBar()->value();
105 auto cursor = textCursor();
106 const auto tmpCursor = textCursor().position();
107 setPlainText(text);
108
109 cursor.setPosition(tmpCursor);
110 setTextCursor(cursor);
111
112 blockSignals(true);
113 verticalScrollBar()->setValue(pos);
114 blockSignals(false);
115
116 emit updateRequest(viewport()->rect(), 0);
117
118 QLog_Trace("UI",
119 QString("FileDiffView::loadDiff - {%1} move scroll to pos {%2}").arg(objectName(), QString::number(pos)));
120}
121
122void FileDiffView::moveScrollBarToPos(int value)
123{
124 blockSignals(true);
125 verticalScrollBar()->setValue(value);
126 blockSignals(false);
127
128 emit updateRequest(viewport()->rect(), 0);
129
130 QLog_Trace("UI",
131 QString("FileDiffView::moveScrollBarToPos - {%1} move scroll to pos {%2}")
132 .arg(objectName(), QString::number(value)));
133}
134
135int FileDiffView::getHeight() const
136{
137 auto block = firstVisibleBlock();
138 auto height = 0;
139
140 while (block.isValid())
141 {
142 height += blockBoundingRect(block).height();
143 block = block.next();
144 }
145
146 return height;
147}
148
149int FileDiffView::getLineHeigth() const
150{
151 return blockBoundingRect(firstVisibleBlock()).height();
152}
153
154int FileDiffView::lineNumberAreaWidth()
155{
156 const auto width = fontMetrics().horizontalAdvance(QLatin1Char('9'));
157 auto digits = mLineNumberArea ? mLineNumberArea->widthInDigitsSize() : 0;
158 auto max = blockCount() + mStartingLine;
159
160 while (max >= 10)
161 {
162 max /= 10;
163 ++digits;
164 }
165
166 auto offset = 0;
167
168 if (mLineNumberArea && mLineNumberArea->commentsAllowed())
169 {
170 const auto padding = 3;
171 offset = (fontMetrics().height() * 2 + padding * 4);
172 }
173
174 return offset + (width * digits);
175}
176
177void FileDiffView::updateLineNumberAreaWidth(int /* newBlockCount */)
178{
179 setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
180}
181
182void FileDiffView::updateLineNumberArea(const QRect &rect, int dy)
183{
184 if (mLineNumberArea)
185 {
186 if (dy != 0)
187 mLineNumberArea->scroll(0, dy);
188 else
189 mLineNumberArea->update(0, rect.y(), mLineNumberArea->width(), rect.height());
190
191 if (rect.contains(viewport()->rect()))
192 updateLineNumberAreaWidth(0);
193 }
194}
195
196void FileDiffView::resizeEvent(QResizeEvent *e)
197{
198 QPlainTextEdit::resizeEvent(e);
199
200 if (mLineNumberArea)
201 {
202 const auto cr = contentsRect();
203
204 mLineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
205 }
206}
207
208bool FileDiffView::eventFilter(QObject *obj, QEvent *event)
209{
210 if (mLineNumberArea && event->type() == QEvent::Enter)
211 {
212 const auto height = mLineNumberArea->width();
213 const auto helpPos = mapFromGlobal(QCursor::pos());
214 const auto x = helpPos.x();
215 if (x >= 0 && x <= height)
216 {
217 QTextCursor cursor = cursorForPosition(helpPos);
218 const auto textRow = cursor.block().blockNumber();
219 auto found = false;
220
221 for (const auto &diff : qAsConst(mFileDiffInfo))
222 {
223 if (textRow + 1 >= diff.startLine && textRow + 1 <= diff.endLine)
224 {
225 mRow = textRow + mStartingLine + 1;
226 found = true;
227 break;
228 }
229 }
230
231 if (!found)
232 mRow = -1;
233
234 repaint();
235 }
236 }
237 else if (event->type() == QEvent::Leave)
238 {
239 mRow = -1;
240 repaint();
241 }
242
243 return QPlainTextEdit::eventFilter(obj, event);
244}
245
246void FileDiffView::showStagingMenu(const QPoint &cursorPos)
247{
248 const auto helpPos = mapFromGlobal(QCursor::pos());
249 const auto x = helpPos.x();
250 auto row = -1;
251
252 if (x >= 0 && x > mLineNumberArea->width())
253 {
254 const auto cursor = cursorForPosition(helpPos);
255 row = cursor.block().blockNumber() + mStartingLine + 1;
256
257 if (row != -1)
258 {
259 const auto chunk
260 = std::find_if(mFileDiffInfo.cbegin(), mFileDiffInfo.cend(), [row](const ChunkDiffInfo::ChunkInfo &chunk) {
261 return chunk.startLine <= row && row <= chunk.endLine;
262 });
263
264 if (chunk != mFileDiffInfo.cend())
265 {
266 const auto menu = new QMenu(this);
267 /*
268 const auto stageLine = menu->addAction(tr("Stage line"));
269 connect(stageLine, &QAction::triggered, this, &FileDiffView::stageLine);
270 */
271
272 const auto stageChunk = menu->addAction(tr("Stage chunk"));
273 connect(stageChunk, &QAction::triggered, this,
274 [this, chunkId = chunk->id]() { emit signalStageChunk(chunkId); });
275
276 menu->move(viewport()->mapToGlobal(cursorPos));
277 menu->exec();
278 }
279 }
280 }
281}
282