Silence compilation warnings
[qgit4/redivivus.git] / src / listview.cpp
blobd49545ed29451778e0052bd90135f16d15b2f1b3
1 /*
2 Description: qgit revision list view
4 Author: Marco Costalba (C) 2005-2007
6 Copyright: See COPYING file that comes with this distribution
8 */
9 #include <QApplication>
10 #include <QHeaderView>
11 #include <QMimeData>
12 #include <QMouseEvent>
13 #include <QPainter>
14 #include <QPixmap>
15 #include <QShortcut>
16 #include <QDrag>
17 #include <QUrl>
18 #include "FileHistory.h"
19 #include "domain.h"
20 #include "mainimpl.h"
21 #include "git.h"
22 #include "listview.h"
24 void getTagMarkParams(QString &name, QStyleOptionViewItem& o,
25 const int type, const bool isCurrent);
26 uint refTypeFromName(SCRef name);
28 using namespace QGit;
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) {
34 d = dm;
35 git = g;
36 fh = d->model();
37 st = &(d->st);
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);
45 setModel(fh);
47 ListViewDelegate* lvd = new ListViewDelegate(git, lp, this);
48 lvd->setLaneHeight(fontMetrics().height() + 2);
49 setItemDelegate(lvd);
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
73 return fh->sha(row);
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
82 return fh->row(sha);
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);
102 #else
103 hv->setResizeMode(LOG_COL, QHeaderView::Interactive);
104 hv->setResizeMode(TIME_COL, QHeaderView::Interactive);
105 hv->setResizeMode(ANN_ID_COL, QHeaderView::ResizeToContents);
106 #endif
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();
125 if (!direction) {
126 idx = idx.sibling(0,0);
127 if (lp->isHighlighted(idx.row())) {
128 setCurrentIndex(idx);
129 return;
133 do {
134 idx = (direction >= 0 ? indexBelow(idx) : indexAbove(idx));
135 if (!idx.isValid())
136 return;
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());
152 if (idx.isValid())
153 setCurrentIndex(idx);
156 void ListView::on_keyDown() {
158 QModelIndex idx = indexBelow(currentIndex());
159 if (idx.isValid())
160 setCurrentIndex(idx);
163 void ListView::on_changeFont(const QFont& f) {
165 setFont(f);
166 ListViewDelegate* lvd = static_cast<ListViewDelegate*>(itemDelegate());
167 lvd->setLaneHeight(fontMetrics().height());
168 scrollToCurrent();
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() {
185 fh->setAnnIdValid();
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))
204 return "";
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);
215 UPDATE_DOMAIN(d);
216 return matchedNum;
219 bool ListView::update() {
221 int stRow = row(st->sha());
222 if (stRow == -1)
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);
233 scrollTo(index);
234 } else {
235 // setCurrentIndex() does not clear previous
236 // selections in a multi selection QListView
237 clearSelection();
239 QModelIndex newIndex = model()->index(stRow, 0);
240 if (newIndex.isValid()) {
242 // emits QItemSelectionModel::currentChanged()
243 setCurrentIndex(newIndex);
244 scrollTo(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
259 st->setSha(selRev);
260 st->setSelectItem(true);
261 UPDATE_DOMAIN(d);
265 void ListView::markDiffToSha(SCRef sha) {
266 if (sha != st->diffToSha()) {
267 st->setDiffToSha(sha);
268 emit showStatusMessage("Marked " + sha + " for diff. (Ctrl-RightClick)");
269 } else {
270 st->setDiffToSha(""); // restore std view
271 emit showStatusMessage("Unmarked diff reference.");
273 UPDATE_DOMAIN(d);
276 bool ListView::filterRightButtonPressed(QMouseEvent* e) {
278 QModelIndex index = indexAt(e->pos());
279 SCRef selSha = sha(index.row());
280 if (selSha.isEmpty())
281 return false;
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
301 return false;
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);
329 int spacing = 4;
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);
337 QString dummy;
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);
341 row = 1;
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;
355 painter.end();
356 return pixmap;
359 void ListView::startDragging(QMouseEvent* /*e*/) {
361 QStringList selRevs;
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;
371 // compose mime data
372 bool contiguous = git->isContiguous(selRevs);
374 // standard text for range description
375 if (contiguous) {
376 QString text;
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) {
405 startDragging(e);
406 return;
409 QTreeView::mouseMoveEvent(e);
412 struct ListView::DropInfo {
413 DropInfo (uint f) : flags(f) {}
415 enum Flags {
416 PATCHES = 1 << 0,
417 REV_LIST = 1 << 1,
418 REV_RANGE = 1 << 2,
419 SAME_REPO = 1 << 3,
421 enum Action {
422 PatchAction = Qt::CopyAction,
423 RebaseAction = Qt::MoveAction,
424 MoveRefAction = (Qt::LinkAction << 1) | Qt::MoveAction,
425 MergeAction = Qt::LinkAction,
428 QString sourceRepo;
429 QString sourceRef;
430 uint sourceRefType;
431 Action action;
432 uint flags;
433 QStringList shas;
435 static QString actionName (ListView::DropInfo::Action a) {
436 switch (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;
450 return 0;
453 void ListView::dragEnterEvent(QDragEnterEvent* e) {
455 if (dropInfo) delete dropInfo;
456 dropInfo = NULL;
457 bool bCleanWorkDir = git->isNothingToCommit();
459 // accept local file urls for patching
460 bool valid=true;
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);
467 e->accept();
470 // parse internal mime format
471 if (!e->mimeData()->hasFormat("application/x-qgit-revs"))
472 return;
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);
484 // check source repo
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);
489 e->ignore();
490 return;
492 } else
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);
500 e->ignore();
501 return;
503 e->accept();
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)) {
524 e->ignore();
525 emit showStatusMessage("Cannot drop onto current selection.");
526 return;
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
551 e->ignore();
552 showStatusMessage("");
553 return;
556 e->accept();
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;
564 QString statusMsg;
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
572 switch (action) {
573 case DropInfo::PatchAction:
574 statusMsg += "Applying patches";
575 break;
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);
581 break;
582 case DropInfo::MoveRefAction:
583 statusMsg += "Moving " + dropInfo->sourceRef;
584 break;
585 case DropInfo::MergeAction:
586 statusMsg += "Merging selected branches into " + targetRef;
587 break;
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;
597 dropInfo = NULL;
598 showStatusMessage("");
601 void ListView::dropEvent(QDropEvent *e) {
603 if (dropInfo->flags & DropInfo::PATCHES) {
604 QStringList files;
605 QList<QUrl> urls = e->mimeData()->urls();
606 FOREACH(QList<QUrl>, it, urls)
607 files << it->toLocalFile();
609 emit applyPatches(files);
610 return;
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);
619 break;
620 case DropInfo::RebaseAction:
621 if (dropInfo->sourceRefType == Git::BRANCH && dropInfo->shas.count() == 1)
622 emit rebase("", dropInfo->sourceRef, targetSHA);
623 else
624 emit rebase(dropInfo->shas.last(),
625 dropInfo->sourceRef.isEmpty() ? dropInfo->shas.first()
626 : dropInfo->sourceRef,
627 targetSHA);
628 break;
629 case DropInfo::MergeAction:
630 emit merge(dropInfo->shas, targetRef);
631 break;
632 case DropInfo::MoveRefAction:
633 emit moveRef(dropInfo->sourceRef, targetSHA);
634 break;
638 void ListView::on_customContextMenuRequested(const QPoint& pos) {
640 QModelIndex index = indexAt(pos);
641 if (!index.isValid())
642 return;
644 if (filterNextContextMenuRequest) {
645 // event filter does not work on them
646 filterNextContextMenuRequest = false;
647 return;
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)
658 return false;
660 // first find the parents
661 p.clear();
662 QString root;
663 if (!isFreeLane(t)) {
664 p = git->revLookup(sha, fh)->parents(); // pointer cannot be NULL
665 root = sha;
666 } else {
667 SCRef par(git->getLaneParent(sha, lane));
668 if (par.isEmpty()) {
669 dbs("ASSERT getLaneParentsChildren: parent not found");
670 return false;
672 p.append(par);
673 root = p.first();
675 // then find children
676 c = git->getChildren(root);
677 return true;
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 {
685 Git* git;
686 const QString sha;
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;
691 QString cur_branch;
693 public:
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;}
700 void next();
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
709 return;
712 // initialize dummy string list
713 ref_names << "";
714 cur_name = ref_names.begin();
716 // detached ?
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
721 next();
725 void RefNameIterator::next()
727 ++cur_name;
729 // switch to next ref type if required
730 while (valid() && cur_name == ref_names.end()) {
731 switch (cur_state) {
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) {
747 git = g;
748 lp = px;
749 laneHeight = 0;
750 diffTargetRow = -1;
753 QSize ListViewDelegate::sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const {
755 return QSize(laneWidth(), laneHeight);
758 void ListViewDelegate::diffTargetChanged(int row) {
760 if (diffTargetRow != row) {
761 diffTargetRow = row;
762 emit updateView();
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());
774 if (fhPtr)
775 *fhPtr = fh;
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;
792 x1 += padding;
793 x2 += padding;
795 int h = laneHeight / 2;
796 int m = (x1 + x2) / 2;
797 int r = (x2 - x1) * 1 / 3;
798 int d = 2 * r;
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
818 // arc
819 switch (type) {
820 case JOIN:
821 case JOIN_R:
822 case HEAD:
823 case HEAD_R: {
824 QConicalGradient gradient(CENTER_UR);
825 gradient.setColorAt(0.375, col);
826 gradient.setColorAt(0.625, activeCol);
827 lanePen.setBrush(gradient);
828 p->setPen(lanePen);
829 p->drawArc(P_CENTER, DELTA_UR);
830 break;
832 case JOIN_L: {
833 QConicalGradient gradient(CENTER_UL);
834 gradient.setColorAt(0.375, activeCol);
835 gradient.setColorAt(0.625, col);
836 lanePen.setBrush(gradient);
837 p->setPen(lanePen);
838 p->drawArc(P_CENTER, DELTA_UL);
839 break;
841 case TAIL:
842 case TAIL_R: {
843 QConicalGradient gradient(CENTER_DR);
844 gradient.setColorAt(0.375, activeCol);
845 gradient.setColorAt(0.625, col);
846 lanePen.setBrush(gradient);
847 p->setPen(lanePen);
848 p->drawArc(P_CENTER, DELTA_DR);
849 break;
851 default:
852 break;
855 lanePen.setColor(col);
856 p->setPen(lanePen);
858 // vertical line
859 switch (type) {
860 case ACTIVE:
861 case NOT_ACTIVE:
862 case MERGE_FORK:
863 case MERGE_FORK_R:
864 case MERGE_FORK_L:
865 case JOIN:
866 case JOIN_R:
867 case JOIN_L:
868 case CROSS:
869 p->drawLine(P_90, P_270);
870 break;
871 case HEAD_L:
872 case BRANCH:
873 p->drawLine(P_CENTER, P_270);
874 break;
875 case TAIL_L:
876 case INITIAL:
877 case BOUNDARY:
878 case BOUNDARY_C:
879 case BOUNDARY_R:
880 case BOUNDARY_L:
881 p->drawLine(P_90, P_CENTER);
882 break;
883 default:
884 break;
887 lanePen.setColor(activeCol);
888 p->setPen(lanePen);
890 // horizontal line
891 switch (type) {
892 case MERGE_FORK:
893 case JOIN:
894 case HEAD:
895 case TAIL:
896 case CROSS:
897 case CROSS_EMPTY:
898 case BOUNDARY_C:
899 p->drawLine(P_180, P_0);
900 break;
901 case MERGE_FORK_R:
902 case BOUNDARY_R:
903 p->drawLine(P_180, P_CENTER);
904 break;
905 case MERGE_FORK_L:
906 case HEAD_L:
907 case TAIL_L:
908 case BOUNDARY_L:
909 p->drawLine(P_CENTER, P_0);
910 break;
911 default:
912 break;
915 // center symbol, e.g. rect or ellipse
916 switch (type) {
917 case ACTIVE:
918 case INITIAL:
919 case BRANCH:
920 p->setPen(Qt::black);
921 p->setBrush(col);
922 p->drawEllipse(R_CENTER);
923 break;
924 case MERGE_FORK:
925 case MERGE_FORK_R:
926 case MERGE_FORK_L:
927 p->setPen(Qt::black);
928 p->setBrush(col);
929 p->drawRect(R_CENTER);
930 break;
931 case UNAPPLIED:
932 // Red minus sign
933 p->setPen(Qt::NoPen);
934 p->setBrush(Qt::red);
935 p->drawRect(m - r, h - 1, d, 2);
936 break;
937 case APPLIED:
938 // Green plus sign
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);
943 break;
944 case BOUNDARY:
945 p->setPen(Qt::black);
946 p->setBrush(back);
947 p->drawEllipse(R_CENTER);
948 break;
949 case BOUNDARY_C:
950 case BOUNDARY_R:
951 case BOUNDARY_L:
952 p->setPen(Qt::black);
953 p->setBrush(back);
954 p->drawRect(R_CENTER);
955 break;
956 default:
957 break;
959 #undef P_CENTER
960 #undef P_0
961 #undef P_90
962 #undef P_180
963 #undef P_270
964 #undef DELTA_UR
965 #undef DELTA_DR
966 #undef DELTA_UL
967 #undef DELTA_DL
968 #undef CENTER_UR
969 #undef CENTER_DR
970 #undef CENTER_UL
971 #undef CENTER_DL
972 #undef 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());
985 else
986 p->fillRect(opt.rect, opt.palette.base());
988 FileHistory* fh;
989 const Rev* r = revLookup(i.row(), &fh);
990 if (!r)
991 return;
993 p->save();
994 p->setClipRect(opt.rect, Qt::IntersectClip);
995 p->translate(opt.rect.topLeft());
997 // calculate lanes
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])) {
1007 activeLane = i;
1008 break;
1011 int x1 = 0, x2 = 0;
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++) {
1019 x1 = x2;
1020 x2 += lw;
1022 int ln = lanes[i];
1023 if (ln == EMPTY)
1024 continue;
1026 QColor color = i == activeLane ? activeColor : colors[i % COLORS_NUM];
1027 paintGraphLane(p, ln, x1, x2, color, activeColor, back);
1029 p->restore();
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);
1037 if (!r)
1038 return;
1040 if (r->isDiffCache)
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);
1051 return;
1053 QStyleOptionViewItem newOpt(opt); // we need a copy
1054 if (pm) {
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);
1057 delete pm;
1059 if (isHighlighted)
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);
1082 if (f)
1083 for (int i = 0; i < f->count(); i++)
1084 if (!f->statusCmp(i, RevFile::UNKNOWN))
1085 return true;
1086 return false;
1089 // adapt style and name based on type
1090 void getTagMarkParams(QString &name, QStyleOptionViewItem& o,
1091 const int type, const bool isCurrent) {
1092 QColor clr;
1094 switch (type) {
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);
1110 if (rt == 0)
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);
1122 return pm;
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)
1151 return QString();
1154 void ListViewDelegate::addTextPixmap(QPixmap** pp, SCRef txt, const QStyleOptionViewItem& opt) const {
1156 QPixmap* pm = *pp;
1157 int ofs = pm->isNull() ? 0 : pm->width() + 2;
1158 int spacing = 4;
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);
1169 #else
1170 QPixmap* newPm = new QPixmap(pixmapSize);
1171 #endif
1173 QPainter p;
1174 p.begin(newPm);
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);
1184 p.end();
1186 delete pm;
1187 *pp = newPm;
1190 // *****************************************************************************
1192 ListViewProxy::ListViewProxy(QObject* p, Domain * dm, Git * g) : QSortFilterProxyModel(p) {
1194 d = dm;
1195 git = g;
1196 colNum = 0;
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);
1208 if (!r) {
1209 dbp("ASSERT in ListViewFilter::isMatch, sha <%1> not found", sha);
1210 return false;
1212 QString target;
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)
1220 target = sha;
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()
1230 return false;
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);
1252 colNum = cn;
1253 if (s)
1254 shaSet = *s;
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()){
1264 lv->setModel(fh);
1265 setSourceModel(NULL);
1267 } else if (isOn && !isHighLight) {
1268 setSourceModel(fh); // trigger a rows scanning
1269 lv->setModel(this);
1271 return (sourceModel() ? rowCount() : 0);