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>
16 #include "FileHistory.h"
23 ListView::ListView(QWidget
* parent
) : QTreeView(parent
), d(NULL
), git(NULL
), fh(NULL
), lp(NULL
) {}
25 void ListView::setup(Domain
* dm
, Git
* g
) {
31 filterNextContextMenuRequest
= false;
33 setFont(QGit::STD_FONT
);
35 // create ListViewProxy unplugged, will be plug
36 // to the model only when filtering is needed
37 lp
= new ListViewProxy(this, d
, git
);
40 ListViewDelegate
* lvd
= new ListViewDelegate(git
, lp
, this);
41 lvd
->setLaneHeight(fontMetrics().height() + 2);
44 setupGeometry(); // after setting delegate
46 // shortcuts are activated only if widget is visible, this is good
47 new QShortcut(Qt::Key_Up
, this, SLOT(on_keyUp()));
48 new QShortcut(Qt::Key_Down
, this, SLOT(on_keyDown()));
50 connect(lvd
, SIGNAL(updateView()), viewport(), SLOT(update()));
52 connect(this, SIGNAL(diffTargetChanged(int)), lvd
, SLOT(diffTargetChanged(int)));
54 connect(this, SIGNAL(customContextMenuRequested(const QPoint
&)),
55 this, SLOT(on_customContextMenuRequested(const QPoint
&)));
58 ListView::~ListView() {
60 git
->cancelDataLoading(fh
); // non blocking
63 const QString
ListView::sha(int row
) const {
65 if (!lp
->sourceModel()) // unplugged
68 QModelIndex idx
= lp
->mapToSource(lp
->index(row
, 0));
69 return fh
->sha(idx
.row());
72 int ListView::row(SCRef sha
) const {
74 if (!lp
->sourceModel()) // unplugged
77 int row
= fh
->row(sha
);
78 QModelIndex idx
= fh
->index(row
, 0);
79 return lp
->mapFromSource(idx
).row();
82 void ListView::setupGeometry() {
84 QPalette pl
= palette();
85 pl
.setColor(QPalette::Base
, ODD_LINE_COL
);
86 pl
.setColor(QPalette::AlternateBase
, EVEN_LINE_COL
);
87 setPalette(pl
); // does not seem to inherit application paletteAnnotate
89 QHeaderView
* hv
= header();
90 hv
->setStretchLastSection(true);
91 hv
->setSectionResizeMode(LOG_COL
, QHeaderView::Interactive
);
92 hv
->setSectionResizeMode(TIME_COL
, QHeaderView::Interactive
);
93 hv
->setSectionResizeMode(ANN_ID_COL
, QHeaderView::ResizeToContents
);
94 hv
->resizeSection(GRAPH_COL
, DEF_GRAPH_COL_WIDTH
);
95 hv
->resizeSection(LOG_COL
, DEF_LOG_COL_WIDTH
);
96 hv
->resizeSection(AUTH_COL
, DEF_AUTH_COL_WIDTH
);
97 hv
->resizeSection(TIME_COL
, DEF_TIME_COL_WIDTH
);
99 if (git
->isMainHistory(fh
))
100 hideColumn(ANN_ID_COL
);
103 void ListView::scrollToNextHighlighted(int direction
) {
105 // Depending on the value of direction, scroll to:
106 // -1 = the next highlighted item above the current one (i.e. newer in history)
107 // 1 = the next highlighted item below the current one (i.e. older in history)
108 // 0 = the first highlighted item from the top of the list
110 QModelIndex idx
= currentIndex();
113 idx
= idx
.sibling(0,0);
114 if (lp
->isHighlighted(idx
.row())) {
115 setCurrentIndex(idx
);
121 idx
= (direction
>= 0 ? indexBelow(idx
) : indexAbove(idx
));
125 } while (!lp
->isHighlighted(idx
.row()));
127 setCurrentIndex(idx
);
130 void ListView::scrollToCurrent(ScrollHint hint
) {
132 if (currentIndex().isValid())
133 scrollTo(currentIndex(), hint
);
136 void ListView::on_keyUp() {
138 QModelIndex idx
= indexAbove(currentIndex());
140 setCurrentIndex(idx
);
143 void ListView::on_keyDown() {
145 QModelIndex idx
= indexBelow(currentIndex());
147 setCurrentIndex(idx
);
150 void ListView::on_changeFont(const QFont
& f
) {
153 ListViewDelegate
* lvd
= static_cast<ListViewDelegate
*>(itemDelegate());
154 lvd
->setLaneHeight(fontMetrics().height());
158 const QString
ListView::currentText(int column
) {
160 QModelIndex idx
= model()->index(currentIndex().row(), column
);
161 return (idx
.isValid() ? idx
.data().toString() : "");
164 int ListView::getLaneType(SCRef sha
, int pos
) const {
166 const Rev
* r
= git
->revLookup(sha
, fh
);
167 return (r
&& pos
< r
->lanes
.count() && pos
>= 0 ? r
->lanes
.at(pos
) : -1);
170 void ListView::showIdValues() {
173 viewport()->update();
176 void ListView::getSelectedItems(QStringList
& selectedItems
) {
178 selectedItems
.clear();
179 QModelIndexList ml
= selectionModel()->selectedRows();
180 FOREACH (QModelIndexList
, it
, ml
)
181 selectedItems
.append(sha((*it
).row()));
183 // selectedRows() returns the items in an unspecified order,
184 // so be sure rows are ordered from newest to oldest.
185 selectedItems
= git
->sortShaListByIndex(selectedItems
);
188 const QString
ListView::shaFromAnnId(uint id
) {
190 if (git
->isMainHistory(fh
))
193 return sha(model()->rowCount() - id
);
196 int ListView::filterRows(bool isOn
, bool highlight
, SCRef filter
, int colNum
, ShaSet
* set
) {
198 setUpdatesEnabled(false);
199 int matchedNum
= lp
->setFilter(isOn
, highlight
, filter
, colNum
, set
);
200 viewport()->update();
201 setUpdatesEnabled(true);
206 bool ListView::update() {
208 int stRow
= row(st
->sha());
210 return false; // main/tree view asked us a sha not in history
212 QModelIndex index
= currentIndex();
213 QItemSelectionModel
* sel
= selectionModel();
215 if (index
.isValid() && (index
.row() == stRow
)) {
217 if (sel
->isSelected(index
) != st
->selectItem())
218 sel
->select(index
, QItemSelectionModel::Toggle
);
222 // setCurrentIndex() does not clear previous
223 // selections in a multi selection QListView
226 QModelIndex newIndex
= model()->index(stRow
, 0);
227 if (newIndex
.isValid()) {
229 // emits QItemSelectionModel::currentChanged()
230 setCurrentIndex(newIndex
);
232 if (!st
->selectItem())
233 sel
->select(newIndex
, QItemSelectionModel::Deselect
);
236 if (git
->isMainHistory(fh
))
237 emit
diffTargetChanged(row(st
->diffToSha()));
239 return currentIndex().isValid();
242 void ListView::currentChanged(const QModelIndex
& index
, const QModelIndex
&) {
244 SCRef selRev
= sha(index
.row());
245 if (st
->sha() != selRev
) { // to avoid looping
247 st
->setSelectItem(true);
252 bool ListView::filterRightButtonPressed(QMouseEvent
* e
) {
254 QModelIndex index
= indexAt(e
->pos());
255 SCRef selSha
= sha(index
.row());
256 if (selSha
.isEmpty())
259 if (e
->modifiers() == Qt::ControlModifier
) { // check for 'diff to' function
261 if (selSha
!= ZERO_SHA
&& st
->sha() != ZERO_SHA
) {
263 if (selSha
!= st
->diffToSha())
264 st
->setDiffToSha(selSha
);
266 st
->setDiffToSha(""); // restore std view
268 filterNextContextMenuRequest
= true;
270 return true; // filter event out
273 // check for 'children & parents' function, i.e. if mouse is on the graph
274 if (index
.column() == GRAPH_COL
) {
276 filterNextContextMenuRequest
= true;
277 QStringList parents
, children
;
278 if (getLaneParentsChildren(selSha
, e
->pos().x(), parents
, children
))
279 emit
lanesContextMenuRequested(parents
, children
);
281 return true; // filter event out
286 void ListView::mousePressEvent(QMouseEvent
* e
) {
288 if (currentIndex().isValid() && e
->button() == Qt::LeftButton
)
289 d
->setReadyToDrag(true);
291 if (e
->button() == Qt::RightButton
&& filterRightButtonPressed(e
))
292 return; // filtered out
294 QTreeView::mousePressEvent(e
);
297 void ListView::mouseReleaseEvent(QMouseEvent
* e
) {
299 d
->setReadyToDrag(false); // in case of just click without moving
300 QTreeView::mouseReleaseEvent(e
);
303 void ListView::mouseMoveEvent(QMouseEvent
* e
) {
305 if (d
->isReadyToDrag()) {
307 if (indexAt(e
->pos()).row() == currentIndex().row())
308 return; // move at least by one line to activate drag
310 if (!d
->setDragging(true))
314 getSelectedItems(selRevs
);
315 selRevs
.removeAll(ZERO_SHA
);
316 if (!selRevs
.empty())
317 emit
revisionsDragged(selRevs
); // blocking until drop event
319 d
->setDragging(false);
321 QTreeView::mouseMoveEvent(e
);
324 void ListView::dragEnterEvent(QDragEnterEvent
* e
) {
326 if (e
->mimeData()->hasFormat("text/plain"))
330 void ListView::dragMoveEvent(QDragMoveEvent
* e
) {
332 // already checked by dragEnterEvent()
336 void ListView::dropEvent(QDropEvent
*e
) {
338 SCList
remoteRevs(e
->mimeData()->text().split('\n', QString::SkipEmptyParts
));
339 if (!remoteRevs
.isEmpty()) {
340 // some sanity check on dropped data
341 SCRef
sha(remoteRevs
.first().section('@', 0, 0));
342 SCRef
remoteRepo(remoteRevs
.first().section('@', 1));
343 if (sha
.length() == 40 && !remoteRepo
.isEmpty())
344 emit
revisionsDropped(remoteRevs
);
348 void ListView::on_customContextMenuRequested(const QPoint
& pos
) {
350 QModelIndex index
= indexAt(pos
);
351 if (!index
.isValid())
354 if (filterNextContextMenuRequest
) {
355 // event filter does not work on them
356 filterNextContextMenuRequest
= false;
359 emit
contextMenu(sha(index
.row()), POPUP_LIST_EV
);
362 bool ListView::getLaneParentsChildren(SCRef sha
, int x
, SList p
, SList c
) {
364 ListViewDelegate
* lvd
= static_cast<ListViewDelegate
*>(itemDelegate());
365 uint lane
= x
/ lvd
->laneWidth();
366 int t
= getLaneType(sha
, lane
);
367 if (t
== EMPTY
|| t
== -1)
370 // first find the parents
373 if (!isFreeLane(t
)) {
374 p
= git
->revLookup(sha
, fh
)->parents(); // pointer cannot be NULL
377 SCRef
par(git
->getLaneParent(sha
, lane
));
379 dbs("ASSERT getLaneParentsChildren: parent not found");
385 // then find children
386 c
= git
->getChildren(root
);
390 // *****************************************************************************
392 ListViewDelegate::ListViewDelegate(Git
* g
, ListViewProxy
* px
, QObject
* p
) : QItemDelegate(p
) {
400 QSize
ListViewDelegate::sizeHint(const QStyleOptionViewItem
&, const QModelIndex
&) const {
402 return QSize(laneWidth(), laneHeight
);
405 void ListViewDelegate::diffTargetChanged(int row
) {
407 if (diffTargetRow
!= row
) {
413 const Rev
* ListViewDelegate::revLookup(int row
, FileHistory
** fhPtr
) const {
415 ListView
* lv
= static_cast<ListView
*>(parent());
416 FileHistory
* fh
= static_cast<FileHistory
*>(lv
->model());
418 if (lp
->sourceModel())
419 fh
= static_cast<FileHistory
*>(lp
->sourceModel());
424 return git
->revLookup(lv
->sha(row
), fh
);
427 static QColor
blend(const QColor
& col1
, const QColor
& col2
, int amount
= 128) {
429 // Returns ((256 - amount)*col1 + amount*col2) / 256;
430 return QColor(((256 - amount
)*col1
.red() + amount
*col2
.red() ) / 256,
431 ((256 - amount
)*col1
.green() + amount
*col2
.green()) / 256,
432 ((256 - amount
)*col1
.blue() + amount
*col2
.blue() ) / 256);
435 void ListViewDelegate::paintGraphLane(QPainter
* p
, int type
, int x1
, int x2
,
436 const QColor
& col
, const QColor
& activeCol
, const QBrush
& back
) const {
438 const int padding
= 2;
442 int h
= laneHeight
/ 2;
443 int m
= (x1
+ x2
) / 2;
444 int r
= (x2
- x1
) * 1 / 3;
447 #define P_CENTER m , h
448 #define P_0 x2, h // >
449 #define P_90 m , 0 // ^
450 #define P_180 x1, h // <
451 #define P_270 m , 2 * h // v
452 #define DELTA_UR 2*(x1 - m), 2*h , 0*16, 90*16 // -,
453 #define DELTA_DR 2*(x1 - m), 2*-h, 270*16, 90*16 // -'
454 #define DELTA_UL 2*(x2 - m), 2*h , 90*16, 90*16 // ,-
455 #define DELTA_DL 2*(x2 - m), 2*-h, 180*16, 90*16 // '-
456 #define CENTER_UR x1, 2*h, 225
457 #define CENTER_DR x1, 0 , 135
458 #define CENTER_UL x2, 2*h, 315
459 #define CENTER_DL x2, 0 , 45
460 #define R_CENTER m - r, h - r, d, d
462 static QColor
const & lanePenColor
= QPalette().color(QPalette::WindowText
);
463 static QPen
lanePen(lanePenColor
, 2); // fast path here
471 QConicalGradient
gradient(CENTER_UR
);
472 gradient
.setColorAt(0.375, col
);
473 gradient
.setColorAt(0.625, activeCol
);
474 lanePen
.setBrush(gradient
);
476 p
->drawArc(P_CENTER
, DELTA_UR
);
480 QConicalGradient
gradient(CENTER_UL
);
481 gradient
.setColorAt(0.375, activeCol
);
482 gradient
.setColorAt(0.625, col
);
483 lanePen
.setBrush(gradient
);
485 p
->drawArc(P_CENTER
, DELTA_UL
);
490 QConicalGradient
gradient(CENTER_DR
);
491 gradient
.setColorAt(0.375, activeCol
);
492 gradient
.setColorAt(0.625, col
);
493 lanePen
.setBrush(gradient
);
495 p
->drawArc(P_CENTER
, DELTA_DR
);
502 lanePen
.setColor(col
);
516 p
->drawLine(P_90
, P_270
);
520 p
->drawLine(P_CENTER
, P_270
);
528 p
->drawLine(P_90
, P_CENTER
);
534 lanePen
.setColor(activeCol
);
546 p
->drawLine(P_180
, P_0
);
550 p
->drawLine(P_180
, P_CENTER
);
556 p
->drawLine(P_CENTER
, P_0
);
562 // center symbol, e.g. rect or ellipse
567 p
->setPen(Qt::black
);
569 p
->drawEllipse(R_CENTER
);
574 p
->setPen(Qt::black
);
576 p
->drawRect(R_CENTER
);
580 p
->setPen(Qt::NoPen
);
581 p
->setBrush(Qt::red
);
582 p
->drawRect(m
- r
, h
- 1, d
, 2);
586 p
->setPen(Qt::NoPen
);
587 p
->setBrush(DARK_GREEN
);
588 p
->drawRect(m
- r
, h
- 1, d
, 2);
589 p
->drawRect(m
- 1, h
- r
, 2, d
);
592 p
->setPen(Qt::black
);
594 p
->drawEllipse(R_CENTER
);
599 p
->setPen(Qt::black
);
601 p
->drawRect(R_CENTER
);
622 void ListViewDelegate::paintGraph(QPainter
* p
, const QStyleOptionViewItem
& opt
,
623 const QModelIndex
& i
) const {
624 static const QColor
& baseColor
= QPalette().color(QPalette::WindowText
);
625 static const QColor colors
[COLORS_NUM
] = { baseColor
, Qt::red
, DARK_GREEN
,
626 Qt::blue
, Qt::darkGray
, BROWN
,
627 Qt::magenta
, ORANGE
};
628 if (opt
.state
& QStyle::State_Selected
)
629 p
->fillRect(opt
.rect
, opt
.palette
.highlight());
630 else if (i
.row() & 1)
631 p
->fillRect(opt
.rect
, opt
.palette
.alternateBase());
633 p
->fillRect(opt
.rect
, opt
.palette
.base());
636 const Rev
* r
= revLookup(i
.row(), &fh
);
641 p
->setClipRect(opt
.rect
, Qt::IntersectClip
);
642 p
->translate(opt
.rect
.topLeft());
645 if (r
->lanes
.count() == 0)
646 git
->setLane(r
->sha(), fh
);
648 QBrush back
= opt
.palette
.base();
649 const QVector
<int>& lanes(r
->lanes
);
650 uint laneNum
= lanes
.count();
652 for (uint i
= 0; i
< laneNum
; i
++)
653 if (isActive(lanes
[i
])) {
659 int maxWidth
= opt
.rect
.width();
660 int lw
= laneWidth();
661 QColor activeColor
= colors
[activeLane
% COLORS_NUM
];
662 if (opt
.state
& QStyle::State_Selected
)
663 activeColor
= blend(activeColor
, opt
.palette
.highlightedText().color(), 208);
664 for (uint i
= 0; i
< laneNum
&& x2
< maxWidth
; i
++) {
673 QColor color
= i
== activeLane
? activeColor
: colors
[i
% COLORS_NUM
];
674 paintGraphLane(p
, ln
, x1
, x2
, color
, activeColor
, back
);
679 void ListViewDelegate::paintLog(QPainter
* p
, const QStyleOptionViewItem
& opt
,
680 const QModelIndex
& index
) const {
682 int row
= index
.row();
683 const Rev
* r
= revLookup(row
);
688 p
->fillRect(opt
.rect
, changedFiles(ZERO_SHA
) ? ORANGE
: DARK_ORANGE
);
690 if (diffTargetRow
== row
)
691 p
->fillRect(opt
.rect
, LIGHT_BLUE
);
693 bool isHighlighted
= lp
->isHighlighted(row
);
694 QPixmap
* pm
= getTagMarks(r
->sha(), opt
);
696 if (!pm
&& !isHighlighted
) { // fast path in common case
697 QItemDelegate::paint(p
, opt
, index
);
700 QStyleOptionViewItem
newOpt(opt
); // we need a copy
702 p
->drawPixmap(newOpt
.rect
.x(), newOpt
.rect
.y() + 1, *pm
); // +1 means leave a pixel spacing above the pixmap
703 newOpt
.rect
.adjust(pm
->width(), 0, 0, 0);
707 newOpt
.font
.setBold(true);
709 QItemDelegate::paint(p
, newOpt
, index
);
712 void ListViewDelegate::paint(QPainter
* p
, const QStyleOptionViewItem
& opt
,
713 const QModelIndex
& index
) const {
715 p
->setRenderHints(QPainter::Antialiasing
);
717 if (index
.column() == GRAPH_COL
)
718 return paintGraph(p
, opt
, index
);
720 if (index
.column() == LOG_COL
)
721 return paintLog(p
, opt
, index
);
723 return QItemDelegate::paint(p
, opt
, index
);
726 bool ListViewDelegate::changedFiles(SCRef sha
) const {
728 const RevFile
* f
= git
->getFiles(sha
);
730 for (int i
= 0; i
< f
->count(); i
++)
731 if (!f
->statusCmp(i
, RevFile::UNKNOWN
))
736 QPixmap
* ListViewDelegate::getTagMarks(SCRef sha
, const QStyleOptionViewItem
& opt
) const {
738 uint rt
= git
->checkRef(sha
);
740 return NULL
; // common case
742 QPixmap
* pm
= new QPixmap(); // must be deleted by caller
744 if (rt
& Git::BRANCH
)
745 addRefPixmap(&pm
, sha
, Git::BRANCH
, opt
);
747 if (rt
& Git::RMT_BRANCH
)
748 addRefPixmap(&pm
, sha
, Git::RMT_BRANCH
, opt
);
751 addRefPixmap(&pm
, sha
, Git::TAG
, opt
);
754 addRefPixmap(&pm
, sha
, Git::REF
, opt
);
759 void ListViewDelegate::addRefPixmap(QPixmap
** pp
, SCRef sha
, int type
, QStyleOptionViewItem opt
) const {
762 SCList refs
= git
->getRefName(sha
, (Git::RefType
)type
, &curBranch
);
763 FOREACH_SL (it
, refs
) {
765 bool isCur
= (curBranch
== *it
);
766 opt
.font
.setBold(isCur
);
769 if (type
== Git::BRANCH
)
770 clr
= (isCur
? Qt::green
: DARK_GREEN
);
772 else if (type
== Git::RMT_BRANCH
)
775 else if (type
== Git::TAG
)
778 else if (type
== Git::REF
)
781 opt
.palette
.setColor(QPalette::Window
, clr
);
782 opt
.palette
.setColor(QPalette::WindowText
, QColor(Qt::black
));
783 addTextPixmap(pp
, *it
, opt
);
787 void ListViewDelegate::addTextPixmap(QPixmap
** pp
, SCRef txt
, const QStyleOptionViewItem
& opt
) const {
790 int ofs
= pm
->isNull() ? 0 : pm
->width() + 2;
792 QFontMetrics
fm(opt
.font
);
793 int pw
= fm
.boundingRect(txt
).width() + 2 * spacing
;
794 int ph
= fm
.height();
796 QPixmap
* newPm
= new QPixmap(ofs
+ pw
, ph
);
800 newPm
->fill(opt
.palette
.base().color());
801 p
.drawPixmap(0, 0, *pm
);
803 p
.setPen(opt
.palette
.color(QPalette::WindowText
));
804 p
.setBrush(opt
.palette
.color(QPalette::Window
));
806 p
.drawRect(ofs
, 0, pw
- 1, ph
- 1);
807 p
.drawText(ofs
+ spacing
, fm
.ascent(), txt
);
814 // *****************************************************************************
816 ListViewProxy::ListViewProxy(QObject
* p
, Domain
* dm
, Git
* g
) : QSortFilterProxyModel(p
) {
822 setDynamicSortFilter(false);
825 bool ListViewProxy::isMatch(SCRef sha
) const {
827 if (colNum
== SHA_MAP_COL
)
828 // in this case shaMap contains all good sha to search for
829 return shaSet
.contains(sha
);
831 const Rev
* r
= git
->revLookup(sha
);
833 dbp("ASSERT in ListViewFilter::isMatch, sha <%1> not found", sha
);
837 if (colNum
== LOG_COL
)
838 target
= r
->shortLog();
839 else if (colNum
== AUTH_COL
)
840 target
= r
->author();
841 else if (colNum
== LOG_MSG_COL
)
842 target
= r
->longLog();
843 else if (colNum
== COMMIT_COL
)
846 // wildcard search, case insensitive
847 return (target
.contains(filter
));
850 bool ListViewProxy::isMatch(int source_row
) const {
852 FileHistory
* fh
= d
->model();
853 if (fh
->rowCount() <= source_row
) // FIXME required to avoid an ASSERT in d->isMatch()
856 bool extFilter
= (colNum
== -1);
857 return ((!extFilter
&& isMatch(fh
->sha(source_row
)))
858 ||( extFilter
&& d
->isMatch(fh
->sha(source_row
))));
861 bool ListViewProxy::isHighlighted(int row
) const {
863 // FIXME row == source_row only because when
864 // higlights the rows are not hidden
865 return (isHighLight
&& isMatch(row
));
868 bool ListViewProxy::filterAcceptsRow(int source_row
, const QModelIndex
&) const {
870 return (isHighLight
|| isMatch(source_row
));
873 int ListViewProxy::setFilter(bool isOn
, bool h
, SCRef fl
, int cn
, ShaSet
* s
) {
875 filter
= QRegExp(fl
, Qt::CaseInsensitive
, QRegExp::Wildcard
);
880 // isHighlighted() is called also when filter is off,
881 // so reset 'isHighLight' flag in that case
882 isHighLight
= h
&& isOn
;
884 ListView
* lv
= static_cast<ListView
*>(parent());
885 FileHistory
* fh
= d
->model();
887 if (!isOn
&& sourceModel()){
889 setSourceModel(NULL
);
891 } else if (isOn
&& !isHighLight
) {
892 setSourceModel(fh
); // trigger a rows scanning
895 return (sourceModel() ? rowCount() : 0);