2 Description: qgit revision list view
4 Author: Marco Costalba (C) 2005-2007
6 Copyright: See COPYING file that comes with this distribution
9 #include <QApplication>
10 #include <QHeaderView>
12 #include <QMouseEvent>
18 #include "FileHistory.h"
24 void getTagMarkParams(QString
&name
, QStyleOptionViewItem
& o
,
25 const int type
, const bool isCurrent
);
26 uint
refTypeFromName(SCRef name
);
30 ListView::ListView(QWidget
* parent
) : QTreeView(parent
), d(NULL
), git(NULL
), fh(NULL
), lp(NULL
), dropInfo(NULL
) {}
32 void ListView::setup(Domain
* dm
, Git
* g
) {
38 filterNextContextMenuRequest
= false;
40 setFont(QGit::STD_FONT
);
42 // create ListViewProxy unplugged, will be plug
43 // to the model only when filtering is needed
44 lp
= new ListViewProxy(this, d
, git
);
47 ListViewDelegate
* lvd
= new ListViewDelegate(git
, lp
, this);
48 lvd
->setLaneHeight(fontMetrics().height() + 2);
51 setupGeometry(); // after setting delegate
53 // shortcuts are activated only if widget is visible, this is good
54 new QShortcut(Qt::Key_Up
, this, SLOT(on_keyUp()));
55 new QShortcut(Qt::Key_Down
, this, SLOT(on_keyDown()));
57 connect(lvd
, SIGNAL(updateView()), viewport(), SLOT(update()));
59 connect(this, SIGNAL(diffTargetChanged(int)), lvd
, SLOT(diffTargetChanged(int)));
61 connect(this, SIGNAL(customContextMenuRequested(const QPoint
&)),
62 this, SLOT(on_customContextMenuRequested(const QPoint
&)));
65 ListView::~ListView() {
67 git
->cancelDataLoading(fh
); // non blocking
70 const QString
ListView::sha(int row
) const {
72 if (!lp
->sourceModel()) // unplugged
75 QModelIndex idx
= lp
->mapToSource(lp
->index(row
, 0));
76 return fh
->sha(idx
.row());
79 int ListView::row(SCRef sha
) const {
81 if (!lp
->sourceModel()) // unplugged
84 int row
= fh
->row(sha
);
85 QModelIndex idx
= fh
->index(row
, 0);
86 return lp
->mapFromSource(idx
).row();
89 void ListView::setupGeometry() {
91 QPalette pl
= palette();
92 pl
.setColor(QPalette::Base
, ODD_LINE_COL
);
93 pl
.setColor(QPalette::AlternateBase
, EVEN_LINE_COL
);
94 setPalette(pl
); // does not seem to inherit application paletteAnnotate
96 QHeaderView
* hv
= header();
97 hv
->setStretchLastSection(true);
98 #if QT_VERSION >= 0x050000
99 hv
->setSectionResizeMode(LOG_COL
, QHeaderView::Interactive
);
100 hv
->setSectionResizeMode(TIME_COL
, QHeaderView::Interactive
);
101 hv
->setSectionResizeMode(ANN_ID_COL
, QHeaderView::ResizeToContents
);
103 hv
->setResizeMode(LOG_COL
, QHeaderView::Interactive
);
104 hv
->setResizeMode(TIME_COL
, QHeaderView::Interactive
);
105 hv
->setResizeMode(ANN_ID_COL
, QHeaderView::ResizeToContents
);
107 hv
->resizeSection(GRAPH_COL
, DEF_GRAPH_COL_WIDTH
);
108 hv
->resizeSection(LOG_COL
, DEF_LOG_COL_WIDTH
);
109 hv
->resizeSection(AUTH_COL
, DEF_AUTH_COL_WIDTH
);
110 hv
->resizeSection(TIME_COL
, DEF_TIME_COL_WIDTH
);
112 if (git
->isMainHistory(fh
))
113 hideColumn(ANN_ID_COL
);
116 void ListView::scrollToNextHighlighted(int direction
) {
118 // Depending on the value of direction, scroll to:
119 // -1 = the next highlighted item above the current one (i.e. newer in history)
120 // 1 = the next highlighted item below the current one (i.e. older in history)
121 // 0 = the first highlighted item from the top of the list
123 QModelIndex idx
= currentIndex();
126 idx
= idx
.sibling(0,0);
127 if (lp
->isHighlighted(idx
.row())) {
128 setCurrentIndex(idx
);
134 idx
= (direction
>= 0 ? indexBelow(idx
) : indexAbove(idx
));
138 } while (!lp
->isHighlighted(idx
.row()));
140 setCurrentIndex(idx
);
143 void ListView::scrollToCurrent(ScrollHint hint
) {
145 if (currentIndex().isValid())
146 scrollTo(currentIndex(), hint
);
149 void ListView::on_keyUp() {
151 QModelIndex idx
= indexAbove(currentIndex());
153 setCurrentIndex(idx
);
156 void ListView::on_keyDown() {
158 QModelIndex idx
= indexBelow(currentIndex());
160 setCurrentIndex(idx
);
163 void ListView::on_changeFont(const QFont
& f
) {
166 ListViewDelegate
* lvd
= static_cast<ListViewDelegate
*>(itemDelegate());
167 lvd
->setLaneHeight(fontMetrics().height());
171 const QString
ListView::currentText(int column
) {
173 QModelIndex idx
= model()->index(currentIndex().row(), column
);
174 return (idx
.isValid() ? idx
.data().toString() : "");
177 int ListView::getLaneType(SCRef sha
, int pos
) const {
179 const Rev
* r
= git
->revLookup(sha
, fh
);
180 return (r
&& pos
< r
->lanes
.count() && pos
>= 0 ? r
->lanes
.at(pos
) : -1);
183 void ListView::showIdValues() {
186 viewport()->update();
189 void ListView::getSelectedItems(QStringList
& selectedItems
) {
191 selectedItems
.clear();
192 QModelIndexList ml
= selectionModel()->selectedRows();
193 FOREACH (QModelIndexList
, it
, ml
)
194 selectedItems
.append(sha((*it
).row()));
196 // selectedRows() returns the items in an unspecified order,
197 // so be sure rows are ordered from newest to oldest.
198 selectedItems
= git
->sortShaListByIndex(selectedItems
);
201 const QString
ListView::shaFromAnnId(uint id
) {
203 if (git
->isMainHistory(fh
))
206 return sha(model()->rowCount() - id
);
209 int ListView::filterRows(bool isOn
, bool highlight
, SCRef filter
, int colNum
, ShaSet
* set
) {
211 setUpdatesEnabled(false);
212 int matchedNum
= lp
->setFilter(isOn
, highlight
, filter
, colNum
, set
);
213 viewport()->update();
214 setUpdatesEnabled(true);
219 bool ListView::update() {
221 int stRow
= row(st
->sha());
223 return false; // main/tree view asked us a sha not in history
225 QModelIndex index
= currentIndex();
226 QItemSelectionModel
* sel
= selectionModel();
228 if (index
.isValid() && (index
.row() == stRow
)) {
230 if (sel
->isSelected(index
) != st
->selectItem())
231 sel
->select(index
, QItemSelectionModel::Toggle
);
235 // setCurrentIndex() does not clear previous
236 // selections in a multi selection QListView
239 QModelIndex newIndex
= model()->index(stRow
, 0);
240 if (newIndex
.isValid()) {
242 // emits QItemSelectionModel::currentChanged()
243 setCurrentIndex(newIndex
);
245 if (!st
->selectItem())
246 sel
->select(newIndex
, QItemSelectionModel::Deselect
);
249 if (git
->isMainHistory(fh
))
250 emit
diffTargetChanged(row(st
->diffToSha()));
252 return currentIndex().isValid();
255 void ListView::currentChanged(const QModelIndex
& index
, const QModelIndex
&) {
257 SCRef selRev
= sha(index
.row());
258 if (st
->sha() != selRev
) { // to avoid looping
260 st
->setSelectItem(true);
265 void ListView::markDiffToSha(SCRef sha
) {
266 if (sha
!= st
->diffToSha()) {
267 st
->setDiffToSha(sha
);
268 emit
showStatusMessage("Marked " + sha
+ " for diff. (Ctrl-RightClick)");
270 st
->setDiffToSha(""); // restore std view
271 emit
showStatusMessage("Unmarked diff reference.");
276 bool ListView::filterRightButtonPressed(QMouseEvent
* e
) {
278 QModelIndex index
= indexAt(e
->pos());
279 SCRef selSha
= sha(index
.row());
280 if (selSha
.isEmpty())
283 if (e
->modifiers() == Qt::ControlModifier
) { // check for 'diff to' function
284 if (selSha
!= ZERO_SHA
) {
286 filterNextContextMenuRequest
= true;
287 markDiffToSha(selSha
);
288 return true; // filter event out
291 // check for 'children & parents' function, i.e. if mouse is on the graph
292 if (index
.column() == GRAPH_COL
) {
294 filterNextContextMenuRequest
= true;
295 QStringList parents
, children
;
296 if (getLaneParentsChildren(selSha
, e
->pos().x(), parents
, children
))
297 emit
lanesContextMenuRequested(parents
, children
);
299 return true; // filter event out
304 void ListView::mousePressEvent(QMouseEvent
* e
) {
305 lastRefName
= refNameAt(e
->pos());
307 if (e
->button() == Qt::RightButton
&& filterRightButtonPressed(e
))
308 return; // filtered out
310 QTreeView::mousePressEvent(e
);
313 void ListView::mouseReleaseEvent(QMouseEvent
* e
) {
315 lastRefName
= ""; // reset
316 QTreeView::mouseReleaseEvent(e
);
320 QPixmap
ListView::pixmapFromSelection(const QStringList
&revs
, const QString
&ref
) const {
321 const int maxRows
= 10;
322 const int dotdotRow
= 5;
323 QStyleOptionViewItem opt
; opt
.initFrom(this);
324 // ListViewDelegate *lvd = dynamic_cast<ListViewDelegate*>(itemDelegate());
326 QFontMetrics
fm(opt
.font
);
327 int height
= fm
.height()+2;
328 int row
= 0, rows
= std::min(revs
.count() + (ref
.isEmpty() ? 0 : 1), maxRows
);
330 QPixmap
pixmap (columnWidth(LOG_COL
), rows
*height
);
331 pixmap
.fill(Qt::transparent
);
333 QPainter
painter(&pixmap
);
334 // render selected ref name
335 if (!ref
.isEmpty()) {
336 QStyleOptionViewItem
o(opt
);
338 getTagMarkParams(dummy
, o
, refTypeFromName(ref
), false);
339 painter
.fillRect(0, 0, fm
.width(ref
)+2*spacing
, height
, o
.palette
.window());
340 painter
.drawText(spacing
, fm
.ascent()+1, ref
);
344 painter
.fillRect(0, row
*height
, pixmap
.width(), (rows
-row
)*height
, opt
.palette
.window());
345 for (QStringList::const_iterator it
= revs
.begin(), end
= revs
.end(); it
!= end
; ++it
) {
346 const Rev
* r
= git
->revLookup(it
->section(" ", 0, 0));
347 if (!r
) continue; // should not happen
348 painter
.drawText(spacing
, row
*height
+ fm
.ascent()+1, r
->shortLog()); ++row
;
349 // jump to last dotdotRows-1 items if necessary
350 if (rows
-row
== dotdotRow
&& end
-it
> dotdotRow
+1) {
351 ++row
; // leave one line empty
352 it
+= end
-it
- dotdotRow
;
359 void ListView::startDragging(QMouseEvent
* /*e*/) {
362 getSelectedItems(selRevs
);
363 selRevs
.removeAll(ZERO_SHA
);
365 QDrag
* drag
= new QDrag(this);
366 QMimeData
* mimeData
= new QMimeData
;
367 if (!selRevs
.empty()) {
368 Qt::DropActions actions
= Qt::CopyAction
; // patch
369 Qt::DropAction default_action
= Qt::CopyAction
;
372 bool contiguous
= git
->isContiguous(selRevs
);
374 // standard text for range description
377 if (selRevs
.size() > 1) text
+= selRevs
.back() + "..";
378 text
+= lastRefName
.isEmpty() ? selRevs
.front() : lastRefName
;
379 mimeData
->setText(text
);
382 // revision range for patch/merge/rebase operations
383 QString mime
= QString("%1@%2\n").arg(contiguous
? "RANGE" : "LIST").arg(d
->m()->currentDir());
384 if (contiguous
&& !lastRefName
.isEmpty())
385 selRevs
.front() += " " + lastRefName
; // append ref name
386 mime
.append(selRevs
.join("\n"));
387 mimeData
->setData("application/x-qgit-revs", mime
.toUtf8());
389 drag
->setMimeData(mimeData
);
390 drag
->setPixmap(pixmapFromSelection(selRevs
, lastRefName
));
392 if (contiguous
) { // rebase enabled
393 actions
|= Qt::MoveAction
; // rebase (local branch) or push (remote branch)
395 // merging (of several shas) is also enabled
396 actions
|= Qt::LinkAction
;
398 drag
->exec(actions
, default_action
);
402 void ListView::mouseMoveEvent(QMouseEvent
* e
) {
404 if (e
->buttons() == Qt::LeftButton
) {
409 QTreeView::mouseMoveEvent(e
);
412 struct ListView::DropInfo
{
413 DropInfo (uint f
) : flags(f
) {}
422 PatchAction
= Qt::CopyAction
,
423 RebaseAction
= Qt::MoveAction
,
424 MoveRefAction
= (Qt::LinkAction
<< 1) | Qt::MoveAction
,
425 MergeAction
= Qt::LinkAction
,
435 static QString
actionName (ListView::DropInfo::Action a
) {
437 case PatchAction
: return "patching";
438 case RebaseAction
: return "rebasing";
439 case MoveRefAction
: return "moving";
440 case MergeAction
: return "merging";
441 default: return "This should not happen.";
446 uint
refTypeFromName(SCRef name
) {
447 if (name
.startsWith("tags/")) return Git::TAG
;
448 if (name
.startsWith("remotes/")) return Git::RMT_BRANCH
;
449 if (!name
.isEmpty()) return Git::BRANCH
;
453 void ListView::dragEnterEvent(QDragEnterEvent
* e
) {
455 if (dropInfo
) delete dropInfo
;
457 bool bCleanWorkDir
= git
->isNothingToCommit();
459 // accept local file urls for patching
461 const QList
<QUrl
>& urls
= e
->mimeData()->urls();
462 for(QList
<QUrl
>::const_iterator it
=urls
.begin(), end
=urls
.end();
463 valid
&& it
!=end
; ++it
)
464 valid
&= it
->isLocalFile();
465 if (urls
.size() > 0 && valid
&& bCleanWorkDir
) {
466 dropInfo
= new DropInfo(DropInfo::PATCHES
);
470 // parse internal mime format
471 if (!e
->mimeData()->hasFormat("application/x-qgit-revs"))
474 dropInfo
= new DropInfo(DropInfo::REV_LIST
);
476 SCRef
revsText(e
->mimeData()->data("application/x-qgit-revs"));
477 QString header
= revsText
.section("\n", 0, 0);
478 dropInfo
->shas
= revsText
.section("\n", 1).split('\n', QString::SkipEmptyParts
);
479 // extract refname and sha from first entry again
480 dropInfo
->sourceRef
= dropInfo
->shas
.front().section(" ", 1);
481 dropInfo
->shas
.front() = dropInfo
->shas
.front().section(" ", 0, 0);
482 dropInfo
->sourceRefType
= refTypeFromName(dropInfo
->sourceRef
);
485 dropInfo
->sourceRepo
= header
.section("@", 1);
486 if (dropInfo
->sourceRepo
!= d
->m()->currentDir()) {
487 if (!QDir().exists(dropInfo
->sourceRepo
)) {
488 emit
showStatusMessage("Remote repository missing: " + dropInfo
->sourceRepo
, 10000);
493 dropInfo
->flags
|= DropInfo::SAME_REPO
;
496 if (!bCleanWorkDir
&& // dirty work dir doesn't allow merging/rebasing
497 // only exception is moving ref names within same repo
498 !(dropInfo
->sourceRefType
!= 0 && dropInfo
->shas
.count() == 1 && (dropInfo
->flags
& DropInfo::SAME_REPO
))) {
499 emit
showStatusMessage("Drag-n-drop rejected: First clean your working dir!", 10000);
505 if (header
.startsWith("RANGE")) dropInfo
->flags
|= DropInfo::REV_RANGE
;
508 void ListView::dragMoveEvent(QDragMoveEvent
* e
) {
509 // When getting here, dragEnterEvent already accepted the drag in general
511 SCRef targetRef
= refNameAt(e
->pos());
512 uint targetRefType
= refTypeFromName(targetRef
);
513 QModelIndex idx
= indexAt(e
->pos());
514 SCRef targetSHA
= sha(idx
.row());
515 uint accepted_actions
= DropInfo::PatchAction
; // applying patches is always allowed
516 DropInfo::Action action
, default_action
= DropInfo::PatchAction
;
518 // extended drop actions (merging, rebasing, pushing) are only allowed
519 if (dropInfo
->flags
& DropInfo::SAME_REPO
&& // on same repo
520 dropInfo
->flags
& DropInfo::REV_LIST
&& // qgit drags
521 idx
.column() == LOG_COL
) { // and when dropping onto LOG_COL
522 // only accept drop when target has different sha than source shas
523 if (dropInfo
->shas
.contains(targetSHA
)) {
525 emit
showStatusMessage("Cannot drop onto current selection.");
528 // decide the preferred drop action based on context and
530 // rebasing is allowed onto any sha (and workdir),
531 // but only if source sha list is contiguous range
532 if (dropInfo
->flags
& DropInfo::REV_RANGE
) {
533 accepted_actions
|= DropInfo::RebaseAction
;
534 default_action
= DropInfo::RebaseAction
;
537 // merging is allowed onto any local branch
538 if (targetRefType
== Git::BRANCH
) {
539 accepted_actions
|= DropInfo::MergeAction
;
540 default_action
= DropInfo::MergeAction
;
543 // pushing is allowed when sha list has 1 item and sourceRef is remote branch
544 if (dropInfo
->shas
.count() == 1 &&
545 dropInfo
->sourceRefType
!= 0 && targetSHA
!= ZERO_SHA
) {
546 accepted_actions
|= DropInfo::MoveRefAction
;
547 default_action
= DropInfo::MoveRefAction
;
549 } else if (e
->source() == this && idx
.row() == currentIndex().row()) {
550 // move at least by one line before enabling drag
552 showStatusMessage("");
557 // check whether modifier keys enforce an action
558 switch (e
->keyboardModifiers()) {
559 case Qt::ControlModifier
: action
= DropInfo::PatchAction
; break;
560 case Qt::ShiftModifier
: action
= DropInfo::RebaseAction
; break;
561 case Qt::AltModifier
: action
= DropInfo::MergeAction
; break;
562 default: action
= default_action
; break;
565 if ((action
& accepted_actions
) == 0) {
566 statusMsg
= DropInfo::actionName(action
) + " not allowed. ";
567 statusMsg
[0] = statusMsg
[0].toUpper();
568 action
= default_action
;
571 // inform user about the to-be-performed action in statusBar
573 case DropInfo::PatchAction
:
574 statusMsg
+= "Applying patches";
576 case DropInfo::RebaseAction
:
577 statusMsg
+= QString("Rebasing %1 onto %2")
578 .arg((dropInfo
->sourceRefType
== Git::BRANCH
&&
579 dropInfo
->shas
.count() == 1) ? dropInfo
->sourceRef
: "selection")
580 .arg(targetRefType
== Git::BRANCH
? targetRef
: targetSHA
);
582 case DropInfo::MoveRefAction
:
583 statusMsg
+= "Moving " + dropInfo
->sourceRef
;
585 case DropInfo::MergeAction
:
586 statusMsg
+= "Merging selected branches into " + targetRef
;
589 emit
showStatusMessage(statusMsg
);
590 dropInfo
->action
= action
;
591 e
->setDropAction(static_cast<Qt::DropAction
>(action
& 0x7));
594 void ListView::dragLeaveEvent(QDragLeaveEvent
* /*e*/)
596 if (dropInfo
) delete dropInfo
;
598 showStatusMessage("");
601 void ListView::dropEvent(QDropEvent
*e
) {
603 if (dropInfo
->flags
& DropInfo::PATCHES
) {
605 QList
<QUrl
> urls
= e
->mimeData()->urls();
606 FOREACH(QList
<QUrl
>, it
, urls
)
607 files
<< it
->toLocalFile();
609 emit
applyPatches(files
);
613 SCRef targetRef
= refNameAt(e
->pos());
614 // uint targetRefType = refTypeFromName(targetRef);
615 SCRef targetSHA
= sha(indexAt(e
->pos()).row());
616 switch(dropInfo
->action
) {
617 case DropInfo::PatchAction
:
618 emit
applyRevisions(dropInfo
->shas
, dropInfo
->sourceRepo
);
620 case DropInfo::RebaseAction
:
621 if (dropInfo
->sourceRefType
== Git::BRANCH
&& dropInfo
->shas
.count() == 1)
622 emit
rebase("", dropInfo
->sourceRef
, targetSHA
);
624 emit
rebase(dropInfo
->shas
.last(),
625 dropInfo
->sourceRef
.isEmpty() ? dropInfo
->shas
.first()
626 : dropInfo
->sourceRef
,
629 case DropInfo::MergeAction
:
630 emit
merge(dropInfo
->shas
, targetRef
);
632 case DropInfo::MoveRefAction
:
633 emit
moveRef(dropInfo
->sourceRef
, targetSHA
);
638 void ListView::on_customContextMenuRequested(const QPoint
& pos
) {
640 QModelIndex index
= indexAt(pos
);
641 if (!index
.isValid())
644 if (filterNextContextMenuRequest
) {
645 // event filter does not work on them
646 filterNextContextMenuRequest
= false;
649 emit
contextMenu(sha(index
.row()), POPUP_LIST_EV
);
652 bool ListView::getLaneParentsChildren(SCRef sha
, int x
, SList p
, SList c
) {
654 ListViewDelegate
* lvd
= static_cast<ListViewDelegate
*>(itemDelegate());
655 uint lane
= x
/ lvd
->laneWidth();
656 int t
= getLaneType(sha
, lane
);
657 if (t
== EMPTY
|| t
== -1)
660 // first find the parents
663 if (!isFreeLane(t
)) {
664 p
= git
->revLookup(sha
, fh
)->parents(); // pointer cannot be NULL
667 SCRef
par(git
->getLaneParent(sha
, lane
));
669 dbs("ASSERT getLaneParentsChildren: parent not found");
675 // then find children
676 c
= git
->getChildren(root
);
680 /** Iterator over all refnames of a given sha.
681 * References are traversed in following order:
682 * detached (name empty), local branches, remote branches, tags, other refs
684 class RefNameIterator
{
687 uint ref_types
; // all reference types associated with sha
688 int cur_state
; // state indicating the currently processed ref type
689 QStringList ref_names
; // ref_names of current type
690 QStringList::const_iterator cur_name
;
694 RefNameIterator(const QString
&sha
, Git
* git
);
695 bool valid() const {return cur_state
!= -1;}
696 QString
name() const {return *cur_name
;}
697 int type() {return cur_state
;}
698 bool isCurrentBranch() {return *cur_name
== cur_branch
;}
703 RefNameIterator::RefNameIterator(const QString
&sha
, Git
*git
)
704 : git(git
), sha(sha
), cur_state(0), cur_branch(git
->getCurrentBranchName())
706 ref_types
= git
->checkRef(sha
);
707 if (ref_types
== 0) {
708 cur_state
= -1; // indicates end
712 // initialize dummy string list
714 cur_name
= ref_names
.begin();
717 if ((ref_types
& Git::CUR_BRANCH
) && cur_branch
.isEmpty()) {
718 // indicate detached state with type() == 0 and empty ref name
719 cur_branch
= *cur_name
;
720 } else { // advance to first real ref name
725 void RefNameIterator::next()
729 // switch to next ref type if required
730 while (valid() && cur_name
== ref_names
.end()) {
732 case 0: cur_state
= Git::BRANCH
; break;
733 case Git::BRANCH
: cur_state
= Git::RMT_BRANCH
; break;
734 case Git::RMT_BRANCH
: cur_state
= Git::TAG
; break;
735 case Git::TAG
: cur_state
= Git::REF
; break;
736 default: cur_state
= -1; // indicate end
738 ref_names
= git
->getRefNames(sha
, (Git::RefType
)cur_state
);
739 cur_name
= ref_names
.begin();
743 // *****************************************************************************
745 ListViewDelegate::ListViewDelegate(Git
* g
, ListViewProxy
* px
, QObject
* p
) : QItemDelegate(p
) {
753 QSize
ListViewDelegate::sizeHint(const QStyleOptionViewItem
&, const QModelIndex
&) const {
755 return QSize(laneWidth(), laneHeight
);
758 void ListViewDelegate::diffTargetChanged(int row
) {
760 if (diffTargetRow
!= row
) {
766 const Rev
* ListViewDelegate::revLookup(int row
, FileHistory
** fhPtr
) const {
768 ListView
* lv
= static_cast<ListView
*>(parent());
769 FileHistory
* fh
= static_cast<FileHistory
*>(lv
->model());
771 if (lp
->sourceModel())
772 fh
= static_cast<FileHistory
*>(lp
->sourceModel());
777 return git
->revLookup(lv
->sha(row
), fh
);
780 static QColor
blend(const QColor
& col1
, const QColor
& col2
, int amount
= 128) {
782 // Returns ((256 - amount)*col1 + amount*col2) / 256;
783 return QColor(((256 - amount
)*col1
.red() + amount
*col2
.red() ) / 256,
784 ((256 - amount
)*col1
.green() + amount
*col2
.green()) / 256,
785 ((256 - amount
)*col1
.blue() + amount
*col2
.blue() ) / 256);
788 void ListViewDelegate::paintGraphLane(QPainter
* p
, int type
, int x1
, int x2
,
789 const QColor
& col
, const QColor
& activeCol
, const QBrush
& back
) const {
791 const int padding
= 2;
795 int h
= laneHeight
/ 2;
796 int m
= (x1
+ x2
) / 2;
797 int r
= (x2
- x1
) * 1 / 3;
800 #define P_CENTER m , h
801 #define P_0 x2, h // >
802 #define P_90 m , 0 // ^
803 #define P_180 x1, h // <
804 #define P_270 m , 2 * h // v
805 #define DELTA_UR 2*(x1 - m), 2*h , 0*16, 90*16 // -,
806 #define DELTA_DR 2*(x1 - m), 2*-h, 270*16, 90*16 // -'
807 #define DELTA_UL 2*(x2 - m), 2*h , 90*16, 90*16 // ,-
808 #define DELTA_DL 2*(x2 - m), 2*-h, 180*16, 90*16 // '-
809 #define CENTER_UR x1, 2*h, 225
810 #define CENTER_DR x1, 0 , 135
811 #define CENTER_UL x2, 2*h, 315
812 #define CENTER_DL x2, 0 , 45
813 #define R_CENTER m - r, h - r, d, d
815 static QColor
const & lanePenColor
= QPalette().color(QPalette::WindowText
);
816 static QPen
lanePen(lanePenColor
, 2); // fast path here
824 QConicalGradient
gradient(CENTER_UR
);
825 gradient
.setColorAt(0.375, col
);
826 gradient
.setColorAt(0.625, activeCol
);
827 lanePen
.setBrush(gradient
);
829 p
->drawArc(P_CENTER
, DELTA_UR
);
833 QConicalGradient
gradient(CENTER_UL
);
834 gradient
.setColorAt(0.375, activeCol
);
835 gradient
.setColorAt(0.625, col
);
836 lanePen
.setBrush(gradient
);
838 p
->drawArc(P_CENTER
, DELTA_UL
);
843 QConicalGradient
gradient(CENTER_DR
);
844 gradient
.setColorAt(0.375, activeCol
);
845 gradient
.setColorAt(0.625, col
);
846 lanePen
.setBrush(gradient
);
848 p
->drawArc(P_CENTER
, DELTA_DR
);
855 lanePen
.setColor(col
);
869 p
->drawLine(P_90
, P_270
);
873 p
->drawLine(P_CENTER
, P_270
);
881 p
->drawLine(P_90
, P_CENTER
);
887 lanePen
.setColor(activeCol
);
899 p
->drawLine(P_180
, P_0
);
903 p
->drawLine(P_180
, P_CENTER
);
909 p
->drawLine(P_CENTER
, P_0
);
915 // center symbol, e.g. rect or ellipse
920 p
->setPen(Qt::black
);
922 p
->drawEllipse(R_CENTER
);
927 p
->setPen(Qt::black
);
929 p
->drawRect(R_CENTER
);
933 p
->setPen(Qt::NoPen
);
934 p
->setBrush(Qt::red
);
935 p
->drawRect(m
- r
, h
- 1, d
, 2);
939 p
->setPen(Qt::NoPen
);
940 p
->setBrush(DARK_GREEN
);
941 p
->drawRect(m
- r
, h
- 1, d
, 2);
942 p
->drawRect(m
- 1, h
- r
, 2, d
);
945 p
->setPen(Qt::black
);
947 p
->drawEllipse(R_CENTER
);
952 p
->setPen(Qt::black
);
954 p
->drawRect(R_CENTER
);
975 void ListViewDelegate::paintGraph(QPainter
* p
, const QStyleOptionViewItem
& opt
,
976 const QModelIndex
& i
) const {
977 static const QColor
& baseColor
= QPalette().color(QPalette::WindowText
);
978 static const QColor colors
[COLORS_NUM
] = { baseColor
, Qt::red
, DARK_GREEN
,
979 Qt::blue
, Qt::darkGray
, BROWN
,
980 Qt::magenta
, ORANGE
};
981 if (opt
.state
& QStyle::State_Selected
)
982 p
->fillRect(opt
.rect
, opt
.palette
.highlight());
983 else if (i
.row() & 1)
984 p
->fillRect(opt
.rect
, opt
.palette
.alternateBase());
986 p
->fillRect(opt
.rect
, opt
.palette
.base());
989 const Rev
* r
= revLookup(i
.row(), &fh
);
994 p
->setClipRect(opt
.rect
, Qt::IntersectClip
);
995 p
->translate(opt
.rect
.topLeft());
998 if (r
->lanes
.count() == 0)
999 git
->setLane(r
->sha(), fh
);
1001 QBrush back
= opt
.palette
.base();
1002 const QVector
<int>& lanes(r
->lanes
);
1003 uint laneNum
= lanes
.count();
1004 uint activeLane
= 0;
1005 for (uint i
= 0; i
< laneNum
; i
++)
1006 if (isActive(lanes
[i
])) {
1012 int maxWidth
= opt
.rect
.width();
1013 int lw
= laneWidth();
1014 QColor activeColor
= colors
[activeLane
% COLORS_NUM
];
1015 if (opt
.state
& QStyle::State_Selected
)
1016 activeColor
= blend(activeColor
, opt
.palette
.highlightedText().color(), 208);
1017 for (uint i
= 0; i
< laneNum
&& x2
< maxWidth
; i
++) {
1026 QColor color
= i
== activeLane
? activeColor
: colors
[i
% COLORS_NUM
];
1027 paintGraphLane(p
, ln
, x1
, x2
, color
, activeColor
, back
);
1032 void ListViewDelegate::paintLog(QPainter
* p
, const QStyleOptionViewItem
& opt
,
1033 const QModelIndex
& index
) const {
1035 int row
= index
.row();
1036 const Rev
* r
= revLookup(row
);
1041 p
->fillRect(opt
.rect
, changedFiles(ZERO_SHA
) ? ORANGE
: DARK_ORANGE
);
1043 if (diffTargetRow
== row
)
1044 p
->fillRect(opt
.rect
, LIGHT_BLUE
);
1046 bool isHighlighted
= lp
->isHighlighted(row
);
1047 QPixmap
* pm
= getTagMarks(r
->sha(), opt
);
1049 if (!pm
&& !isHighlighted
) { // fast path in common case
1050 QItemDelegate::paint(p
, opt
, index
);
1053 QStyleOptionViewItem
newOpt(opt
); // we need a copy
1055 p
->drawPixmap(newOpt
.rect
.x(), newOpt
.rect
.y() + 1, *pm
); // +1 means leave a pixel spacing above the pixmap
1056 newOpt
.rect
.adjust(pm
->width(), 0, 0, 0);
1060 newOpt
.font
.setBold(true);
1062 QItemDelegate::paint(p
, newOpt
, index
);
1065 void ListViewDelegate::paint(QPainter
* p
, const QStyleOptionViewItem
& opt
,
1066 const QModelIndex
& index
) const {
1068 p
->setRenderHints(QPainter::Antialiasing
);
1070 if (index
.column() == GRAPH_COL
)
1071 return paintGraph(p
, opt
, index
);
1073 if (index
.column() == LOG_COL
)
1074 return paintLog(p
, opt
, index
);
1076 return QItemDelegate::paint(p
, opt
, index
);
1079 bool ListViewDelegate::changedFiles(SCRef sha
) const {
1081 const RevFile
* f
= git
->getFiles(sha
);
1083 for (int i
= 0; i
< f
->count(); i
++)
1084 if (!f
->statusCmp(i
, RevFile::UNKNOWN
))
1089 // adapt style and name based on type
1090 void getTagMarkParams(QString
&name
, QStyleOptionViewItem
& o
,
1091 const int type
, const bool isCurrent
) {
1095 case 0: name
= "detached"; clr
= Qt::red
; break;
1096 case Git::BRANCH
: clr
= isCurrent
? Qt::green
: DARK_GREEN
; break;
1097 case Git::RMT_BRANCH
: clr
= LIGHT_ORANGE
; break;
1098 case Git::TAG
: clr
= Qt::yellow
; break;
1099 case Git::REF
: clr
= PURPLE
; break;
1102 o
.palette
.setColor(QPalette::Window
, clr
);
1103 o
.palette
.setColor(QPalette::WindowText
, QColor(Qt::black
));
1104 o
.font
.setBold(isCurrent
);
1107 QPixmap
* ListViewDelegate::getTagMarks(SCRef sha
, const QStyleOptionViewItem
& opt
) const {
1109 uint rt
= git
->checkRef(sha
);
1111 return NULL
; // common case: no refs at all
1113 QPixmap
* pm
= new QPixmap(); // must be deleted by caller
1115 for (RefNameIterator
it(sha
, git
); it
.valid(); it
.next()) {
1116 QStyleOptionViewItem
o(opt
);
1117 QString name
= it
.name();
1118 getTagMarkParams(name
, o
, it
.type(), it
.isCurrentBranch());
1119 addTextPixmap(&pm
, name
, o
);
1125 QString
ListView::refNameAt(const QPoint
&pos
)
1127 QModelIndex index
= indexAt(pos
);
1128 if (index
.column() != LOG_COL
) return QString();
1130 int spacing
= 4; // inner spacing within pixmaps (cf. addTextPixmap)
1131 int ofs
= visualRect(index
).left();
1132 for (RefNameIterator
it(sha(index
.row()), git
); it
.valid(); it
.next()) {
1133 QStyleOptionViewItem o
;
1134 QString name
= it
.name();
1135 getTagMarkParams(name
, o
, it
.type(), it
.isCurrentBranch());
1137 QFontMetrics
fm(o
.font
);
1138 ofs
+= fm
.boundingRect(name
).width() + 2*spacing
;
1139 if (pos
.x() <= ofs
) {
1140 // name found: return fully-qualified ref name (cf. Git::getRefs() for names)
1141 switch (it
.type()) {
1142 case Git::BRANCH
: return it
.name(); break;
1143 case Git::TAG
: return "tags/" + it
.name(); break;
1144 case Git::RMT_BRANCH
: return "remotes/" + it
.name(); break;
1145 case Git::REF
: return "bases/" + it
.name(); break;
1146 default: return QString(); break;
1149 ofs
+= 2; // distance between pixmaps (cf. addTextPixmap)
1154 void ListViewDelegate::addTextPixmap(QPixmap
** pp
, SCRef txt
, const QStyleOptionViewItem
& opt
) const {
1157 int ofs
= pm
->isNull() ? 0 : pm
->width() + 2;
1159 QFontMetrics
fm(opt
.font
);
1160 int pw
= fm
.boundingRect(txt
).width() + 2 * spacing
;
1161 int ph
= fm
.height();
1163 QSize
pixmapSize(ofs
+ pw
, ph
);
1165 #if QT_VERSION >= QT_VERSION_CHECK(5,6,0)
1166 qreal dpr
= qApp
->devicePixelRatio();
1167 QPixmap
* newPm
= new QPixmap(pixmapSize
* dpr
);
1168 newPm
->setDevicePixelRatio(dpr
);
1170 QPixmap
* newPm
= new QPixmap(pixmapSize
);
1175 if (!pm
->isNull()) {
1176 newPm
->fill(opt
.palette
.base().color());
1177 p
.drawPixmap(0, 0, *pm
);
1179 p
.setPen(opt
.palette
.color(QPalette::WindowText
));
1180 p
.setBrush(opt
.palette
.color(QPalette::Window
));
1181 p
.setFont(opt
.font
);
1182 p
.drawRect(ofs
, 0, pw
- 1, ph
- 1);
1183 p
.drawText(ofs
+ spacing
, fm
.ascent(), txt
);
1190 // *****************************************************************************
1192 ListViewProxy::ListViewProxy(QObject
* p
, Domain
* dm
, Git
* g
) : QSortFilterProxyModel(p
) {
1197 isHighLight
= false;
1198 setDynamicSortFilter(false);
1201 bool ListViewProxy::isMatch(SCRef sha
) const {
1203 if (colNum
== SHA_MAP_COL
)
1204 // in this case shaMap contains all good sha to search for
1205 return shaSet
.contains(sha
);
1207 const Rev
* r
= git
->revLookup(sha
);
1209 dbp("ASSERT in ListViewFilter::isMatch, sha <%1> not found", sha
);
1213 if (colNum
== LOG_COL
)
1214 target
= r
->shortLog();
1215 else if (colNum
== AUTH_COL
)
1216 target
= r
->author();
1217 else if (colNum
== LOG_MSG_COL
)
1218 target
= r
->longLog();
1219 else if (colNum
== COMMIT_COL
)
1222 // wildcard search, case insensitive
1223 return (target
.contains(filter
));
1226 bool ListViewProxy::isMatch(int source_row
) const {
1228 FileHistory
* fh
= d
->model();
1229 if (fh
->rowCount() <= source_row
) // FIXME required to avoid an ASSERT in d->isMatch()
1232 bool extFilter
= (colNum
== -1);
1233 return ((!extFilter
&& isMatch(fh
->sha(source_row
)))
1234 ||( extFilter
&& d
->isMatch(fh
->sha(source_row
))));
1237 bool ListViewProxy::isHighlighted(int row
) const {
1239 // FIXME row == source_row only because when
1240 // higlights the rows are not hidden
1241 return (isHighLight
&& isMatch(row
));
1244 bool ListViewProxy::filterAcceptsRow(int source_row
, const QModelIndex
&) const {
1246 return (isHighLight
|| isMatch(source_row
));
1249 int ListViewProxy::setFilter(bool isOn
, bool h
, SCRef fl
, int cn
, ShaSet
* s
) {
1251 filter
= QRegExp(fl
, Qt::CaseInsensitive
, QRegExp::Wildcard
);
1256 // isHighlighted() is called also when filter is off,
1257 // so reset 'isHighLight' flag in that case
1258 isHighLight
= h
&& isOn
;
1260 ListView
* lv
= static_cast<ListView
*>(parent());
1261 FileHistory
* fh
= d
->model();
1263 if (!isOn
&& sourceModel()){
1265 setSourceModel(NULL
);
1267 } else if (isOn
&& !isHighLight
) {
1268 setSourceModel(fh
); // trigger a rows scanning
1271 return (sourceModel() ? rowCount() : 0);