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>
11 #include <QMouseEvent>
21 ListView::ListView(QWidget
* parent
) : QTreeView(parent
), d(NULL
), git(NULL
), fh(NULL
), lp(NULL
) {}
23 void ListView::setup(Domain
* dm
, Git
* g
) {
29 filterNextContextMenuRequest
= false;
31 setFont(QGit::STD_FONT
);
33 // create ListViewProxy unplugged, will be plug
34 // to the model only when filtering is needed
35 lp
= new ListViewProxy(this, d
, git
);
38 ListViewDelegate
* lvd
= new ListViewDelegate(git
, lp
, this);
39 lvd
->setLaneHeight(fontMetrics().height() + 2);
42 setupGeometry(); // after setting delegate
44 // shortcuts are activated only if widget is visible, this is good
45 new QShortcut(Qt::Key_Up
, this, SLOT(on_keyUp()));
46 new QShortcut(Qt::Key_Down
, this, SLOT(on_keyDown()));
48 connect(lvd
, SIGNAL(updateView()), viewport(), SLOT(update()));
50 connect(this, SIGNAL(diffTargetChanged(int)), lvd
, SLOT(diffTargetChanged(int)));
52 connect(this, SIGNAL(customContextMenuRequested(const QPoint
&)),
53 this, SLOT(on_customContextMenuRequested(const QPoint
&)));
56 ListView::~ListView() {
58 git
->cancelDataLoading(fh
); // non blocking
61 const QString
ListView::sha(int row
) const {
63 if (!lp
->sourceModel()) // unplugged
66 QModelIndex idx
= lp
->mapToSource(lp
->index(row
, 0));
67 return fh
->sha(idx
.row());
70 int ListView::row(SCRef sha
) const {
72 if (!lp
->sourceModel()) // unplugged
75 int row
= fh
->row(sha
);
76 QModelIndex idx
= fh
->index(row
, 0);
77 return lp
->mapFromSource(idx
).row();
80 void ListView::setupGeometry() {
82 QPalette pl
= palette();
83 pl
.setColor(QPalette::Base
, ODD_LINE_COL
);
84 pl
.setColor(QPalette::AlternateBase
, EVEN_LINE_COL
);
85 setPalette(pl
); // does not seem to inherit application paletteAnnotate
87 QHeaderView
* hv
= header();
88 hv
->setStretchLastSection(true);
89 hv
->setResizeMode(LOG_COL
, QHeaderView::Interactive
);
90 hv
->setResizeMode(TIME_COL
, QHeaderView::Interactive
);
91 hv
->setResizeMode(ANN_ID_COL
, QHeaderView::ResizeToContents
);
92 hv
->resizeSection(GRAPH_COL
, DEF_GRAPH_COL_WIDTH
);
93 hv
->resizeSection(LOG_COL
, DEF_LOG_COL_WIDTH
);
94 hv
->resizeSection(AUTH_COL
, DEF_AUTH_COL_WIDTH
);
95 hv
->resizeSection(TIME_COL
, DEF_TIME_COL_WIDTH
);
97 if (git
->isMainHistory(fh
))
98 hideColumn(ANN_ID_COL
);
101 void ListView::scrollToNextHighlighted(int direction
) {
103 // Depending on the value of direction, scroll to:
104 // -1 = the next highlighted item above the current one (i.e. newer in history)
105 // 1 = the next highlighted item below the current one (i.e. older in history)
106 // 0 = the first highlighted item from the top of the list
108 QModelIndex idx
= currentIndex();
111 idx
= idx
.sibling(0,0);
112 if (lp
->isHighlighted(idx
.row())) {
113 setCurrentIndex(idx
);
119 idx
= (direction
>= 0 ? indexBelow(idx
) : indexAbove(idx
));
123 } while (!lp
->isHighlighted(idx
.row()));
125 setCurrentIndex(idx
);
128 void ListView::scrollToCurrent(ScrollHint hint
) {
130 if (currentIndex().isValid())
131 scrollTo(currentIndex(), hint
);
134 void ListView::on_keyUp() {
136 QModelIndex idx
= indexAbove(currentIndex());
138 setCurrentIndex(idx
);
141 void ListView::on_keyDown() {
143 QModelIndex idx
= indexBelow(currentIndex());
145 setCurrentIndex(idx
);
148 void ListView::on_changeFont(const QFont
& f
) {
151 ListViewDelegate
* lvd
= static_cast<ListViewDelegate
*>(itemDelegate());
152 lvd
->setLaneHeight(fontMetrics().height());
156 const QString
ListView::currentText(int column
) {
158 QModelIndex idx
= model()->index(currentIndex().row(), column
);
159 return (idx
.isValid() ? idx
.data().toString() : "");
162 int ListView::getLaneType(SCRef sha
, int pos
) const {
164 const Rev
* r
= git
->revLookup(sha
, fh
);
165 return (r
&& pos
< r
->lanes
.count() && pos
>= 0 ? r
->lanes
.at(pos
) : -1);
168 void ListView::showIdValues() {
171 viewport()->update();
174 void ListView::getSelectedItems(QStringList
& selectedItems
) {
176 selectedItems
.clear();
177 QModelIndexList ml
= selectionModel()->selectedRows();
178 FOREACH (QModelIndexList
, it
, ml
)
179 selectedItems
.append(sha((*it
).row()));
181 // selectedRows() returns the items in an unspecified order,
182 // so be sure rows are ordered from newest to oldest.
183 selectedItems
= git
->sortShaListByIndex(selectedItems
);
186 const QString
ListView::shaFromAnnId(uint id
) {
188 if (git
->isMainHistory(fh
))
191 return sha(model()->rowCount() - id
);
194 int ListView::filterRows(bool isOn
, bool highlight
, SCRef filter
, int colNum
, ShaSet
* set
) {
196 setUpdatesEnabled(false);
197 int matchedNum
= lp
->setFilter(isOn
, highlight
, filter
, colNum
, set
);
198 viewport()->update();
199 setUpdatesEnabled(true);
204 bool ListView::update() {
206 int stRow
= row(st
->sha());
208 return false; // main/tree view asked us a sha not in history
210 QModelIndex index
= currentIndex();
211 QItemSelectionModel
* sel
= selectionModel();
213 if (index
.isValid() && (index
.row() == stRow
)) {
215 if (sel
->isSelected(index
) != st
->selectItem())
216 sel
->select(index
, QItemSelectionModel::Toggle
);
220 // setCurrentIndex() does not clear previous
221 // selections in a multi selection QListView
224 QModelIndex newIndex
= model()->index(stRow
, 0);
225 if (newIndex
.isValid()) {
227 // emits QItemSelectionModel::currentChanged()
228 setCurrentIndex(newIndex
);
230 if (!st
->selectItem())
231 sel
->select(newIndex
, QItemSelectionModel::Deselect
);
234 if (git
->isMainHistory(fh
))
235 emit
diffTargetChanged(row(st
->diffToSha()));
237 return currentIndex().isValid();
240 void ListView::currentChanged(const QModelIndex
& index
, const QModelIndex
&) {
242 SCRef selRev
= sha(index
.row());
243 if (st
->sha() != selRev
) { // to avoid looping
245 st
->setSelectItem(true);
250 bool ListView::filterRightButtonPressed(QMouseEvent
* e
) {
252 QModelIndex index
= indexAt(e
->pos());
253 SCRef selSha
= sha(index
.row());
254 if (selSha
.isEmpty())
257 if (e
->modifiers() == Qt::ControlModifier
) { // check for 'diff to' function
259 if (selSha
!= ZERO_SHA
&& st
->sha() != ZERO_SHA
) {
261 if (selSha
!= st
->diffToSha())
262 st
->setDiffToSha(selSha
);
264 st
->setDiffToSha(""); // restore std view
266 filterNextContextMenuRequest
= true;
268 return true; // filter event out
271 // check for 'children & parents' function, i.e. if mouse is on the graph
272 if (index
.column() == GRAPH_COL
) {
274 filterNextContextMenuRequest
= true;
275 QStringList parents
, children
;
276 if (getLaneParentsChildren(selSha
, e
->pos().x(), parents
, children
))
277 emit
lanesContextMenuRequested(parents
, children
);
279 return true; // filter event out
284 void ListView::mousePressEvent(QMouseEvent
* e
) {
286 if (currentIndex().isValid() && e
->button() == Qt::LeftButton
)
287 d
->setReadyToDrag(true);
289 if (e
->button() == Qt::RightButton
&& filterRightButtonPressed(e
))
290 return; // filtered out
292 QTreeView::mousePressEvent(e
);
295 void ListView::mouseReleaseEvent(QMouseEvent
* e
) {
297 d
->setReadyToDrag(false); // in case of just click without moving
298 QTreeView::mouseReleaseEvent(e
);
301 void ListView::mouseMoveEvent(QMouseEvent
* e
) {
303 if (d
->isReadyToDrag()) {
305 if (indexAt(e
->pos()).row() == currentIndex().row())
306 return; // move at least by one line to activate drag
308 if (!d
->setDragging(true))
312 getSelectedItems(selRevs
);
313 selRevs
.removeAll(ZERO_SHA
);
314 if (!selRevs
.empty())
315 emit
revisionsDragged(selRevs
); // blocking until drop event
317 d
->setDragging(false);
319 QTreeView::mouseMoveEvent(e
);
322 void ListView::dragEnterEvent(QDragEnterEvent
* e
) {
324 if (e
->mimeData()->hasFormat("text/plain"))
328 void ListView::dragMoveEvent(QDragMoveEvent
* e
) {
330 // already checked by dragEnterEvent()
334 void ListView::dropEvent(QDropEvent
*e
) {
336 SCList
remoteRevs(e
->mimeData()->text().split('\n', QString::SkipEmptyParts
));
337 if (!remoteRevs
.isEmpty()) {
338 // some sanity check on dropped data
339 SCRef
sha(remoteRevs
.first().section('@', 0, 0));
340 SCRef
remoteRepo(remoteRevs
.first().section('@', 1));
341 if (sha
.length() == 40 && !remoteRepo
.isEmpty())
342 emit
revisionsDropped(remoteRevs
);
346 void ListView::on_customContextMenuRequested(const QPoint
& pos
) {
348 QModelIndex index
= indexAt(pos
);
349 if (!index
.isValid())
352 if (filterNextContextMenuRequest
) {
353 // event filter does not work on them
354 filterNextContextMenuRequest
= false;
357 emit
contextMenu(sha(index
.row()), POPUP_LIST_EV
);
360 bool ListView::getLaneParentsChildren(SCRef sha
, int x
, SList p
, SList c
) {
362 ListViewDelegate
* lvd
= static_cast<ListViewDelegate
*>(itemDelegate());
363 uint lane
= x
/ lvd
->laneWidth();
364 int t
= getLaneType(sha
, lane
);
365 if (t
== EMPTY
|| t
== -1)
368 // first find the parents
371 if (!isFreeLane(t
)) {
372 p
= git
->revLookup(sha
, fh
)->parents(); // pointer cannot be NULL
375 SCRef
par(git
->getLaneParent(sha
, lane
));
377 dbs("ASSERT getLaneParentsChildren: parent not found");
383 // then find children
384 c
= git
->getChildren(root
);
388 // *****************************************************************************
390 ListViewDelegate::ListViewDelegate(Git
* g
, ListViewProxy
* px
, QObject
* p
) : QItemDelegate(p
) {
398 QSize
ListViewDelegate::sizeHint(const QStyleOptionViewItem
&, const QModelIndex
&) const {
400 return QSize(laneWidth(), laneHeight
);
403 void ListViewDelegate::diffTargetChanged(int row
) {
405 if (diffTargetRow
!= row
) {
411 const Rev
* ListViewDelegate::revLookup(int row
, FileHistory
** fhPtr
) const {
413 ListView
* lv
= static_cast<ListView
*>(parent());
414 FileHistory
* fh
= static_cast<FileHistory
*>(lv
->model());
416 if (lp
->sourceModel())
417 fh
= static_cast<FileHistory
*>(lp
->sourceModel());
422 return git
->revLookup(lv
->sha(row
), fh
);
425 static QColor
blend(const QColor
& col1
, const QColor
& col2
, int amount
= 128) {
427 // Returns ((256 - amount)*col1 + amount*col2) / 256;
428 return QColor(((256 - amount
)*col1
.red() + amount
*col2
.red() ) / 256,
429 ((256 - amount
)*col1
.green() + amount
*col2
.green()) / 256,
430 ((256 - amount
)*col1
.blue() + amount
*col2
.blue() ) / 256);
433 void ListViewDelegate::paintGraphLane(QPainter
* p
, int type
, int x1
, int x2
,
434 const QColor
& col
, const QColor
& activeCol
, const QBrush
& back
) const {
436 const int padding
= 2;
440 int h
= laneHeight
/ 2;
441 int m
= (x1
+ x2
) / 2;
442 int r
= (x2
- x1
) * 1 / 3;
445 #define P_CENTER m , h
446 #define P_0 x2, h // >
447 #define P_90 m , 0 // ^
448 #define P_180 x1, h // <
449 #define P_270 m , 2 * h // v
450 #define DELTA_UR 2*(x1 - m), 2*h , 0*16, 90*16 // -,
451 #define DELTA_DR 2*(x1 - m), 2*-h, 270*16, 90*16 // -'
452 #define DELTA_UL 2*(x2 - m), 2*h , 90*16, 90*16 // ,-
453 #define DELTA_DL 2*(x2 - m), 2*-h, 180*16, 90*16 // '-
454 #define CENTER_UR x1, 2*h, 225
455 #define CENTER_DR x1, 0 , 135
456 #define CENTER_UL x2, 2*h, 315
457 #define CENTER_DL x2, 0 , 45
458 #define R_CENTER m - r, h - r, d, d
460 static QPen
lanePen(Qt::black
, 2); // fast path here
468 QConicalGradient
gradient(CENTER_UR
);
469 gradient
.setColorAt(0.375, col
);
470 gradient
.setColorAt(0.625, activeCol
);
471 lanePen
.setBrush(gradient
);
473 p
->drawArc(P_CENTER
, DELTA_UR
);
477 QConicalGradient
gradient(CENTER_UL
);
478 gradient
.setColorAt(0.375, activeCol
);
479 gradient
.setColorAt(0.625, col
);
480 lanePen
.setBrush(gradient
);
482 p
->drawArc(P_CENTER
, DELTA_UL
);
487 QConicalGradient
gradient(CENTER_DR
);
488 gradient
.setColorAt(0.375, activeCol
);
489 gradient
.setColorAt(0.625, col
);
490 lanePen
.setBrush(gradient
);
492 p
->drawArc(P_CENTER
, DELTA_DR
);
499 lanePen
.setColor(col
);
513 p
->drawLine(P_90
, P_270
);
517 p
->drawLine(P_CENTER
, P_270
);
525 p
->drawLine(P_90
, P_CENTER
);
531 lanePen
.setColor(activeCol
);
543 p
->drawLine(P_180
, P_0
);
547 p
->drawLine(P_180
, P_CENTER
);
553 p
->drawLine(P_CENTER
, P_0
);
559 // center symbol, e.g. rect or ellipse
564 p
->setPen(Qt::black
);
566 p
->drawEllipse(R_CENTER
);
571 p
->setPen(Qt::black
);
573 p
->drawRect(R_CENTER
);
577 p
->setPen(Qt::NoPen
);
578 p
->setBrush(Qt::red
);
579 p
->drawRect(m
- r
, h
- 1, d
, 2);
583 p
->setPen(Qt::NoPen
);
584 p
->setBrush(DARK_GREEN
);
585 p
->drawRect(m
- r
, h
- 1, d
, 2);
586 p
->drawRect(m
- 1, h
- r
, 2, d
);
589 p
->setPen(Qt::black
);
591 p
->drawEllipse(R_CENTER
);
596 p
->setPen(Qt::black
);
598 p
->drawRect(R_CENTER
);
619 void ListViewDelegate::paintGraph(QPainter
* p
, const QStyleOptionViewItem
& opt
,
620 const QModelIndex
& i
) const {
622 static const QColor colors
[COLORS_NUM
] = { Qt::red
, DARK_GREEN
,
623 Qt::blue
, Qt::darkGray
, BROWN
,
624 Qt::magenta
, ORANGE
};
625 if (opt
.state
& QStyle::State_Selected
)
626 p
->fillRect(opt
.rect
, opt
.palette
.highlight());
627 else if (i
.row() & 1)
628 p
->fillRect(opt
.rect
, opt
.palette
.alternateBase());
630 p
->fillRect(opt
.rect
, opt
.palette
.base());
633 const Rev
* r
= revLookup(i
.row(), &fh
);
638 p
->setClipRect(opt
.rect
, Qt::IntersectClip
);
639 p
->translate(opt
.rect
.topLeft());
642 if (r
->lanes
.count() == 0)
643 git
->setLane(r
->sha(), fh
);
645 QBrush back
= opt
.palette
.base();
646 const QVector
<int>& lanes(r
->lanes
);
647 uint laneNum
= lanes
.count();
649 for (uint i
= 0; i
< laneNum
; i
++)
650 if (isActive(lanes
[i
])) {
656 int maxWidth
= opt
.rect
.width();
657 int lw
= laneWidth();
658 QColor activeColor
= colors
[activeLane
% COLORS_NUM
];
659 if (opt
.state
& QStyle::State_Selected
)
660 activeColor
= blend(activeColor
, opt
.palette
.highlightedText().color(), 208);
661 for (uint i
= 0; i
< laneNum
&& x2
< maxWidth
; i
++) {
670 QColor color
= i
== activeLane
? activeColor
: colors
[i
% COLORS_NUM
];
671 paintGraphLane(p
, ln
, x1
, x2
, color
, activeColor
, back
);
676 void ListViewDelegate::paintLog(QPainter
* p
, const QStyleOptionViewItem
& opt
,
677 const QModelIndex
& index
) const {
679 int row
= index
.row();
680 const Rev
* r
= revLookup(row
);
685 p
->fillRect(opt
.rect
, changedFiles(ZERO_SHA
) ? ORANGE
: DARK_ORANGE
);
687 if (diffTargetRow
== row
)
688 p
->fillRect(opt
.rect
, LIGHT_BLUE
);
690 bool isHighlighted
= lp
->isHighlighted(row
);
691 QPixmap
* pm
= getTagMarks(r
->sha(), opt
);
693 if (!pm
&& !isHighlighted
) { // fast path in common case
694 QItemDelegate::paint(p
, opt
, index
);
697 QStyleOptionViewItem
newOpt(opt
); // we need a copy
699 p
->drawPixmap(newOpt
.rect
.x(), newOpt
.rect
.y() + 1, *pm
); // +1 means leave a pixel spacing above the pixmap
700 newOpt
.rect
.adjust(pm
->width(), 0, 0, 0);
704 newOpt
.font
.setBold(true);
706 QItemDelegate::paint(p
, newOpt
, index
);
709 void ListViewDelegate::paint(QPainter
* p
, const QStyleOptionViewItem
& opt
,
710 const QModelIndex
& index
) const {
712 p
->setRenderHints(QPainter::Antialiasing
);
714 if (index
.column() == GRAPH_COL
)
715 return paintGraph(p
, opt
, index
);
717 if (index
.column() == LOG_COL
)
718 return paintLog(p
, opt
, index
);
720 return QItemDelegate::paint(p
, opt
, index
);
723 bool ListViewDelegate::changedFiles(SCRef sha
) const {
725 const RevFile
* f
= git
->getFiles(sha
);
727 for (int i
= 0; i
< f
->count(); i
++)
728 if (!f
->statusCmp(i
, RevFile::UNKNOWN
))
733 QPixmap
* ListViewDelegate::getTagMarks(SCRef sha
, const QStyleOptionViewItem
& opt
) const {
735 uint rt
= git
->checkRef(sha
);
737 return NULL
; // common case
739 QPixmap
* pm
= new QPixmap(); // must be deleted by caller
741 if (rt
& Git::BRANCH
)
742 addRefPixmap(&pm
, sha
, Git::BRANCH
, opt
);
744 if (rt
& Git::RMT_BRANCH
)
745 addRefPixmap(&pm
, sha
, Git::RMT_BRANCH
, opt
);
748 addRefPixmap(&pm
, sha
, Git::TAG
, opt
);
751 addRefPixmap(&pm
, sha
, Git::REF
, opt
);
756 void ListViewDelegate::addRefPixmap(QPixmap
** pp
, SCRef sha
, int type
, QStyleOptionViewItem opt
) const {
759 SCList refs
= git
->getRefName(sha
, (Git::RefType
)type
, &curBranch
);
760 FOREACH_SL (it
, refs
) {
762 bool isCur
= (curBranch
== *it
);
763 opt
.font
.setBold(isCur
);
766 if (type
== Git::BRANCH
)
767 clr
= (isCur
? Qt::green
: DARK_GREEN
);
769 else if (type
== Git::RMT_BRANCH
)
772 else if (type
== Git::TAG
)
775 else if (type
== Git::REF
)
778 opt
.palette
.setColor(QPalette::Window
, clr
);
779 addTextPixmap(pp
, *it
, opt
);
783 void ListViewDelegate::addTextPixmap(QPixmap
** pp
, SCRef txt
, const QStyleOptionViewItem
& opt
) const {
786 int ofs
= pm
->isNull() ? 0 : pm
->width() + 2;
788 QFontMetrics
fm(opt
.font
);
789 int pw
= fm
.boundingRect(txt
).width() + 2 * spacing
;
790 int ph
= fm
.height();
792 QPixmap
* newPm
= new QPixmap(ofs
+ pw
, ph
);
796 newPm
->fill(opt
.palette
.base().color());
797 p
.drawPixmap(0, 0, *pm
);
799 p
.setPen(opt
.palette
.color(QPalette::WindowText
));
800 p
.setBrush(opt
.palette
.color(QPalette::Window
));
802 p
.drawRect(ofs
, 0, pw
- 1, ph
- 1);
803 p
.drawText(ofs
+ spacing
, fm
.ascent(), txt
);
810 // *****************************************************************************
812 ListViewProxy::ListViewProxy(QObject
* p
, Domain
* dm
, Git
* g
) : QSortFilterProxyModel(p
) {
818 setDynamicSortFilter(false);
821 bool ListViewProxy::isMatch(SCRef sha
) const {
823 if (colNum
== SHA_MAP_COL
)
824 // in this case shaMap contains all good sha to search for
825 return shaSet
.contains(sha
);
827 const Rev
* r
= git
->revLookup(sha
);
829 dbp("ASSERT in ListViewFilter::isMatch, sha <%1> not found", sha
);
833 if (colNum
== LOG_COL
)
834 target
= r
->shortLog();
835 else if (colNum
== AUTH_COL
)
836 target
= r
->author();
837 else if (colNum
== LOG_MSG_COL
)
838 target
= r
->longLog();
839 else if (colNum
== COMMIT_COL
)
842 // wildcard search, case insensitive
843 return (target
.contains(filter
));
846 bool ListViewProxy::isMatch(int source_row
) const {
848 FileHistory
* fh
= d
->model();
849 if (fh
->rowCount() <= source_row
) // FIXME required to avoid an ASSERT in d->isMatch()
852 bool extFilter
= (colNum
== -1);
853 return ((!extFilter
&& isMatch(fh
->sha(source_row
)))
854 ||( extFilter
&& d
->isMatch(fh
->sha(source_row
))));
857 bool ListViewProxy::isHighlighted(int row
) const {
859 // FIXME row == source_row only because when
860 // higlights the rows are not hidden
861 return (isHighLight
&& isMatch(row
));
864 bool ListViewProxy::filterAcceptsRow(int source_row
, const QModelIndex
&) const {
866 return (isHighLight
|| isMatch(source_row
));
869 int ListViewProxy::setFilter(bool isOn
, bool h
, SCRef fl
, int cn
, ShaSet
* s
) {
871 filter
= QRegExp(fl
, Qt::CaseInsensitive
, QRegExp::Wildcard
);
876 // isHighlighted() is called also when filter is off,
877 // so reset 'isHighLight' flag in that case
878 isHighLight
= h
&& isOn
;
880 ListView
* lv
= static_cast<ListView
*>(parent());
881 FileHistory
* fh
= d
->model();
883 if (!isOn
&& sourceModel()){
885 setSourceModel(NULL
);
887 } else if (isOn
&& !isHighLight
) {
888 setSourceModel(fh
); // trigger a rows scanning
891 return (sourceModel() ? rowCount() : 0);