Chris@57: /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ Chris@57: Chris@57: /* Chris@57: EasyMercurial Chris@57: Chris@57: Based on HgExplorer by Jari Korhonen Chris@57: Copyright (c) 2010 Jari Korhonen Chris@244: Copyright (c) 2011 Chris Cannam Chris@244: Copyright (c) 2011 Queen Mary, University of London Chris@57: Chris@57: This program is free software; you can redistribute it and/or Chris@57: modify it under the terms of the GNU General Public License as Chris@57: published by the Free Software Foundation; either version 2 of the Chris@57: License, or (at your option) any later version. See the file Chris@57: COPYING included with this distribution for more information. Chris@57: */ Chris@44: Chris@44: #include "grapher.h" Chris@46: #include "connectionitem.h" Chris@53: #include "dateitem.h" Chris@114: #include "debug.h" Chris@119: #include "changesetscene.h" Chris@44: Chris@273: #include Chris@273: Chris@44: #include Chris@44: Chris@273: Grapher::Grapher(ChangesetScene *scene) : Chris@273: m_scene(scene) Chris@273: { Chris@273: QSettings settings; Chris@273: settings.beginGroup("Presentation"); Chris@273: m_showDates = (settings.value("dateformat", 0) == 1); Chris@273: } Chris@273: Chris@268: int Grapher::findAvailableColumn(int row, int parent, bool preferParentCol) cannam@45: { cannam@45: int col = parent; cannam@45: if (preferParentCol) { Chris@145: if (isAvailable(row, col)) return col; cannam@45: } cannam@45: while (col > 0) { Chris@145: if (isAvailable(row, --col)) return col; cannam@45: } cannam@45: while (col < 0) { Chris@145: if (isAvailable(row, ++col)) return col; cannam@45: } cannam@45: col = parent; Chris@268: int sign = (col < 0 ? -1 : 1); cannam@45: while (1) { Chris@106: col += sign; Chris@145: if (isAvailable(row, col)) return col; cannam@45: } cannam@45: } Chris@44: Chris@145: bool Grapher::isAvailable(int row, int col) Chris@145: { Chris@145: if (m_alloc.contains(row) && m_alloc[row].contains(col)) return false; Chris@145: if (!m_haveAllocatedUncommittedColumn) return true; Chris@145: if (!m_uncommitted) return true; Chris@145: return !(row <= m_uncommittedParentRow && col == m_uncommitted->column()); Chris@145: } Chris@145: Chris@106: void Grapher::layoutRow(QString id) Chris@44: { cannam@45: if (m_handled.contains(id)) { Chris@106: return; Chris@44: } Chris@46: if (!m_changesets.contains(id)) { Chris@106: throw LayoutException(QString("Changeset %1 not in ID map").arg(id)); Chris@44: } cannam@45: if (!m_items.contains(id)) { Chris@106: throw LayoutException(QString("Changeset %1 not in item map").arg(id)); Chris@44: } Chris@46: Changeset *cs = m_changesets[id]; cannam@45: ChangesetItem *item = m_items[id]; Chris@114: DEBUG << "layoutRow: Looking at " << id.toStdString() << endl; cannam@45: Chris@44: int row = 0; cannam@45: int nparents = cs->parents().size(); cannam@45: cannam@45: if (nparents > 0) { Chris@106: bool haveRow = false; Chris@106: foreach (QString parentId, cs->parents()) { cannam@45: Chris@106: if (!m_changesets.contains(parentId)) continue; Chris@106: if (!m_items.contains(parentId)) continue; cannam@45: Chris@106: if (!m_handled.contains(parentId)) { Chris@106: layoutRow(parentId); Chris@106: } cannam@45: Chris@106: ChangesetItem *parentItem = m_items[parentId]; Chris@106: if (!haveRow || parentItem->row() < row) { Chris@106: row = parentItem->row(); Chris@106: haveRow = true; Chris@106: } Chris@106: } Chris@106: row = row - 1; cannam@45: } cannam@45: Chris@52: // row is now an upper bound on our eventual row (because we want Chris@52: // to be above all parents). But we also want to ensure we are Chris@52: // above all nodes that have earlier dates (to the nearest day). Chris@52: // m_rowDates maps each row to a date: use that. Chris@52: Chris@273: QString date; Chris@273: if (m_showDates) { Chris@273: date = cs->date(); Chris@273: } else { Chris@273: date = cs->age(); Chris@273: } Chris@53: while (m_rowDates.contains(row) && m_rowDates[row] != date) { Chris@106: --row; Chris@52: } Chris@52: Chris@52: // We have already laid out all nodes that have earlier timestamps Chris@52: // than this one, so we know (among other things) that we can Chris@52: // safely fill in this row has having this date, if it isn't in Chris@52: // the map yet (it cannot have an earlier date) Chris@52: Chris@52: if (!m_rowDates.contains(row)) { Chris@106: m_rowDates[row] = date; Chris@52: } Chris@52: Chris@145: // If we're the parent of the uncommitted item, make a note of our Chris@145: // row (we need it later, to avoid overwriting the connecting line) Chris@153: if (!m_uncommittedParents.empty() && m_uncommittedParents[0] == id) { Chris@145: m_uncommittedParentRow = row; Chris@145: } Chris@145: Chris@114: DEBUG << "putting " << cs->id().toStdString() << " at row " << row Chris@114: << endl; cannam@45: Chris@44: item->setRow(row); cannam@45: m_handled.insert(id); Chris@44: } Chris@44: Chris@106: void Grapher::layoutCol(QString id) Chris@44: { cannam@45: if (m_handled.contains(id)) { Chris@114: DEBUG << "Already looked at " << id.toStdString() << endl; Chris@106: return; cannam@45: } Chris@46: if (!m_changesets.contains(id)) { Chris@106: throw LayoutException(QString("Changeset %1 not in ID map").arg(id)); cannam@45: } cannam@45: if (!m_items.contains(id)) { Chris@106: throw LayoutException(QString("Changeset %1 not in item map").arg(id)); cannam@45: } Chris@53: Chris@46: Changeset *cs = m_changesets[id]; Chris@122: DEBUG << "layoutCol: Looking at " << id.toStdString() << endl; Chris@53: cannam@45: ChangesetItem *item = m_items[id]; Chris@47: cannam@45: int col = 0; Chris@46: int row = item->row(); Chris@46: QString branch = cs->branch(); Chris@47: cannam@45: int nparents = cs->parents().size(); Chris@46: QString parentId; Chris@46: int parentsOnSameBranch = 0; cannam@45: Chris@46: switch (nparents) { cannam@45: Chris@46: case 0: Chris@268: col = m_branchHomes[cs->branch()]; Chris@268: col = findAvailableColumn(row, col, true); Chris@106: break; cannam@45: Chris@46: case 1: Chris@106: parentId = cs->parents()[0]; cannam@45: Chris@106: if (!m_changesets.contains(parentId) || Chris@153: !m_changesets[parentId]->isOnBranch(branch)) { Chris@106: // new branch Chris@106: col = m_branchHomes[branch]; Chris@106: } else { Chris@106: col = m_items[parentId]->column(); Chris@106: } cannam@45: Chris@268: col = findAvailableColumn(row, col, true); Chris@106: break; Chris@46: Chris@46: case 2: Chris@106: // a merge: behave differently if parents are both on the same Chris@106: // branch (we also want to behave differently for nodes that Chris@106: // have multiple children on the same branch -- spreading them Chris@106: // out rather than having affinity to a specific branch) Chris@46: Chris@106: foreach (QString parentId, cs->parents()) { Chris@106: if (!m_changesets.contains(parentId)) continue; Chris@153: if (m_changesets[parentId]->isOnBranch(branch)) { Chris@106: ChangesetItem *parentItem = m_items[parentId]; Chris@106: col += parentItem->column(); Chris@106: parentsOnSameBranch++; Chris@106: } Chris@106: } Chris@46: Chris@106: if (parentsOnSameBranch > 0) { Chris@106: col /= parentsOnSameBranch; Chris@268: col = findAvailableColumn(item->row(), col, true); Chris@106: } else { Chris@268: col = findAvailableColumn(item->row(), col, false); Chris@106: } Chris@106: break; Chris@44: } Chris@44: Chris@122: DEBUG << "putting " << cs->id().toStdString() << " at col " << col << endl; cannam@45: Chris@46: m_alloc[row].insert(col); cannam@45: item->setColumn(col); cannam@45: m_handled.insert(id); Chris@47: Chris@153: // If we're the first parent of the uncommitted item, it should be Chris@153: // given the same column as us (we already noted that its Chris@153: // connecting line would end at our row) Chris@145: Chris@153: if (m_uncommittedParents.contains(id)) { Chris@153: if (m_uncommittedParents[0] == id) { Chris@268: int ucol = findAvailableColumn(row-1, col, true); Chris@153: m_uncommitted->setColumn(ucol); Chris@153: m_haveAllocatedUncommittedColumn = true; Chris@153: } Chris@153: // also, if the uncommitted item has a different branch from Chris@153: // any of its parents, tell it to show the branch Chris@153: if (!cs->isOnBranch(m_uncommitted->branch())) { Chris@153: DEBUG << "Uncommitted branch " << m_uncommitted->branch() Chris@153: << " differs from my branch " << cs->branch() Chris@153: << ", asking it to show branch" << endl; Chris@153: m_uncommitted->setShowBranch(true); Chris@153: } Chris@145: } Chris@145: Chris@311: Chris@51: // Normally the children will lay out themselves, but we can do Chris@51: // a better job in some special cases: Chris@51: Chris@47: int nchildren = cs->children().size(); Chris@49: Chris@53: // look for merging children and children distant from us but in a Chris@53: // straight line, and make sure nobody is going to overwrite their Chris@53: // connection lines Chris@51: Chris@49: foreach (QString childId, cs->children()) { Chris@139: DEBUG << "reserving connection line space" << endl; Chris@49: if (!m_changesets.contains(childId)) continue; Chris@49: Changeset *child = m_changesets[childId]; Chris@106: int childRow = m_items[childId]->row(); Chris@55: if (child->parents().size() > 1 || Chris@153: child->isOnBranch(cs->branch())) { Chris@55: for (int r = row-1; r > childRow; --r) { Chris@49: m_alloc[r].insert(col); Chris@49: } Chris@106: } Chris@49: } Chris@49: Chris@51: // look for the case where exactly two children have the same Chris@51: // branch as us: split them to a little either side of our position Chris@51: Chris@47: if (nchildren > 1) { Chris@106: QList special; Chris@106: foreach (QString childId, cs->children()) { Chris@106: if (!m_changesets.contains(childId)) continue; Chris@106: Changeset *child = m_changesets[childId]; Chris@153: if (child->isOnBranch(branch) && Chris@106: child->parents().size() == 1) { Chris@106: special.push_back(childId); Chris@106: } Chris@106: } Chris@106: if (special.size() == 2) { Chris@139: DEBUG << "handling split-in-two for children " << special[0] << " and " << special[1] << endl; Chris@106: for (int i = 0; i < 2; ++i) { Chris@106: int off = i * 2 - 1; // 0 -> -1, 1 -> 1 Chris@106: ChangesetItem *it = m_items[special[i]]; Chris@268: it->setColumn(findAvailableColumn(it->row(), col + off, true)); Chris@106: for (int r = row-1; r >= it->row(); --r) { Chris@106: m_alloc[r].insert(it->column()); Chris@106: } Chris@106: m_handled.insert(special[i]); Chris@106: } Chris@106: } Chris@47: } cannam@45: } cannam@45: Chris@106: bool Grapher::rangesConflict(const Range &r1, const Range &r2) Chris@46: { Chris@46: // allow some additional space at edges. we really ought also to Chris@46: // permit space at the end of a branch up to the point where the Chris@46: // merge happens Chris@46: int a1 = r1.first - 2, b1 = r1.second + 2; Chris@46: int a2 = r2.first - 2, b2 = r2.second + 2; Chris@46: if (a1 > b2 || b1 < a2) return false; Chris@46: if (a2 > b1 || b2 < a1) return false; Chris@46: return true; Chris@46: } Chris@46: Chris@106: void Grapher::allocateBranchHomes(Changesets csets) Chris@46: { Chris@46: foreach (Changeset *cs, csets) { Chris@106: QString branch = cs->branch(); Chris@106: ChangesetItem *item = m_items[cs->id()]; Chris@106: if (!item) continue; Chris@106: int row = item->row(); Chris@106: if (!m_branchRanges.contains(branch)) { Chris@106: m_branchRanges[branch] = Range(row, row); Chris@106: } else { Chris@106: Range p = m_branchRanges[branch]; Chris@106: if (row < p.first) p.first = row; Chris@106: if (row > p.second) p.second = row; Chris@106: m_branchRanges[branch] = p; Chris@106: } Chris@46: } Chris@46: Chris@46: m_branchHomes[""] = 0; Chris@153: m_branchHomes["default"] = 0; Chris@46: Chris@268: foreach (QString branch, m_branchRanges.keys()) { Chris@106: if (branch == "") continue; Chris@106: QSet taken; Chris@106: taken.insert(0); Chris@106: Range myRange = m_branchRanges[branch]; Chris@106: foreach (QString other, m_branchRanges.keys()) { Chris@106: if (other == branch || other == "") continue; Chris@106: Range otherRange = m_branchRanges[other]; Chris@106: if (rangesConflict(myRange, otherRange)) { Chris@106: if (m_branchHomes.contains(other)) { Chris@106: taken.insert(m_branchHomes[other]); Chris@106: } Chris@106: } Chris@106: } Chris@106: int home = 2; Chris@106: while (taken.contains(home)) { Chris@268: if (home > 0) { Chris@268: if (home % 2 == 1) { Chris@268: home = -home; Chris@268: } else { Chris@268: home = home + 1; Chris@268: } Chris@268: } else { Chris@268: if ((-home) % 2 == 1) { Chris@268: home = home + 1; Chris@268: } else { Chris@268: home = -(home-2); Chris@268: } Chris@268: } Chris@106: } Chris@106: m_branchHomes[branch] = home; Chris@46: } Chris@46: Chris@268: foreach (QString branch, m_branchRanges.keys()) { Chris@268: DEBUG << branch.toStdString() << ": " << m_branchRanges[branch].first << " - " << m_branchRanges[branch].second << ", home " << m_branchHomes[branch] << endl; Chris@46: } Chris@46: } Chris@46: Chris@52: static bool Chris@145: compareChangesetsByDate(Changeset *const &a, Changeset *const &b) Chris@52: { Chris@52: return a->timestamp() < b->timestamp(); Chris@52: } Chris@52: Chris@53: ChangesetItem * Chris@145: Grapher::getItemFor(Changeset *cs) Chris@53: { Chris@53: if (!cs || !m_items.contains(cs->id())) return 0; Chris@53: return m_items[cs->id()]; Chris@53: } Chris@53: Chris@153: void Grapher::layout(Changesets csets, Chris@153: QStringList uncommittedParents, Chris@153: QString uncommittedBranch) cannam@45: { Chris@46: m_changesets.clear(); cannam@45: m_items.clear(); cannam@45: m_alloc.clear(); Chris@46: m_branchHomes.clear(); cannam@45: Chris@153: m_uncommittedParents = uncommittedParents; Chris@145: m_haveAllocatedUncommittedColumn = false; Chris@145: m_uncommittedParentRow = 0; Chris@145: m_uncommitted = 0; Chris@145: Chris@139: DEBUG << "Grapher::layout: Have " << csets.size() << " changesets" << endl; Chris@139: Chris@53: if (csets.empty()) return; Chris@53: Chris@145: // Create (but don't yet position) the changeset items Chris@145: Chris@44: foreach (Changeset *cs, csets) { cannam@45: Chris@106: QString id = cs->id(); cannam@45: Chris@106: if (id == "") { Chris@106: throw LayoutException("Changeset has no ID"); Chris@106: } Chris@106: if (m_changesets.contains(id)) { Chris@145: DEBUG << "Duplicate changeset ID " << id Chris@145: << " in Grapher::layout()" << endl; Chris@106: throw LayoutException(QString("Duplicate changeset ID %1").arg(id)); Chris@106: } cannam@45: Chris@106: m_changesets[id] = cs; cannam@45: cannam@45: ChangesetItem *item = new ChangesetItem(cs); cannam@45: item->setX(0); cannam@45: item->setY(0); Chris@250: item->setZValue(0); Chris@106: m_items[id] = item; Chris@141: m_scene->addChangesetItem(item); cannam@45: } cannam@45: Chris@74: // Add the connecting lines Chris@74: Chris@46: foreach (Changeset *cs, csets) { Chris@106: QString id = cs->id(); Chris@106: ChangesetItem *item = m_items[id]; Chris@106: bool merge = (cs->parents().size() > 1); Chris@106: foreach (QString parentId, cs->parents()) { Chris@106: if (!m_changesets.contains(parentId)) continue; Chris@106: Changeset *parent = m_changesets[parentId]; Chris@106: parent->addChild(id); Chris@106: ConnectionItem *conn = new ConnectionItem(); Chris@106: if (merge) conn->setConnectionType(ConnectionItem::Merge); Chris@106: conn->setChild(item); Chris@106: conn->setParent(m_items[parentId]); Chris@250: conn->setZValue(-1); Chris@106: m_scene->addItem(conn); Chris@106: } Chris@46: } Chris@145: Chris@145: // Add uncommitted item and connecting line as necessary Chris@145: Chris@153: if (!m_uncommittedParents.empty()) { Chris@311: Chris@145: m_uncommitted = new UncommittedItem(); Chris@153: m_uncommitted->setBranch(uncommittedBranch); Chris@250: m_uncommitted->setZValue(10); Chris@149: m_scene->addUncommittedItem(m_uncommitted); Chris@311: Chris@311: bool haveParentOnBranch = false; Chris@153: foreach (QString p, m_uncommittedParents) { Chris@153: ConnectionItem *conn = new ConnectionItem(); Chris@153: conn->setConnectionType(ConnectionItem::Merge); Chris@311: ChangesetItem *pitem = m_items[p]; Chris@311: conn->setParent(pitem); Chris@153: conn->setChild(m_uncommitted); Chris@250: conn->setZValue(0); Chris@153: m_scene->addItem(conn); Chris@311: if (pitem) { Chris@311: if (pitem->getChangeset()->branch() == uncommittedBranch) { Chris@311: haveParentOnBranch = true; Chris@311: } Chris@311: } Chris@153: } Chris@311: Chris@311: // If the uncommitted item has no parents on the same branch, Chris@311: // tell it it has a new branch (the "show branch" flag is set Chris@311: // elsewhere for this item) Chris@311: m_uncommitted->setIsNewBranch(!haveParentOnBranch); Chris@145: } Chris@46: Chris@74: // Add the branch labels Chris@145: Chris@74: foreach (Changeset *cs, csets) { Chris@74: QString id = cs->id(); Chris@74: ChangesetItem *item = m_items[id]; Chris@74: bool haveChildOnSameBranch = false; Chris@74: foreach (QString childId, cs->children()) { Chris@74: Changeset *child = m_changesets[childId]; Chris@74: if (child->branch() == cs->branch()) { Chris@74: haveChildOnSameBranch = true; Chris@74: break; Chris@74: } Chris@74: } Chris@74: item->setShowBranch(!haveChildOnSameBranch); Chris@74: } Chris@74: Chris@52: // We need to lay out the changesets in forward chronological Chris@52: // order. We have no guarantees about the order in which Chris@52: // changesets appear in the list -- in a simple repository they Chris@52: // will generally be reverse chronological, but that's far from Chris@52: // guaranteed. So, sort explicitly using the date comparator Chris@52: // above Chris@51: Chris@52: qStableSort(csets.begin(), csets.end(), compareChangesetsByDate); Chris@51: Chris@53: foreach (Changeset *cs, csets) { Chris@145: DEBUG << "id " << cs->id().toStdString() << ", ts " << cs->timestamp() Chris@145: << ", date " << cs->datetime().toStdString() << endl; Chris@53: } Chris@53: cannam@45: m_handled.clear(); Chris@53: foreach (Changeset *cs, csets) { Chris@106: layoutRow(cs->id()); cannam@45: } Chris@46: Chris@46: allocateBranchHomes(csets); Chris@46: cannam@45: m_handled.clear(); Chris@53: foreach (Changeset *cs, csets) { Chris@106: foreach (QString parentId, cs->parents()) { Chris@106: if (!m_handled.contains(parentId) && Chris@106: m_changesets.contains(parentId)) { Chris@106: layoutCol(parentId); Chris@106: } Chris@106: } Chris@106: layoutCol(cs->id()); Chris@53: } Chris@53: Chris@145: // Find row and column extents. We know that 0 is an upper bound Chris@145: // on row, and that mincol must be <= 0 and maxcol >= 0, so these Chris@145: // initial values are good Chris@55: Chris@53: int minrow = 0, maxrow = 0; Chris@53: int mincol = 0, maxcol = 0; Chris@53: Chris@53: foreach (int r, m_alloc.keys()) { Chris@106: if (r < minrow) minrow = r; Chris@106: if (r > maxrow) maxrow = r; Chris@106: ColumnSet &c = m_alloc[r]; Chris@106: foreach (int i, c) { Chris@106: if (i < mincol) mincol = i; Chris@106: if (i > maxcol) maxcol = i; Chris@106: } Chris@53: } Chris@53: Chris@153: int datemincol = mincol, datemaxcol = maxcol; Chris@153: Chris@153: if (mincol == maxcol) { Chris@153: --datemincol; Chris@153: ++datemaxcol; Chris@153: } else if (m_alloc[minrow].contains(mincol)) { Chris@153: --datemincol; Chris@153: } Chris@153: Chris@145: // We've given the uncommitted item a column, but not a row yet -- Chris@145: // it always goes at the top Chris@145: Chris@145: if (m_uncommitted) { Chris@145: --minrow; Chris@145: DEBUG << "putting uncommitted item at row " << minrow << endl; Chris@145: m_uncommitted->setRow(minrow); Chris@145: } Chris@145: Chris@145: // Changeset items that have nothing to either side of them can be Chris@145: // made double-width Chris@145: Chris@145: foreach (Changeset *cs, csets) { Chris@145: ChangesetItem *item = m_items[cs->id()]; Chris@145: if (isAvailable(item->row(), item->column()-1) && Chris@145: isAvailable(item->row(), item->column()+1)) { Chris@145: item->setWide(true); Chris@145: } Chris@145: } Chris@145: Chris@145: if (m_uncommitted) { Chris@145: if (isAvailable(m_uncommitted->row(), m_uncommitted->column()-1) && Chris@145: isAvailable(m_uncommitted->row(), m_uncommitted->column()+1)) { Chris@145: m_uncommitted->setWide(true); Chris@145: } Chris@145: } Chris@145: Chris@53: QString prevDate; Chris@53: int changeRow = 0; Chris@53: Chris@53: bool even = false; Chris@53: int n = 0; Chris@53: Chris@53: for (int row = minrow; row <= maxrow; ++row) { Chris@53: Chris@106: QString date = m_rowDates[row]; Chris@106: n++; Chris@106: Chris@106: if (date != prevDate) { Chris@106: if (prevDate != "") { Chris@106: DateItem *item = new DateItem(); Chris@106: item->setDateString(prevDate); Chris@153: item->setCols(datemincol, datemaxcol - datemincol + 1); Chris@106: item->setRows(changeRow, n); Chris@106: item->setEven(even); Chris@250: item->setZValue(-2); Chris@168: m_scene->addDateItem(item); Chris@106: even = !even; Chris@106: } Chris@106: prevDate = date; Chris@106: changeRow = row; Chris@106: n = 0; Chris@106: } Chris@53: } Chris@53: Chris@53: if (n > 0) { Chris@106: DateItem *item = new DateItem(); Chris@106: item->setDateString(prevDate); Chris@153: item->setCols(datemincol, datemaxcol - datemincol + 1); Chris@106: item->setRows(changeRow, n+1); Chris@106: item->setEven(even); Chris@250: item->setZValue(-2); Chris@168: m_scene->addDateItem(item); Chris@106: even = !even; cannam@45: } Chris@44: } Chris@44: