1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "sidebysidediffeditorwidget.h"
27 #include "selectabletexteditorwidget.h"
28 #include "diffeditorconstants.h"
29 #include "diffeditordocument.h"
30 #include "diffutils.h"
31 
32 #include <QMenu>
33 #include <QPainter>
34 #include <QScrollBar>
35 #include <QTextBlock>
36 #include <QVBoxLayout>
37 
38 #include <texteditor/textdocument.h>
39 #include <texteditor/textdocumentlayout.h>
40 #include <texteditor/texteditorsettings.h>
41 #include <texteditor/fontsettings.h>
42 #include <texteditor/displaysettings.h>
43 
44 #include <coreplugin/icore.h>
45 #include <coreplugin/find/highlightscrollbarcontroller.h>
46 #include <coreplugin/minisplitter.h>
47 
48 #include <utils/porting.h>
49 #include <utils/tooltip/tooltip.h>
50 
51 using namespace Core;
52 using namespace TextEditor;
53 using namespace Utils;
54 
55 namespace DiffEditor {
56 namespace Internal {
57 
58 class SideDiffEditorWidget : public SelectableTextEditorWidget
59 {
60     Q_OBJECT
61 public:
62     SideDiffEditorWidget(QWidget *parent = nullptr);
63 
64     // block number, file info
fileInfo() const65     QMap<int, DiffFileInfo> fileInfo() const { return m_fileInfo; }
66 
67     void setLineNumber(int blockNumber, int lineNumber);
68     void setFileInfo(int blockNumber, const DiffFileInfo &fileInfo);
setSkippedLines(int blockNumber,int skippedLines,const QString & contextInfo=QString ())69     void setSkippedLines(int blockNumber, int skippedLines, const QString &contextInfo = QString()) {
70         m_skippedLines[blockNumber] = qMakePair(skippedLines, contextInfo);
71         setSeparator(blockNumber, true);
72     }
73     void setChunkIndex(int startBlockNumber, int blockCount, int chunkIndex);
setSeparator(int blockNumber,bool separator)74     void setSeparator(int blockNumber, bool separator) {
75         m_separators[blockNumber] = separator;
76     }
isFileLine(int blockNumber) const77     bool isFileLine(int blockNumber) const {
78         return m_fileInfo.contains(blockNumber);
79     }
80     int blockNumberForFileIndex(int fileIndex) const;
81     int fileIndexForBlockNumber(int blockNumber) const;
82     int chunkIndexForBlockNumber(int blockNumber) const;
83     int chunkRowForBlockNumber(int blockNumber) const;
84     int chunkRowsCountForBlockNumber(int blockNumber) const;
isChunkLine(int blockNumber) const85     bool isChunkLine(int blockNumber) const {
86         return m_skippedLines.contains(blockNumber);
87     }
88     void clearAll(const QString &message);
89     void clearAllData();
90     void saveState();
91     using TextEditor::TextEditorWidget::restoreState;
92     void restoreState();
93 
94     void setFolded(int blockNumber, bool folded);
95 
96     void setDisplaySettings(const DisplaySettings &ds) override;
97 
98 signals:
99     void jumpToOriginalFileRequested(int diffFileIndex,
100                                      int lineNumber,
101                                      int columnNumber);
102     void contextMenuRequested(QMenu *menu,
103                               int diffFileIndex,
104                               int chunkIndex,
105                               const ChunkSelection &selection);
106     void foldChanged(int blockNumber, bool folded);
107     void gotDisplaySettings();
108     void gotFocus();
109 
110 protected:
extraAreaWidth(int * markWidthPtr=nullptr) const111     int extraAreaWidth(int *markWidthPtr = nullptr) const override {
112         return SelectableTextEditorWidget::extraAreaWidth(markWidthPtr);
113     }
114     void applyFontSettings() override;
115 
116     QString lineNumber(int blockNumber) const override;
117     int lineNumberDigits() const override;
118     bool selectionVisible(int blockNumber) const override;
119     bool replacementVisible(int blockNumber) const override;
120     QColor replacementPenColor(int blockNumber) const override;
121     QString plainTextFromSelection(const QTextCursor &cursor) const override;
122     void mouseDoubleClickEvent(QMouseEvent *e) override;
123     void keyPressEvent(QKeyEvent *e) override;
124     void contextMenuEvent(QContextMenuEvent *e) override;
125     void paintEvent(QPaintEvent *e) override;
126     void scrollContentsBy(int dx, int dy) override;
127     void customDrawCollapsedBlockPopup(QPainter &painter,
128                                        const QTextBlock &block,
129                                        QPointF offset,
130                                        const QRect &clip);
131     void drawCollapsedBlockPopup(QPainter &painter,
132                                  const QTextBlock &block,
133                                  QPointF offset,
134                                  const QRect &clip) override;
135     void focusInEvent(QFocusEvent *e) override;
136 
137 private:
138     void paintSeparator(QPainter &painter, QColor &color, const QString &text,
139                         const QTextBlock &block, int top);
140     void jumpToOriginalFile(const QTextCursor &cursor);
141 
142     // block number, visual line number.
143     QMap<int, int> m_lineNumbers;
144     // block number, fileInfo. Set for file lines only.
145     QMap<int, DiffFileInfo> m_fileInfo;
146     // block number, skipped lines and context info. Set for chunk lines only.
147     QMap<int, QPair<int, QString> > m_skippedLines;
148     // start block number, block count of a chunk, chunk index inside a file.
149     QMap<int, QPair<int, int> > m_chunkInfo;
150     // block number, separator. Set for file, chunk or span line.
151     QMap<int, bool> m_separators;
152     QColor m_fileLineForeground;
153     QColor m_chunkLineForeground;
154     QColor m_textForeground;
155     QByteArray m_state;
156 
157     QTextBlock m_drawCollapsedBlock;
158     QPointF m_drawCollapsedOffset;
159     QRect m_drawCollapsedClip;
160     int m_lineNumberDigits = 1;
161 };
162 
SideDiffEditorWidget(QWidget * parent)163 SideDiffEditorWidget::SideDiffEditorWidget(QWidget *parent)
164     : SelectableTextEditorWidget("DiffEditor.SideDiffEditor", parent)
165 {
166     DisplaySettings settings = displaySettings();
167     settings.m_textWrapping = false;
168     settings.m_displayLineNumbers = true;
169     settings.m_markTextChanges = false;
170     settings.m_highlightBlocks = false;
171     SelectableTextEditorWidget::setDisplaySettings(settings);
172 
173     connect(this, &TextEditorWidget::tooltipRequested, this, [this](const QPoint &point, int position) {
174         const int block = document()->findBlock(position).blockNumber();
175         const auto it = m_fileInfo.constFind(block);
176         if (it != m_fileInfo.constEnd())
177             ToolTip::show(point, it.value().fileName, this);
178         else
179             ToolTip::hide();
180     });
181 
182     auto documentLayout = qobject_cast<TextDocumentLayout*>(document()->documentLayout());
183     if (documentLayout)
184         connect(documentLayout, &TextDocumentLayout::foldChanged,
185                 this, &SideDiffEditorWidget::foldChanged);
186     setCodeFoldingSupported(true);
187     setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
188 }
189 
saveState()190 void SideDiffEditorWidget::saveState()
191 {
192     if (!m_state.isNull())
193         return;
194 
195     m_state = SelectableTextEditorWidget::saveState();
196 }
197 
restoreState()198 void SideDiffEditorWidget::restoreState()
199 {
200     if (m_state.isNull())
201         return;
202 
203     SelectableTextEditorWidget::restoreState(m_state);
204     m_state.clear();
205 }
206 
setFolded(int blockNumber,bool folded)207 void SideDiffEditorWidget::setFolded(int blockNumber, bool folded)
208 {
209     QTextBlock block = document()->findBlockByNumber(blockNumber);
210     if (!block.isValid())
211         return;
212 
213     if (TextDocumentLayout::isFolded(block) == folded)
214         return;
215 
216     TextDocumentLayout::doFoldOrUnfold(block, !folded);
217 
218     auto documentLayout = qobject_cast<TextDocumentLayout*>(document()->documentLayout());
219     documentLayout->requestUpdate();
220     documentLayout->emitDocumentSizeChanged();
221 }
222 
setDisplaySettings(const DisplaySettings & ds)223 void SideDiffEditorWidget::setDisplaySettings(const DisplaySettings &ds)
224 {
225     DisplaySettings settings = displaySettings();
226     settings.m_visualizeWhitespace = ds.m_visualizeWhitespace;
227     settings.m_displayFoldingMarkers = ds.m_displayFoldingMarkers;
228     settings.m_scrollBarHighlights = ds.m_scrollBarHighlights;
229     settings.m_highlightCurrentLine = ds.m_highlightCurrentLine;
230     SelectableTextEditorWidget::setDisplaySettings(settings);
231     emit gotDisplaySettings();
232 }
233 
applyFontSettings()234 void SideDiffEditorWidget::applyFontSettings()
235 {
236     SelectableTextEditorWidget::applyFontSettings();
237     const FontSettings &fs = textDocument()->fontSettings();
238     m_fileLineForeground = fs.formatFor(C_DIFF_FILE_LINE).foreground();
239     m_chunkLineForeground = fs.formatFor(C_DIFF_CONTEXT_LINE).foreground();
240     m_textForeground = fs.toTextCharFormat(C_TEXT).foreground().color();
241     update();
242 }
243 
lineNumber(int blockNumber) const244 QString SideDiffEditorWidget::lineNumber(int blockNumber) const
245 {
246     const auto it = m_lineNumbers.constFind(blockNumber);
247     if (it != m_lineNumbers.constEnd())
248         return QString::number(it.value());
249     return QString();
250 }
251 
lineNumberDigits() const252 int SideDiffEditorWidget::lineNumberDigits() const
253 {
254     return m_lineNumberDigits;
255 }
256 
selectionVisible(int blockNumber) const257 bool SideDiffEditorWidget::selectionVisible(int blockNumber) const
258 {
259     return !m_separators.value(blockNumber, false);
260 }
261 
replacementVisible(int blockNumber) const262 bool SideDiffEditorWidget::replacementVisible(int blockNumber) const
263 {
264     return isChunkLine(blockNumber) || (isFileLine(blockNumber)
265            && TextDocumentLayout::isFolded(document()->findBlockByNumber(blockNumber)));
266 }
267 
replacementPenColor(int blockNumber) const268 QColor SideDiffEditorWidget::replacementPenColor(int blockNumber) const
269 {
270     Q_UNUSED(blockNumber)
271     return m_chunkLineForeground;
272 }
273 
plainTextFromSelection(const QTextCursor & cursor) const274 QString SideDiffEditorWidget::plainTextFromSelection(const QTextCursor &cursor) const
275 {
276     const int startPosition = cursor.selectionStart();
277     const int endPosition = cursor.selectionEnd();
278     if (startPosition == endPosition)
279         return QString(); // no selection
280 
281     const QTextBlock startBlock = document()->findBlock(startPosition);
282     const QTextBlock endBlock = document()->findBlock(endPosition);
283     QTextBlock block = startBlock;
284     QString text;
285     bool textInserted = false;
286     while (block.isValid() && block.blockNumber() <= endBlock.blockNumber()) {
287         if (selectionVisible(block.blockNumber())) {
288             if (block == startBlock) {
289                 if (block == endBlock)
290                     text = cursor.selectedText(); // just one line text
291                 else
292                     text = block.text().mid(startPosition - block.position());
293             } else {
294                 if (textInserted)
295                     text += '\n';
296                 if (block == endBlock)
297                     text += make_stringview(block.text()).left(endPosition - block.position());
298                 else
299                     text += block.text();
300             }
301             textInserted = true;
302         }
303         block = block.next();
304     }
305 
306     return convertToPlainText(text);
307 }
308 
setLineNumber(int blockNumber,int lineNumber)309 void SideDiffEditorWidget::setLineNumber(int blockNumber, int lineNumber)
310 {
311     const QString lineNumberString = QString::number(lineNumber);
312     m_lineNumbers.insert(blockNumber, lineNumber);
313     m_lineNumberDigits = qMax(m_lineNumberDigits, lineNumberString.count());
314 }
315 
setFileInfo(int blockNumber,const DiffFileInfo & fileInfo)316 void SideDiffEditorWidget::setFileInfo(int blockNumber, const DiffFileInfo &fileInfo)
317 {
318     m_fileInfo[blockNumber] = fileInfo;
319     setSeparator(blockNumber, true);
320 }
321 
setChunkIndex(int startBlockNumber,int blockCount,int chunkIndex)322 void SideDiffEditorWidget::setChunkIndex(int startBlockNumber, int blockCount, int chunkIndex)
323 {
324     m_chunkInfo.insert(startBlockNumber, qMakePair(blockCount, chunkIndex));
325 }
326 
blockNumberForFileIndex(int fileIndex) const327 int SideDiffEditorWidget::blockNumberForFileIndex(int fileIndex) const
328 {
329     if (fileIndex < 0 || fileIndex >= m_fileInfo.count())
330         return -1;
331 
332     return std::next(m_fileInfo.constBegin(), fileIndex).key();
333 }
334 
fileIndexForBlockNumber(int blockNumber) const335 int SideDiffEditorWidget::fileIndexForBlockNumber(int blockNumber) const
336 {
337     int i = -1;
338     for (auto it = m_fileInfo.cbegin(), end = m_fileInfo.cend(); it != end; ++it, ++i) {
339         if (it.key() > blockNumber)
340             break;
341     }
342 
343     return i;
344 }
345 
chunkIndexForBlockNumber(int blockNumber) const346 int SideDiffEditorWidget::chunkIndexForBlockNumber(int blockNumber) const
347 {
348     if (m_chunkInfo.isEmpty())
349         return -1;
350 
351     auto it = m_chunkInfo.upperBound(blockNumber);
352     if (it == m_chunkInfo.constBegin())
353         return -1;
354 
355     --it;
356 
357     if (blockNumber < it.key() + it.value().first)
358         return it.value().second;
359 
360     return -1;
361 }
362 
chunkRowForBlockNumber(int blockNumber) const363 int SideDiffEditorWidget::chunkRowForBlockNumber(int blockNumber) const
364 {
365     if (m_chunkInfo.isEmpty())
366         return -1;
367 
368     auto it = m_chunkInfo.upperBound(blockNumber);
369     if (it == m_chunkInfo.constBegin())
370         return -1;
371 
372     --it;
373 
374     if (blockNumber < it.key() + it.value().first)
375         return blockNumber - it.key();
376 
377     return -1;
378 }
379 
chunkRowsCountForBlockNumber(int blockNumber) const380 int SideDiffEditorWidget::chunkRowsCountForBlockNumber(int blockNumber) const
381 {
382     if (m_chunkInfo.isEmpty())
383         return -1;
384 
385     auto it = m_chunkInfo.upperBound(blockNumber);
386     if (it == m_chunkInfo.constBegin())
387         return -1;
388 
389     --it;
390 
391     if (blockNumber < it.key() + it.value().first)
392         return it.value().first;
393 
394     return -1;
395 }
396 
clearAll(const QString & message)397 void SideDiffEditorWidget::clearAll(const QString &message)
398 {
399     setBlockSelection(false);
400     clear();
401     clearAllData();
402     setExtraSelections(TextEditorWidget::OtherSelection,
403                        QList<QTextEdit::ExtraSelection>());
404     setPlainText(message);
405 }
406 
clearAllData()407 void SideDiffEditorWidget::clearAllData()
408 {
409     m_lineNumberDigits = 1;
410     m_lineNumbers.clear();
411     m_fileInfo.clear();
412     m_skippedLines.clear();
413     m_chunkInfo.clear();
414     m_separators.clear();
415     setSelections(QMap<int, QList<DiffSelection> >());
416 }
417 
scrollContentsBy(int dx,int dy)418 void SideDiffEditorWidget::scrollContentsBy(int dx, int dy)
419 {
420     SelectableTextEditorWidget::scrollContentsBy(dx, dy);
421     // TODO: update only chunk lines
422     viewport()->update();
423 }
424 
paintSeparator(QPainter & painter,QColor & color,const QString & text,const QTextBlock & block,int top)425 void SideDiffEditorWidget::paintSeparator(QPainter &painter,
426                                           QColor &color,
427                                           const QString &text,
428                                           const QTextBlock &block,
429                                           int top)
430 {
431     QPointF offset = contentOffset();
432     painter.save();
433 
434     QColor foreground = color;
435     if (!foreground.isValid())
436         foreground = m_textForeground;
437     if (!foreground.isValid())
438         foreground = palette().windowText().color();
439 
440     painter.setPen(foreground);
441 
442     const QString replacementText = " {" + foldReplacementText(block) + "}; ";
443     const int replacementTextWidth = fontMetrics().horizontalAdvance(replacementText) + 24;
444     int x = replacementTextWidth + int(offset.x());
445     if (x < document()->documentMargin()
446             || !TextDocumentLayout::isFolded(block)) {
447         x = int(document()->documentMargin());
448     }
449     const QString elidedText = fontMetrics().elidedText(text,
450                                                         Qt::ElideRight,
451                                                         viewport()->width() - x);
452     QTextLayout *layout = block.layout();
453     QTextLine textLine = layout->lineAt(0);
454     QRectF lineRect = textLine.naturalTextRect().translated(offset.x(), top);
455     QRect clipRect = contentsRect();
456     clipRect.setLeft(x);
457     painter.setClipRect(clipRect);
458     painter.drawText(QPointF(x, lineRect.top() + textLine.ascent()),
459                      elidedText);
460     painter.restore();
461 }
462 
mouseDoubleClickEvent(QMouseEvent * e)463 void SideDiffEditorWidget::mouseDoubleClickEvent(QMouseEvent *e)
464 {
465     if (e->button() == Qt::LeftButton && !(e->modifiers() & Qt::ShiftModifier)) {
466         QTextCursor cursor = cursorForPosition(e->pos());
467         jumpToOriginalFile(cursor);
468         e->accept();
469         return;
470     }
471     SelectableTextEditorWidget::mouseDoubleClickEvent(e);
472 }
473 
keyPressEvent(QKeyEvent * e)474 void SideDiffEditorWidget::keyPressEvent(QKeyEvent *e)
475 {
476     if (e->key() == Qt::Key_Enter || e->key() == Qt::Key_Return) {
477         jumpToOriginalFile(textCursor());
478         e->accept();
479         return;
480     }
481     SelectableTextEditorWidget::keyPressEvent(e);
482 }
483 
contextMenuEvent(QContextMenuEvent * e)484 void SideDiffEditorWidget::contextMenuEvent(QContextMenuEvent *e)
485 {
486     QPointer<QMenu> menu = createStandardContextMenu();
487 
488     const QTextCursor tc = textCursor();
489     QTextCursor start = tc;
490     start.setPosition(tc.selectionStart());
491     QTextCursor end = tc;
492     end.setPosition(tc.selectionEnd());
493     const int startBlockNumber = start.blockNumber();
494     const int endBlockNumber = end.blockNumber();
495 
496     QTextCursor cursor = cursorForPosition(e->pos());
497     const int blockNumber = cursor.blockNumber();
498 
499     const int fileIndex = fileIndexForBlockNumber(blockNumber);
500     const int chunkIndex = chunkIndexForBlockNumber(blockNumber);
501 
502     const int selectionStartFileIndex = fileIndexForBlockNumber(startBlockNumber);
503     const int selectionStartChunkIndex = chunkIndexForBlockNumber(startBlockNumber);
504     const int selectionEndFileIndex = fileIndexForBlockNumber(endBlockNumber);
505     const int selectionEndChunkIndex = chunkIndexForBlockNumber(endBlockNumber);
506 
507     const int selectionStart = selectionStartFileIndex == fileIndex
508             && selectionStartChunkIndex == chunkIndex
509             ? chunkRowForBlockNumber(startBlockNumber)
510             : 0;
511 
512     const int selectionEnd = selectionEndFileIndex == fileIndex
513             && selectionEndChunkIndex == chunkIndex
514             ? chunkRowForBlockNumber(endBlockNumber)
515             : chunkRowsCountForBlockNumber(blockNumber);
516 
517     QList<int> rows;
518     for (int i = selectionStart; i <= selectionEnd; ++i)
519         rows.append(i);
520 
521     const ChunkSelection selection(rows, rows);
522 
523     emit contextMenuRequested(menu, fileIndexForBlockNumber(blockNumber),
524                               chunkIndexForBlockNumber(blockNumber),
525                               selection);
526 
527     connect(this, &SideDiffEditorWidget::destroyed, menu.data(), &QMenu::deleteLater);
528     menu->exec(e->globalPos());
529     delete menu;
530 }
531 
jumpToOriginalFile(const QTextCursor & cursor)532 void SideDiffEditorWidget::jumpToOriginalFile(const QTextCursor &cursor)
533 {
534     if (m_fileInfo.isEmpty())
535         return;
536 
537     const int blockNumber = cursor.blockNumber();
538     if (!m_lineNumbers.contains(blockNumber))
539         return;
540 
541     const int lineNumber = m_lineNumbers.value(blockNumber);
542     const int columnNumber = cursor.positionInBlock();
543 
544     emit jumpToOriginalFileRequested(fileIndexForBlockNumber(blockNumber),
545                                      lineNumber, columnNumber);
546 }
547 
skippedText(int skippedNumber)548 static QString skippedText(int skippedNumber)
549 {
550     if (skippedNumber > 0)
551         return SideBySideDiffEditorWidget::tr("Skipped %n lines...", nullptr, skippedNumber);
552     if (skippedNumber == -2)
553         return SideBySideDiffEditorWidget::tr("Binary files differ");
554     return SideBySideDiffEditorWidget::tr("Skipped unknown number of lines...");
555 }
556 
paintEvent(QPaintEvent * e)557 void SideDiffEditorWidget::paintEvent(QPaintEvent *e)
558 {
559     SelectableTextEditorWidget::paintEvent(e);
560 
561     QPainter painter(viewport());
562     QPointF offset = contentOffset();
563     QTextBlock currentBlock = firstVisibleBlock();
564 
565     while (currentBlock.isValid()) {
566         if (currentBlock.isVisible()) {
567             qreal top = blockBoundingGeometry(currentBlock).translated(offset).top();
568             qreal bottom = top + blockBoundingRect(currentBlock).height();
569 
570             if (top > e->rect().bottom())
571                 break;
572 
573             if (bottom >= e->rect().top()) {
574                 const int blockNumber = currentBlock.blockNumber();
575 
576                 auto it = m_skippedLines.constFind(blockNumber);
577                 if (it != m_skippedLines.constEnd()) {
578                     QString skippedRowsText = '[' + skippedText(it->first) + ']';
579                     if (!it->second.isEmpty())
580                         skippedRowsText += ' ' + it->second;
581                     paintSeparator(painter, m_chunkLineForeground,
582                                    skippedRowsText, currentBlock, top);
583                 }
584 
585                 const DiffFileInfo fileInfo = m_fileInfo.value(blockNumber);
586                 if (!fileInfo.fileName.isEmpty()) {
587                     const QString fileNameText = fileInfo.typeInfo.isEmpty()
588                             ? fileInfo.fileName
589                             : tr("[%1] %2").arg(fileInfo.typeInfo)
590                               .arg(fileInfo.fileName);
591                     paintSeparator(painter, m_fileLineForeground,
592                                    fileNameText, currentBlock, top);
593                 }
594             }
595         }
596         currentBlock = currentBlock.next();
597     }
598 
599     if (m_drawCollapsedBlock.isValid()) {
600         // draw it now
601         customDrawCollapsedBlockPopup(painter,
602                                       m_drawCollapsedBlock,
603                                       m_drawCollapsedOffset,
604                                       m_drawCollapsedClip);
605         // reset the data for the drawing
606         m_drawCollapsedBlock = QTextBlock();
607     }
608 }
609 
customDrawCollapsedBlockPopup(QPainter & painter,const QTextBlock & block,QPointF offset,const QRect & clip)610 void SideDiffEditorWidget::customDrawCollapsedBlockPopup(QPainter &painter,
611                                                    const QTextBlock &block,
612                                                    QPointF offset,
613                                                    const QRect &clip)
614 {
615     int margin = block.document()->documentMargin();
616     qreal maxWidth = 0;
617     qreal blockHeight = 0;
618     QTextBlock b = block;
619 
620     while (!b.isVisible()) {
621         const int blockNumber = b.blockNumber();
622         if (!m_skippedLines.contains(blockNumber) && !m_separators.contains(blockNumber)) {
623             b.setVisible(true); // make sure block bounding rect works
624             QRectF r = blockBoundingRect(b).translated(offset);
625 
626             QTextLayout *layout = b.layout();
627             for (int i = layout->lineCount()-1; i >= 0; --i)
628                 maxWidth = qMax(maxWidth, layout->lineAt(i).naturalTextWidth() + 2*margin);
629 
630             blockHeight += r.height();
631 
632             b.setVisible(false); // restore previous state
633             b.setLineCount(0); // restore 0 line count for invisible block
634         }
635         b = b.next();
636     }
637 
638     painter.save();
639     painter.setRenderHint(QPainter::Antialiasing, true);
640     painter.translate(.5, .5);
641     QBrush brush = palette().base();
642     const QTextCharFormat &ifdefedOutFormat
643             = textDocument()->fontSettings().toTextCharFormat(C_DISABLED_CODE);
644     if (ifdefedOutFormat.hasProperty(QTextFormat::BackgroundBrush))
645         brush = ifdefedOutFormat.background();
646     painter.setBrush(brush);
647     painter.drawRoundedRect(QRectF(offset.x(),
648                                    offset.y(),
649                                    maxWidth, blockHeight).adjusted(0, 0, 0, 0), 3, 3);
650     painter.restore();
651 
652     QTextBlock end = b;
653     b = block;
654     while (b != end) {
655         const int blockNumber = b.blockNumber();
656         if (!m_skippedLines.contains(blockNumber) && !m_separators.contains(blockNumber)) {
657             b.setVisible(true); // make sure block bounding rect works
658             QRectF r = blockBoundingRect(b).translated(offset);
659             QTextLayout *layout = b.layout();
660             QVector<QTextLayout::FormatRange> selections;
661             layout->draw(&painter, offset, selections, clip);
662 
663             b.setVisible(false); // restore previous state
664             b.setLineCount(0); // restore 0 line count for invisible block
665             offset.ry() += r.height();
666         }
667         b = b.next();
668     }
669 }
670 
drawCollapsedBlockPopup(QPainter & painter,const QTextBlock & block,QPointF offset,const QRect & clip)671 void SideDiffEditorWidget::drawCollapsedBlockPopup(QPainter &painter,
672                                                    const QTextBlock &block,
673                                                    QPointF offset,
674                                                    const QRect &clip)
675 {
676     Q_UNUSED(painter)
677     // Called from inside SelectableTextEditorWidget::paintEvent().
678     // Postpone the drawing for now, do it after our paintEvent's
679     // custom painting. Store the data for the future redraw.
680     m_drawCollapsedBlock = block;
681     m_drawCollapsedOffset = offset;
682     m_drawCollapsedClip = clip;
683 }
684 
focusInEvent(QFocusEvent * e)685 void SideDiffEditorWidget::focusInEvent(QFocusEvent *e)
686 {
687     SelectableTextEditorWidget::focusInEvent(e);
688     emit gotFocus();
689 }
690 
691 //////////////////
692 
SideBySideDiffEditorWidget(QWidget * parent)693 SideBySideDiffEditorWidget::SideBySideDiffEditorWidget(QWidget *parent)
694     : QWidget(parent)
695     , m_controller(this)
696 {
697     m_leftEditor = new SideDiffEditorWidget(this);
698     m_leftEditor->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
699     m_leftEditor->setReadOnly(true);
700     m_leftEditor->setCodeStyle(TextEditorSettings::codeStyle());
701     connect(m_leftEditor, &SideDiffEditorWidget::jumpToOriginalFileRequested,
702             this, &SideBySideDiffEditorWidget::slotLeftJumpToOriginalFileRequested);
703     connect(m_leftEditor, &SideDiffEditorWidget::contextMenuRequested,
704             this, &SideBySideDiffEditorWidget::slotLeftContextMenuRequested,
705             Qt::DirectConnection);
706 
707     m_rightEditor = new SideDiffEditorWidget(this);
708     m_rightEditor->setReadOnly(true);
709     m_rightEditor->setCodeStyle(TextEditorSettings::codeStyle());
710     connect(m_rightEditor, &SideDiffEditorWidget::jumpToOriginalFileRequested,
711             this, &SideBySideDiffEditorWidget::slotRightJumpToOriginalFileRequested);
712     connect(m_rightEditor, &SideDiffEditorWidget::contextMenuRequested,
713             this, &SideBySideDiffEditorWidget::slotRightContextMenuRequested,
714             Qt::DirectConnection);
715 
716     auto setupHighlightController = [this]() {
717         HighlightScrollBarController *highlightController = m_leftEditor->highlightScrollBarController();
718         if (highlightController)
719             highlightController->setScrollArea(m_rightEditor);
720     };
721 
722     setupHighlightController();
723     connect(m_leftEditor, &SideDiffEditorWidget::gotDisplaySettings, this, setupHighlightController);
724 
725     m_rightEditor->verticalScrollBar()->setFocusProxy(m_leftEditor);
726     connect(m_leftEditor, &SideDiffEditorWidget::gotFocus, this, [this]() {
727         if (m_rightEditor->verticalScrollBar()->focusProxy() == m_leftEditor)
728             return; // We already did it before.
729 
730         // Hack #1. If the left editor got a focus last time
731         // we don't want to focus right editor when clicking the right
732         // scrollbar.
733         m_rightEditor->verticalScrollBar()->setFocusProxy(m_leftEditor);
734 
735         // Hack #2. If the focus is currently not on the scrollbar's proxy
736         // and we click on the scrollbar, the focus will go to the parent
737         // of the scrollbar. In order to give the focus to the proxy
738         // we need to set a click focus policy on the scrollbar.
739         // See QApplicationPrivate::giveFocusAccordingToFocusPolicy().
740         m_rightEditor->verticalScrollBar()->setFocusPolicy(Qt::ClickFocus);
741 
742         // Hack #3. Setting the focus policy is not orthogonal to setting
743         // the focus proxy and unfortuantely it changes the policy of the proxy
744         // too. We bring back the original policy to keep tab focus working.
745         m_leftEditor->setFocusPolicy(Qt::StrongFocus);
746     });
747     connect(m_rightEditor, &SideDiffEditorWidget::gotFocus, this, [this]() {
748         // Unhack #1.
749         m_rightEditor->verticalScrollBar()->setFocusProxy(nullptr);
750         // Unhack #2.
751         m_rightEditor->verticalScrollBar()->setFocusPolicy(Qt::NoFocus);
752     });
753 
754     connect(TextEditorSettings::instance(),
755             &TextEditorSettings::fontSettingsChanged,
756             this, &SideBySideDiffEditorWidget::setFontSettings);
757     setFontSettings(TextEditorSettings::fontSettings());
758 
759     connect(m_leftEditor->verticalScrollBar(), &QAbstractSlider::valueChanged,
760             this, &SideBySideDiffEditorWidget::leftVSliderChanged);
761     connect(m_leftEditor->verticalScrollBar(), &QAbstractSlider::actionTriggered,
762             this, &SideBySideDiffEditorWidget::leftVSliderChanged);
763 
764     connect(m_leftEditor->horizontalScrollBar(), &QAbstractSlider::valueChanged,
765             this, &SideBySideDiffEditorWidget::leftHSliderChanged);
766     connect(m_leftEditor->horizontalScrollBar(), &QAbstractSlider::actionTriggered,
767             this, &SideBySideDiffEditorWidget::leftHSliderChanged);
768 
769     connect(m_leftEditor, &QPlainTextEdit::cursorPositionChanged,
770             this, &SideBySideDiffEditorWidget::leftCursorPositionChanged);
771 
772     connect(m_rightEditor->verticalScrollBar(), &QAbstractSlider::valueChanged,
773             this, &SideBySideDiffEditorWidget::rightVSliderChanged);
774     connect(m_rightEditor->verticalScrollBar(), &QAbstractSlider::actionTriggered,
775             this, &SideBySideDiffEditorWidget::rightVSliderChanged);
776 
777     connect(m_rightEditor->horizontalScrollBar(), &QAbstractSlider::valueChanged,
778             this, &SideBySideDiffEditorWidget::rightHSliderChanged);
779     connect(m_rightEditor->horizontalScrollBar(), &QAbstractSlider::actionTriggered,
780             this, &SideBySideDiffEditorWidget::rightHSliderChanged);
781 
782     connect(m_rightEditor, &QPlainTextEdit::cursorPositionChanged,
783             this, &SideBySideDiffEditorWidget::rightCursorPositionChanged);
784 
785     connect(m_leftEditor, &SideDiffEditorWidget::foldChanged,
786             m_rightEditor, &SideDiffEditorWidget::setFolded);
787     connect(m_rightEditor, &SideDiffEditorWidget::foldChanged,
788             m_leftEditor, &SideDiffEditorWidget::setFolded);
789 
790     connect(m_leftEditor->horizontalScrollBar(), &QAbstractSlider::rangeChanged,
791             this, &SideBySideDiffEditorWidget::syncHorizontalScrollBarPolicy);
792 
793     connect(m_rightEditor->horizontalScrollBar(), &QAbstractSlider::rangeChanged,
794             this, &SideBySideDiffEditorWidget::syncHorizontalScrollBarPolicy);
795 
796     syncHorizontalScrollBarPolicy();
797 
798     m_splitter = new MiniSplitter(this);
799     m_splitter->addWidget(m_leftEditor);
800     m_splitter->addWidget(m_rightEditor);
801     QVBoxLayout *l = new QVBoxLayout(this);
802     l->setContentsMargins(0, 0, 0, 0);
803     l->addWidget(m_splitter);
804     setFocusProxy(m_leftEditor);
805 
806     auto leftContext = new IContext(this);
807     leftContext->setWidget(m_leftEditor);
808     leftContext->setContext(Core::Context(Utils::Id(Constants::SIDE_BY_SIDE_VIEW_ID).withSuffix(1)));
809     Core::ICore::addContextObject(leftContext);
810     auto rightContext = new IContext(this);
811     rightContext->setWidget(m_rightEditor);
812     rightContext->setContext(Core::Context(Utils::Id(Constants::SIDE_BY_SIDE_VIEW_ID).withSuffix(2)));
813     Core::ICore::addContextObject(rightContext);
814 }
815 
leftEditorWidget() const816 TextEditorWidget *SideBySideDiffEditorWidget::leftEditorWidget() const
817 {
818     return m_leftEditor;
819 }
820 
rightEditorWidget() const821 TextEditorWidget *SideBySideDiffEditorWidget::rightEditorWidget() const
822 {
823     return m_rightEditor;
824 }
825 
setDocument(DiffEditorDocument * document)826 void SideBySideDiffEditorWidget::setDocument(DiffEditorDocument *document)
827 {
828     m_controller.setDocument(document);
829     clear();
830     QList<FileData> diffFileList;
831     QString workingDirectory;
832     if (document) {
833         diffFileList = document->diffFiles();
834         workingDirectory = document->baseDirectory();
835     }
836     setDiff(diffFileList, workingDirectory);
837 }
838 
diffDocument() const839 DiffEditorDocument *SideBySideDiffEditorWidget::diffDocument() const
840 {
841     return m_controller.document();
842 }
843 
clear(const QString & message)844 void SideBySideDiffEditorWidget::clear(const QString &message)
845 {
846     const bool oldIgnore = m_controller.m_ignoreCurrentIndexChange;
847     m_controller.m_ignoreCurrentIndexChange = true;
848     setDiff(QList<FileData>(), QString());
849     m_leftEditor->clearAll(message);
850     m_rightEditor->clearAll(message);
851     m_controller.m_ignoreCurrentIndexChange = oldIgnore;
852 }
853 
setDiff(const QList<FileData> & diffFileList,const QString & workingDirectory)854 void SideBySideDiffEditorWidget::setDiff(const QList<FileData> &diffFileList,
855                                          const QString &workingDirectory)
856 {
857     Q_UNUSED(workingDirectory)
858 
859     const bool oldIgnore = m_controller.m_ignoreCurrentIndexChange;
860     m_controller.m_ignoreCurrentIndexChange = true;
861     m_leftEditor->clear();
862     m_rightEditor->clear();
863 
864     m_controller.m_contextFileData = diffFileList;
865     if (m_controller.m_contextFileData.isEmpty()) {
866         const QString msg = tr("No difference.");
867         m_leftEditor->setPlainText(msg);
868         m_rightEditor->setPlainText(msg);
869     } else {
870         showDiff();
871     }
872     m_controller.m_ignoreCurrentIndexChange = oldIgnore;
873 }
874 
setCurrentDiffFileIndex(int diffFileIndex)875 void SideBySideDiffEditorWidget::setCurrentDiffFileIndex(int diffFileIndex)
876 {
877     if (m_controller.m_ignoreCurrentIndexChange)
878         return;
879 
880     const int blockNumber = m_leftEditor->blockNumberForFileIndex(diffFileIndex);
881 
882     const bool oldIgnore = m_controller.m_ignoreCurrentIndexChange;
883     m_controller.m_ignoreCurrentIndexChange = true;
884     QTextBlock leftBlock = m_leftEditor->document()->findBlockByNumber(blockNumber);
885     QTextCursor leftCursor = m_leftEditor->textCursor();
886     leftCursor.setPosition(leftBlock.position());
887     m_leftEditor->setTextCursor(leftCursor);
888     m_leftEditor->verticalScrollBar()->setValue(blockNumber);
889 
890     QTextBlock rightBlock = m_rightEditor->document()->findBlockByNumber(blockNumber);
891     QTextCursor rightCursor = m_rightEditor->textCursor();
892     rightCursor.setPosition(rightBlock.position());
893     m_rightEditor->setTextCursor(rightCursor);
894     m_rightEditor->verticalScrollBar()->setValue(blockNumber);
895 
896     m_controller.m_ignoreCurrentIndexChange = oldIgnore;
897 }
898 
setHorizontalSync(bool sync)899 void SideBySideDiffEditorWidget::setHorizontalSync(bool sync)
900 {
901     m_horizontalSync = sync;
902     rightHSliderChanged();
903 }
904 
saveState()905 void SideBySideDiffEditorWidget::saveState()
906 {
907     m_leftEditor->saveState();
908     m_rightEditor->saveState();
909 }
910 
restoreState()911 void SideBySideDiffEditorWidget::restoreState()
912 {
913     m_leftEditor->restoreState();
914     m_rightEditor->restoreState();
915 }
916 
showDiff()917 void SideBySideDiffEditorWidget::showDiff()
918 {
919     QMap<int, QList<DiffSelection> > leftFormats;
920     QMap<int, QList<DiffSelection> > rightFormats;
921 
922     QString leftTexts, rightTexts;
923     int blockNumber = 0;
924     QChar separator = '\n';
925     QHash<int, int> foldingIndent;
926     for (const FileData &contextFileData : qAsConst(m_controller.m_contextFileData)) {
927         QString leftText, rightText;
928 
929         foldingIndent.insert(blockNumber, 1);
930         leftFormats[blockNumber].append(DiffSelection(&m_controller.m_fileLineFormat));
931         rightFormats[blockNumber].append(DiffSelection(&m_controller.m_fileLineFormat));
932         m_leftEditor->setFileInfo(blockNumber, contextFileData.leftFileInfo);
933         m_rightEditor->setFileInfo(blockNumber, contextFileData.rightFileInfo);
934         leftText = separator;
935         rightText = separator;
936         blockNumber++;
937 
938         int lastLeftLineNumber = -1;
939 
940         if (contextFileData.binaryFiles) {
941             foldingIndent.insert(blockNumber, 2);
942             leftFormats[blockNumber].append(DiffSelection(&m_controller.m_chunkLineFormat));
943             rightFormats[blockNumber].append(DiffSelection(&m_controller.m_chunkLineFormat));
944             m_leftEditor->setSkippedLines(blockNumber, -2);
945             m_rightEditor->setSkippedLines(blockNumber, -2);
946             leftText += separator;
947             rightText += separator;
948             blockNumber++;
949         } else {
950             for (int j = 0; j < contextFileData.chunks.count(); j++) {
951                 const ChunkData &chunkData = contextFileData.chunks.at(j);
952 
953                 int leftLineNumber = chunkData.leftStartingLineNumber;
954                 int rightLineNumber = chunkData.rightStartingLineNumber;
955 
956                 if (!chunkData.contextChunk) {
957                     const int skippedLines = leftLineNumber - lastLeftLineNumber - 1;
958                     if (skippedLines > 0) {
959                         foldingIndent.insert(blockNumber, 2);
960                         leftFormats[blockNumber].append(DiffSelection(&m_controller.m_chunkLineFormat));
961                         rightFormats[blockNumber].append(DiffSelection(&m_controller.m_chunkLineFormat));
962                         m_leftEditor->setSkippedLines(blockNumber, skippedLines, chunkData.contextInfo);
963                         m_rightEditor->setSkippedLines(blockNumber, skippedLines, chunkData.contextInfo);
964                         leftText += separator;
965                         rightText += separator;
966                         blockNumber++;
967                     }
968 
969                     m_leftEditor->setChunkIndex(blockNumber, chunkData.rows.count(), j);
970                     m_rightEditor->setChunkIndex(blockNumber, chunkData.rows.count(), j);
971 
972                     for (const RowData &rowData : chunkData.rows) {
973                         TextLineData leftLineData = rowData.leftLine;
974                         TextLineData rightLineData = rowData.rightLine;
975                         if (leftLineData.textLineType == TextLineData::TextLine) {
976                             leftText += leftLineData.text;
977                             lastLeftLineNumber = leftLineNumber;
978                             leftLineNumber++;
979                             m_leftEditor->setLineNumber(blockNumber, leftLineNumber);
980                         } else if (leftLineData.textLineType == TextLineData::Separator) {
981                             m_leftEditor->setSeparator(blockNumber, true);
982                         }
983 
984                         if (rightLineData.textLineType == TextLineData::TextLine) {
985                             rightText += rightLineData.text;
986                             rightLineNumber++;
987                             m_rightEditor->setLineNumber(blockNumber, rightLineNumber);
988                         } else if (rightLineData.textLineType == TextLineData::Separator) {
989                             m_rightEditor->setSeparator(blockNumber, true);
990                         }
991 
992                         if (!rowData.equal) {
993                             if (rowData.leftLine.textLineType == TextLineData::TextLine)
994                                 leftFormats[blockNumber].append(DiffSelection(&m_controller.m_leftLineFormat));
995                             else
996                                 leftFormats[blockNumber].append(DiffSelection(&m_spanLineFormat));
997                             if (rowData.rightLine.textLineType == TextLineData::TextLine)
998                                 rightFormats[blockNumber].append(DiffSelection(&m_controller.m_rightLineFormat));
999                             else
1000                                 rightFormats[blockNumber].append(DiffSelection(&m_spanLineFormat));
1001                         }
1002 
1003                         for (auto it = leftLineData.changedPositions.cbegin(),
1004                                   end = leftLineData.changedPositions.cend(); it != end; ++it) {
1005                             leftFormats[blockNumber].append(
1006                                         DiffSelection(it.key(), it.value(),
1007                                                       &m_controller.m_leftCharFormat));
1008                         }
1009 
1010                         for (auto it = rightLineData.changedPositions.cbegin(),
1011                                   end = rightLineData.changedPositions.cend(); it != end; ++it) {
1012                             rightFormats[blockNumber].append(
1013                                         DiffSelection(it.key(), it.value(),
1014                                                       &m_controller.m_rightCharFormat));
1015                         }
1016 
1017                         leftText += separator;
1018                         rightText += separator;
1019                         blockNumber++;
1020                     }
1021                 }
1022 
1023                 if (j == contextFileData.chunks.count() - 1) { // the last chunk
1024                     int skippedLines = -2;
1025                     if (chunkData.contextChunk) {
1026                         // if it's context chunk
1027                         skippedLines = chunkData.rows.count();
1028                     } else if (!contextFileData.lastChunkAtTheEndOfFile
1029                                && !contextFileData.contextChunksIncluded) {
1030                         // if not a context chunk and not a chunk at the end of file
1031                         // and context lines not included
1032                         skippedLines = -1; // unknown count skipped by the end of file
1033                     }
1034 
1035                     if (skippedLines >= -1) {
1036                         leftFormats[blockNumber].append(DiffSelection(&m_controller.m_chunkLineFormat));
1037                         rightFormats[blockNumber].append(DiffSelection(&m_controller.m_chunkLineFormat));
1038                         m_leftEditor->setSkippedLines(blockNumber, skippedLines);
1039                         m_rightEditor->setSkippedLines(blockNumber, skippedLines);
1040                         leftText += separator;
1041                         rightText += separator;
1042                         blockNumber++;
1043                     } // otherwise nothing skipped
1044                 }
1045             }
1046         }
1047         leftText.replace('\r', ' ');
1048         rightText.replace('\r', ' ');
1049         leftTexts += leftText;
1050         rightTexts += rightText;
1051     }
1052 
1053     if (leftTexts.isEmpty() && rightTexts.isEmpty())
1054         return;
1055 
1056     const bool oldIgnore = m_controller.m_ignoreCurrentIndexChange;
1057     m_controller.m_ignoreCurrentIndexChange = true;
1058     m_leftEditor->clear();
1059     m_leftEditor->setPlainText(leftTexts);
1060     m_rightEditor->clear();
1061     m_rightEditor->setPlainText(rightTexts);
1062     m_controller.m_ignoreCurrentIndexChange = oldIgnore;
1063 
1064     QTextBlock block = m_leftEditor->document()->firstBlock();
1065     for (int b = 0; block.isValid(); block = block.next(), ++b)
1066         SelectableTextEditorWidget::setFoldingIndent(block, foldingIndent.value(b, 3));
1067     block = m_rightEditor->document()->firstBlock();
1068     for (int b = 0; block.isValid(); block = block.next(), ++b)
1069         SelectableTextEditorWidget::setFoldingIndent(block, foldingIndent.value(b, 3));
1070 
1071     m_leftEditor->setSelections(leftFormats);
1072     m_rightEditor->setSelections(rightFormats);
1073 }
1074 
setFontSettings(const FontSettings & fontSettings)1075 void SideBySideDiffEditorWidget::setFontSettings(
1076         const FontSettings &fontSettings)
1077 {
1078     m_spanLineFormat  = fontSettings.toTextCharFormat(C_LINE_NUMBER);
1079     m_controller.setFontSettings(fontSettings);
1080 }
1081 
slotLeftJumpToOriginalFileRequested(int diffFileIndex,int lineNumber,int columnNumber)1082 void SideBySideDiffEditorWidget::slotLeftJumpToOriginalFileRequested(
1083         int diffFileIndex,
1084         int lineNumber,
1085         int columnNumber)
1086 {
1087     if (diffFileIndex < 0 || diffFileIndex >= m_controller.m_contextFileData.count())
1088         return;
1089 
1090     const FileData fileData = m_controller.m_contextFileData.at(diffFileIndex);
1091     const QString leftFileName = fileData.leftFileInfo.fileName;
1092     const QString rightFileName = fileData.rightFileInfo.fileName;
1093     if (leftFileName == rightFileName) {
1094         // The same file (e.g. in git diff), jump to the line number taken from the right editor.
1095         // Warning: git show SHA^ vs SHA or git diff HEAD vs Index
1096         // (when Working tree has changed in meantime) will not work properly.
1097         for (const ChunkData &chunkData : fileData.chunks) {
1098 
1099             int leftLineNumber = chunkData.leftStartingLineNumber;
1100             int rightLineNumber = chunkData.rightStartingLineNumber;
1101 
1102             for (int j = 0; j < chunkData.rows.count(); j++) {
1103                 const RowData rowData = chunkData.rows.at(j);
1104                 if (rowData.leftLine.textLineType == TextLineData::TextLine)
1105                     leftLineNumber++;
1106                 if (rowData.rightLine.textLineType == TextLineData::TextLine)
1107                     rightLineNumber++;
1108                 if (leftLineNumber == lineNumber) {
1109                     int colNr = rowData.equal ? columnNumber : 0;
1110                     m_controller.jumpToOriginalFile(leftFileName, rightLineNumber, colNr);
1111                     return;
1112                 }
1113             }
1114         }
1115     } else {
1116         // different file (e.g. in Tools | Diff...)
1117         m_controller.jumpToOriginalFile(leftFileName, lineNumber, columnNumber);
1118     }
1119 }
1120 
slotRightJumpToOriginalFileRequested(int diffFileIndex,int lineNumber,int columnNumber)1121 void SideBySideDiffEditorWidget::slotRightJumpToOriginalFileRequested(
1122         int diffFileIndex,
1123         int lineNumber,
1124         int columnNumber)
1125 {
1126     if (diffFileIndex < 0 || diffFileIndex >= m_controller.m_contextFileData.count())
1127         return;
1128 
1129     const FileData fileData = m_controller.m_contextFileData.at(diffFileIndex);
1130     const QString fileName = fileData.rightFileInfo.fileName;
1131     m_controller.jumpToOriginalFile(fileName, lineNumber, columnNumber);
1132 }
1133 
slotLeftContextMenuRequested(QMenu * menu,int fileIndex,int chunkIndex,const ChunkSelection & selection)1134 void SideBySideDiffEditorWidget::slotLeftContextMenuRequested(QMenu *menu,
1135                                                               int fileIndex,
1136                                                               int chunkIndex,
1137                                                               const ChunkSelection &selection)
1138 {
1139     menu->addSeparator();
1140 
1141     m_controller.addCodePasterAction(menu, fileIndex, chunkIndex);
1142     m_controller.addApplyAction(menu, fileIndex, chunkIndex);
1143     m_controller.addExtraActions(menu, fileIndex, chunkIndex, selection);
1144 }
1145 
slotRightContextMenuRequested(QMenu * menu,int fileIndex,int chunkIndex,const ChunkSelection & selection)1146 void SideBySideDiffEditorWidget::slotRightContextMenuRequested(QMenu *menu,
1147                                                                int fileIndex,
1148                                                                int chunkIndex,
1149                                                                const ChunkSelection &selection)
1150 {
1151     menu->addSeparator();
1152 
1153     m_controller.addCodePasterAction(menu, fileIndex, chunkIndex);
1154     m_controller.addRevertAction(menu, fileIndex, chunkIndex);
1155     m_controller.addExtraActions(menu, fileIndex, chunkIndex, selection);
1156 }
1157 
leftVSliderChanged()1158 void SideBySideDiffEditorWidget::leftVSliderChanged()
1159 {
1160     if (m_controller.m_ignoreCurrentIndexChange)
1161         return;
1162 
1163     m_rightEditor->verticalScrollBar()->setValue(m_leftEditor->verticalScrollBar()->value());
1164 }
1165 
rightVSliderChanged()1166 void SideBySideDiffEditorWidget::rightVSliderChanged()
1167 {
1168     if (m_controller.m_ignoreCurrentIndexChange)
1169         return;
1170 
1171     m_leftEditor->verticalScrollBar()->setValue(m_rightEditor->verticalScrollBar()->value());
1172 }
1173 
leftHSliderChanged()1174 void SideBySideDiffEditorWidget::leftHSliderChanged()
1175 {
1176     if (m_controller.m_ignoreCurrentIndexChange)
1177         return;
1178 
1179     if (m_horizontalSync)
1180         m_rightEditor->horizontalScrollBar()->setValue(m_leftEditor->horizontalScrollBar()->value());
1181 }
1182 
rightHSliderChanged()1183 void SideBySideDiffEditorWidget::rightHSliderChanged()
1184 {
1185     if (m_controller.m_ignoreCurrentIndexChange)
1186         return;
1187 
1188     if (m_horizontalSync)
1189         m_leftEditor->horizontalScrollBar()->setValue(m_rightEditor->horizontalScrollBar()->value());
1190 }
1191 
leftCursorPositionChanged()1192 void SideBySideDiffEditorWidget::leftCursorPositionChanged()
1193 {
1194     if (m_controller.m_ignoreCurrentIndexChange)
1195         return;
1196 
1197     handlePositionChange(m_leftEditor, m_rightEditor);
1198     leftVSliderChanged();
1199     leftHSliderChanged();
1200 }
1201 
rightCursorPositionChanged()1202 void SideBySideDiffEditorWidget::rightCursorPositionChanged()
1203 {
1204     if (m_controller.m_ignoreCurrentIndexChange)
1205         return;
1206 
1207     handlePositionChange(m_rightEditor, m_leftEditor);
1208     rightVSliderChanged();
1209     rightHSliderChanged();
1210 }
1211 
syncHorizontalScrollBarPolicy()1212 void SideBySideDiffEditorWidget::syncHorizontalScrollBarPolicy()
1213 {
1214     const bool alwaysOn = m_leftEditor->horizontalScrollBar()->maximum()
1215             || m_rightEditor->horizontalScrollBar()->maximum();
1216     const Qt::ScrollBarPolicy newPolicy = alwaysOn
1217             ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAsNeeded;
1218     if (m_leftEditor->horizontalScrollBarPolicy() != newPolicy)
1219         m_leftEditor->setHorizontalScrollBarPolicy(newPolicy);
1220     if (m_rightEditor->horizontalScrollBarPolicy() != newPolicy)
1221         m_rightEditor->setHorizontalScrollBarPolicy(newPolicy);
1222 }
1223 
handlePositionChange(SideDiffEditorWidget * source,SideDiffEditorWidget * dest)1224 void SideBySideDiffEditorWidget::handlePositionChange(SideDiffEditorWidget *source, SideDiffEditorWidget *dest)
1225 {
1226     if (m_controller.m_ignoreCurrentIndexChange)
1227         return;
1228 
1229     const bool oldIgnore = m_controller.m_ignoreCurrentIndexChange;
1230     m_controller.m_ignoreCurrentIndexChange = true;
1231     syncCursor(source, dest);
1232     emit currentDiffFileIndexChanged(
1233                 source->fileIndexForBlockNumber(source->textCursor().blockNumber()));
1234     m_controller.m_ignoreCurrentIndexChange = oldIgnore;
1235 }
1236 
syncCursor(SideDiffEditorWidget * source,SideDiffEditorWidget * dest)1237 void SideBySideDiffEditorWidget::syncCursor(SideDiffEditorWidget *source, SideDiffEditorWidget *dest)
1238 {
1239     const int oldHSliderPos = dest->horizontalScrollBar()->value();
1240 
1241     const QTextCursor sourceCursor = source->textCursor();
1242     const int sourceLine = sourceCursor.blockNumber();
1243     const int sourceColumn = sourceCursor.positionInBlock();
1244     QTextCursor destCursor = dest->textCursor();
1245     const QTextBlock destBlock = dest->document()->findBlockByNumber(sourceLine);
1246     const int destColumn = qMin(sourceColumn, destBlock.length());
1247     const int destPosition = destBlock.position() + destColumn;
1248     destCursor.setPosition(destPosition);
1249     dest->setTextCursor(destCursor);
1250 
1251     dest->horizontalScrollBar()->setValue(oldHSliderPos);
1252 }
1253 
1254 } // namespace Internal
1255 } // namespace DiffEditor
1256 
1257 #include "sidebysidediffeditorwidget.moc"
1258