Mercurial > hg > easyhg
comparison src/grapher.cpp @ 370:b9c153e00e84
Move source files to src/
author | Chris Cannam |
---|---|
date | Thu, 24 Mar 2011 10:27:51 +0000 |
parents | grapher.cpp@4811eb34e819 |
children | f051d210521e |
comparison
equal
deleted
inserted
replaced
369:19cce6d2c470 | 370:b9c153e00e84 |
---|---|
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ | |
2 | |
3 /* | |
4 EasyMercurial | |
5 | |
6 Based on HgExplorer by Jari Korhonen | |
7 Copyright (c) 2010 Jari Korhonen | |
8 Copyright (c) 2011 Chris Cannam | |
9 Copyright (c) 2011 Queen Mary, University of London | |
10 | |
11 This program is free software; you can redistribute it and/or | |
12 modify it under the terms of the GNU General Public License as | |
13 published by the Free Software Foundation; either version 2 of the | |
14 License, or (at your option) any later version. See the file | |
15 COPYING included with this distribution for more information. | |
16 */ | |
17 | |
18 #include "grapher.h" | |
19 #include "connectionitem.h" | |
20 #include "dateitem.h" | |
21 #include "debug.h" | |
22 #include "changesetscene.h" | |
23 | |
24 #include <QSettings> | |
25 | |
26 #include <iostream> | |
27 | |
28 Grapher::Grapher(ChangesetScene *scene) : | |
29 m_scene(scene) | |
30 { | |
31 QSettings settings; | |
32 settings.beginGroup("Presentation"); | |
33 m_showDates = (settings.value("dateformat", 0) == 1); | |
34 } | |
35 | |
36 int Grapher::findAvailableColumn(int row, int parent, bool preferParentCol) | |
37 { | |
38 int col = parent; | |
39 if (preferParentCol) { | |
40 if (isAvailable(row, col)) return col; | |
41 } | |
42 while (col > 0) { | |
43 if (isAvailable(row, --col)) return col; | |
44 } | |
45 while (col < 0) { | |
46 if (isAvailable(row, ++col)) return col; | |
47 } | |
48 col = parent; | |
49 int sign = (col < 0 ? -1 : 1); | |
50 while (1) { | |
51 col += sign; | |
52 if (isAvailable(row, col)) return col; | |
53 } | |
54 } | |
55 | |
56 bool Grapher::isAvailable(int row, int col) | |
57 { | |
58 if (m_alloc.contains(row) && m_alloc[row].contains(col)) return false; | |
59 if (!m_haveAllocatedUncommittedColumn) return true; | |
60 if (!m_uncommitted) return true; | |
61 return !(row <= m_uncommittedParentRow && col == m_uncommitted->column()); | |
62 } | |
63 | |
64 void Grapher::layoutRow(QString id) | |
65 { | |
66 if (m_handled.contains(id)) { | |
67 return; | |
68 } | |
69 if (!m_changesets.contains(id)) { | |
70 throw LayoutException(QString("Changeset %1 not in ID map").arg(id)); | |
71 } | |
72 if (!m_items.contains(id)) { | |
73 throw LayoutException(QString("Changeset %1 not in item map").arg(id)); | |
74 } | |
75 Changeset *cs = m_changesets[id]; | |
76 ChangesetItem *item = m_items[id]; | |
77 DEBUG << "layoutRow: Looking at " << id.toStdString() << endl; | |
78 | |
79 int row = 0; | |
80 int nparents = cs->parents().size(); | |
81 | |
82 if (nparents > 0) { | |
83 bool haveRow = false; | |
84 foreach (QString parentId, cs->parents()) { | |
85 | |
86 if (!m_changesets.contains(parentId)) continue; | |
87 if (!m_items.contains(parentId)) continue; | |
88 | |
89 if (!m_handled.contains(parentId)) { | |
90 layoutRow(parentId); | |
91 } | |
92 | |
93 ChangesetItem *parentItem = m_items[parentId]; | |
94 if (!haveRow || parentItem->row() < row) { | |
95 row = parentItem->row(); | |
96 haveRow = true; | |
97 } | |
98 } | |
99 row = row - 1; | |
100 } | |
101 | |
102 // row is now an upper bound on our eventual row (because we want | |
103 // to be above all parents). But we also want to ensure we are | |
104 // above all nodes that have earlier dates (to the nearest day). | |
105 // m_rowDates maps each row to a date: use that. | |
106 | |
107 QString date; | |
108 if (m_showDates) { | |
109 date = cs->date(); | |
110 } else { | |
111 date = cs->age(); | |
112 } | |
113 while (m_rowDates.contains(row) && m_rowDates[row] != date) { | |
114 --row; | |
115 } | |
116 | |
117 // We have already laid out all nodes that have earlier timestamps | |
118 // than this one, so we know (among other things) that we can | |
119 // safely fill in this row has having this date, if it isn't in | |
120 // the map yet (it cannot have an earlier date) | |
121 | |
122 if (!m_rowDates.contains(row)) { | |
123 m_rowDates[row] = date; | |
124 } | |
125 | |
126 // If we're the parent of the uncommitted item, make a note of our | |
127 // row (we need it later, to avoid overwriting the connecting line) | |
128 if (!m_uncommittedParents.empty() && m_uncommittedParents[0] == id) { | |
129 m_uncommittedParentRow = row; | |
130 } | |
131 | |
132 DEBUG << "putting " << cs->id().toStdString() << " at row " << row | |
133 << endl; | |
134 | |
135 item->setRow(row); | |
136 m_handled.insert(id); | |
137 } | |
138 | |
139 void Grapher::layoutCol(QString id) | |
140 { | |
141 if (m_handled.contains(id)) { | |
142 DEBUG << "Already looked at " << id.toStdString() << endl; | |
143 return; | |
144 } | |
145 if (!m_changesets.contains(id)) { | |
146 throw LayoutException(QString("Changeset %1 not in ID map").arg(id)); | |
147 } | |
148 if (!m_items.contains(id)) { | |
149 throw LayoutException(QString("Changeset %1 not in item map").arg(id)); | |
150 } | |
151 | |
152 Changeset *cs = m_changesets[id]; | |
153 DEBUG << "layoutCol: Looking at " << id.toStdString() << endl; | |
154 | |
155 ChangesetItem *item = m_items[id]; | |
156 | |
157 int col = 0; | |
158 int row = item->row(); | |
159 QString branch = cs->branch(); | |
160 | |
161 int nparents = cs->parents().size(); | |
162 QString parentId; | |
163 int parentsOnSameBranch = 0; | |
164 | |
165 switch (nparents) { | |
166 | |
167 case 0: | |
168 col = m_branchHomes[cs->branch()]; | |
169 col = findAvailableColumn(row, col, true); | |
170 break; | |
171 | |
172 case 1: | |
173 parentId = cs->parents()[0]; | |
174 | |
175 if (!m_changesets.contains(parentId) || | |
176 !m_changesets[parentId]->isOnBranch(branch)) { | |
177 // new branch | |
178 col = m_branchHomes[branch]; | |
179 } else { | |
180 col = m_items[parentId]->column(); | |
181 } | |
182 | |
183 col = findAvailableColumn(row, col, true); | |
184 break; | |
185 | |
186 case 2: | |
187 // a merge: behave differently if parents are both on the same | |
188 // branch (we also want to behave differently for nodes that | |
189 // have multiple children on the same branch -- spreading them | |
190 // out rather than having affinity to a specific branch) | |
191 | |
192 foreach (QString parentId, cs->parents()) { | |
193 if (!m_changesets.contains(parentId)) continue; | |
194 if (m_changesets[parentId]->isOnBranch(branch)) { | |
195 ChangesetItem *parentItem = m_items[parentId]; | |
196 col += parentItem->column(); | |
197 parentsOnSameBranch++; | |
198 } | |
199 } | |
200 | |
201 if (parentsOnSameBranch > 0) { | |
202 col /= parentsOnSameBranch; | |
203 col = findAvailableColumn(item->row(), col, true); | |
204 } else { | |
205 col = findAvailableColumn(item->row(), col, false); | |
206 } | |
207 break; | |
208 } | |
209 | |
210 DEBUG << "putting " << cs->id().toStdString() << " at col " << col << endl; | |
211 | |
212 m_alloc[row].insert(col); | |
213 item->setColumn(col); | |
214 m_handled.insert(id); | |
215 | |
216 // If we're the first parent of the uncommitted item, it should be | |
217 // given the same column as us (we already noted that its | |
218 // connecting line would end at our row) | |
219 | |
220 if (m_uncommittedParents.contains(id)) { | |
221 if (m_uncommittedParents[0] == id) { | |
222 int ucol = findAvailableColumn(row-1, col, true); | |
223 m_uncommitted->setColumn(ucol); | |
224 m_haveAllocatedUncommittedColumn = true; | |
225 } | |
226 // also, if the uncommitted item has a different branch from | |
227 // any of its parents, tell it to show the branch | |
228 if (!cs->isOnBranch(m_uncommitted->branch())) { | |
229 DEBUG << "Uncommitted branch " << m_uncommitted->branch() | |
230 << " differs from my branch " << cs->branch() | |
231 << ", asking it to show branch" << endl; | |
232 m_uncommitted->setShowBranch(true); | |
233 } | |
234 } | |
235 | |
236 | |
237 // Normally the children will lay out themselves, but we can do | |
238 // a better job in some special cases: | |
239 | |
240 int nchildren = cs->children().size(); | |
241 | |
242 // look for merging children and children distant from us but in a | |
243 // straight line, and make sure nobody is going to overwrite their | |
244 // connection lines | |
245 | |
246 foreach (QString childId, cs->children()) { | |
247 DEBUG << "reserving connection line space" << endl; | |
248 if (!m_changesets.contains(childId)) continue; | |
249 Changeset *child = m_changesets[childId]; | |
250 int childRow = m_items[childId]->row(); | |
251 if (child->parents().size() > 1 || | |
252 child->isOnBranch(cs->branch())) { | |
253 for (int r = row-1; r > childRow; --r) { | |
254 m_alloc[r].insert(col); | |
255 } | |
256 } | |
257 } | |
258 | |
259 // look for the case where exactly two children have the same | |
260 // branch as us: split them to a little either side of our position | |
261 | |
262 if (nchildren > 1) { | |
263 QList<QString> special; | |
264 foreach (QString childId, cs->children()) { | |
265 if (!m_changesets.contains(childId)) continue; | |
266 Changeset *child = m_changesets[childId]; | |
267 if (child->isOnBranch(branch) && | |
268 child->parents().size() == 1) { | |
269 special.push_back(childId); | |
270 } | |
271 } | |
272 if (special.size() == 2) { | |
273 DEBUG << "handling split-in-two for children " << special[0] << " and " << special[1] << endl; | |
274 for (int i = 0; i < 2; ++i) { | |
275 int off = i * 2 - 1; // 0 -> -1, 1 -> 1 | |
276 ChangesetItem *it = m_items[special[i]]; | |
277 it->setColumn(findAvailableColumn(it->row(), col + off, true)); | |
278 for (int r = row-1; r >= it->row(); --r) { | |
279 m_alloc[r].insert(it->column()); | |
280 } | |
281 m_handled.insert(special[i]); | |
282 } | |
283 } | |
284 } | |
285 } | |
286 | |
287 bool Grapher::rangesConflict(const Range &r1, const Range &r2) | |
288 { | |
289 // allow some additional space at edges. we really ought also to | |
290 // permit space at the end of a branch up to the point where the | |
291 // merge happens | |
292 int a1 = r1.first - 2, b1 = r1.second + 2; | |
293 int a2 = r2.first - 2, b2 = r2.second + 2; | |
294 if (a1 > b2 || b1 < a2) return false; | |
295 if (a2 > b1 || b2 < a1) return false; | |
296 return true; | |
297 } | |
298 | |
299 void Grapher::allocateBranchHomes(Changesets csets) | |
300 { | |
301 foreach (Changeset *cs, csets) { | |
302 QString branch = cs->branch(); | |
303 ChangesetItem *item = m_items[cs->id()]; | |
304 if (!item) continue; | |
305 int row = item->row(); | |
306 if (!m_branchRanges.contains(branch)) { | |
307 m_branchRanges[branch] = Range(row, row); | |
308 } else { | |
309 Range p = m_branchRanges[branch]; | |
310 if (row < p.first) p.first = row; | |
311 if (row > p.second) p.second = row; | |
312 m_branchRanges[branch] = p; | |
313 } | |
314 } | |
315 | |
316 m_branchHomes[""] = 0; | |
317 m_branchHomes["default"] = 0; | |
318 | |
319 foreach (QString branch, m_branchRanges.keys()) { | |
320 if (branch == "") continue; | |
321 QSet<int> taken; | |
322 taken.insert(0); | |
323 Range myRange = m_branchRanges[branch]; | |
324 foreach (QString other, m_branchRanges.keys()) { | |
325 if (other == branch || other == "") continue; | |
326 Range otherRange = m_branchRanges[other]; | |
327 if (rangesConflict(myRange, otherRange)) { | |
328 if (m_branchHomes.contains(other)) { | |
329 taken.insert(m_branchHomes[other]); | |
330 } | |
331 } | |
332 } | |
333 int home = 2; | |
334 while (taken.contains(home)) { | |
335 if (home > 0) { | |
336 if (home % 2 == 1) { | |
337 home = -home; | |
338 } else { | |
339 home = home + 1; | |
340 } | |
341 } else { | |
342 if ((-home) % 2 == 1) { | |
343 home = home + 1; | |
344 } else { | |
345 home = -(home-2); | |
346 } | |
347 } | |
348 } | |
349 m_branchHomes[branch] = home; | |
350 } | |
351 | |
352 foreach (QString branch, m_branchRanges.keys()) { | |
353 DEBUG << branch.toStdString() << ": " << m_branchRanges[branch].first << " - " << m_branchRanges[branch].second << ", home " << m_branchHomes[branch] << endl; | |
354 } | |
355 } | |
356 | |
357 static bool | |
358 compareChangesetsByDate(Changeset *const &a, Changeset *const &b) | |
359 { | |
360 return a->timestamp() < b->timestamp(); | |
361 } | |
362 | |
363 ChangesetItem * | |
364 Grapher::getItemFor(Changeset *cs) | |
365 { | |
366 if (!cs || !m_items.contains(cs->id())) return 0; | |
367 return m_items[cs->id()]; | |
368 } | |
369 | |
370 void Grapher::layout(Changesets csets, | |
371 QStringList uncommittedParents, | |
372 QString uncommittedBranch) | |
373 { | |
374 m_changesets.clear(); | |
375 m_items.clear(); | |
376 m_alloc.clear(); | |
377 m_branchHomes.clear(); | |
378 | |
379 m_uncommittedParents = uncommittedParents; | |
380 m_haveAllocatedUncommittedColumn = false; | |
381 m_uncommittedParentRow = 0; | |
382 m_uncommitted = 0; | |
383 | |
384 DEBUG << "Grapher::layout: Have " << csets.size() << " changesets" << endl; | |
385 | |
386 if (csets.empty()) return; | |
387 | |
388 // Create (but don't yet position) the changeset items | |
389 | |
390 foreach (Changeset *cs, csets) { | |
391 | |
392 QString id = cs->id(); | |
393 | |
394 if (id == "") { | |
395 throw LayoutException("Changeset has no ID"); | |
396 } | |
397 if (m_changesets.contains(id)) { | |
398 DEBUG << "Duplicate changeset ID " << id | |
399 << " in Grapher::layout()" << endl; | |
400 throw LayoutException(QString("Duplicate changeset ID %1").arg(id)); | |
401 } | |
402 | |
403 m_changesets[id] = cs; | |
404 | |
405 ChangesetItem *item = new ChangesetItem(cs); | |
406 item->setX(0); | |
407 item->setY(0); | |
408 item->setZValue(0); | |
409 m_items[id] = item; | |
410 m_scene->addChangesetItem(item); | |
411 } | |
412 | |
413 // Add the connecting lines | |
414 | |
415 foreach (Changeset *cs, csets) { | |
416 QString id = cs->id(); | |
417 ChangesetItem *item = m_items[id]; | |
418 bool merge = (cs->parents().size() > 1); | |
419 foreach (QString parentId, cs->parents()) { | |
420 if (!m_changesets.contains(parentId)) continue; | |
421 Changeset *parent = m_changesets[parentId]; | |
422 parent->addChild(id); | |
423 ConnectionItem *conn = new ConnectionItem(); | |
424 if (merge) conn->setConnectionType(ConnectionItem::Merge); | |
425 conn->setChild(item); | |
426 conn->setParent(m_items[parentId]); | |
427 conn->setZValue(-1); | |
428 m_scene->addItem(conn); | |
429 } | |
430 } | |
431 | |
432 // Add uncommitted item and connecting line as necessary | |
433 | |
434 if (!m_uncommittedParents.empty()) { | |
435 | |
436 m_uncommitted = new UncommittedItem(); | |
437 m_uncommitted->setBranch(uncommittedBranch); | |
438 m_uncommitted->setZValue(10); | |
439 m_scene->addUncommittedItem(m_uncommitted); | |
440 | |
441 bool haveParentOnBranch = false; | |
442 foreach (QString p, m_uncommittedParents) { | |
443 ConnectionItem *conn = new ConnectionItem(); | |
444 conn->setConnectionType(ConnectionItem::Merge); | |
445 ChangesetItem *pitem = m_items[p]; | |
446 conn->setParent(pitem); | |
447 conn->setChild(m_uncommitted); | |
448 conn->setZValue(0); | |
449 m_scene->addItem(conn); | |
450 if (pitem) { | |
451 if (pitem->getChangeset()->branch() == uncommittedBranch) { | |
452 haveParentOnBranch = true; | |
453 } | |
454 } | |
455 } | |
456 | |
457 // If the uncommitted item has no parents on the same branch, | |
458 // tell it it has a new branch (the "show branch" flag is set | |
459 // elsewhere for this item) | |
460 m_uncommitted->setIsNewBranch(!haveParentOnBranch); | |
461 } | |
462 | |
463 // Add the branch labels | |
464 | |
465 foreach (Changeset *cs, csets) { | |
466 QString id = cs->id(); | |
467 ChangesetItem *item = m_items[id]; | |
468 bool haveChildOnSameBranch = false; | |
469 foreach (QString childId, cs->children()) { | |
470 Changeset *child = m_changesets[childId]; | |
471 if (child->branch() == cs->branch()) { | |
472 haveChildOnSameBranch = true; | |
473 break; | |
474 } | |
475 } | |
476 item->setShowBranch(!haveChildOnSameBranch); | |
477 } | |
478 | |
479 // We need to lay out the changesets in forward chronological | |
480 // order. We have no guarantees about the order in which | |
481 // changesets appear in the list -- in a simple repository they | |
482 // will generally be reverse chronological, but that's far from | |
483 // guaranteed. So, sort explicitly using the date comparator | |
484 // above | |
485 | |
486 qStableSort(csets.begin(), csets.end(), compareChangesetsByDate); | |
487 | |
488 foreach (Changeset *cs, csets) { | |
489 DEBUG << "id " << cs->id().toStdString() << ", ts " << cs->timestamp() | |
490 << ", date " << cs->datetime().toStdString() << endl; | |
491 } | |
492 | |
493 m_handled.clear(); | |
494 foreach (Changeset *cs, csets) { | |
495 layoutRow(cs->id()); | |
496 } | |
497 | |
498 allocateBranchHomes(csets); | |
499 | |
500 m_handled.clear(); | |
501 foreach (Changeset *cs, csets) { | |
502 foreach (QString parentId, cs->parents()) { | |
503 if (!m_handled.contains(parentId) && | |
504 m_changesets.contains(parentId)) { | |
505 layoutCol(parentId); | |
506 } | |
507 } | |
508 layoutCol(cs->id()); | |
509 } | |
510 | |
511 // Find row and column extents. We know that 0 is an upper bound | |
512 // on row, and that mincol must be <= 0 and maxcol >= 0, so these | |
513 // initial values are good | |
514 | |
515 int minrow = 0, maxrow = 0; | |
516 int mincol = 0, maxcol = 0; | |
517 | |
518 foreach (int r, m_alloc.keys()) { | |
519 if (r < minrow) minrow = r; | |
520 if (r > maxrow) maxrow = r; | |
521 ColumnSet &c = m_alloc[r]; | |
522 foreach (int i, c) { | |
523 if (i < mincol) mincol = i; | |
524 if (i > maxcol) maxcol = i; | |
525 } | |
526 } | |
527 | |
528 int datemincol = mincol, datemaxcol = maxcol; | |
529 | |
530 if (mincol == maxcol) { | |
531 --datemincol; | |
532 ++datemaxcol; | |
533 } else if (m_alloc[minrow].contains(mincol)) { | |
534 --datemincol; | |
535 } | |
536 | |
537 // We've given the uncommitted item a column, but not a row yet -- | |
538 // it always goes at the top | |
539 | |
540 if (m_uncommitted) { | |
541 --minrow; | |
542 DEBUG << "putting uncommitted item at row " << minrow << endl; | |
543 m_uncommitted->setRow(minrow); | |
544 } | |
545 | |
546 // Changeset items that have nothing to either side of them can be | |
547 // made double-width | |
548 | |
549 foreach (Changeset *cs, csets) { | |
550 ChangesetItem *item = m_items[cs->id()]; | |
551 if (isAvailable(item->row(), item->column()-1) && | |
552 isAvailable(item->row(), item->column()+1)) { | |
553 item->setWide(true); | |
554 } | |
555 } | |
556 | |
557 if (m_uncommitted) { | |
558 if (isAvailable(m_uncommitted->row(), m_uncommitted->column()-1) && | |
559 isAvailable(m_uncommitted->row(), m_uncommitted->column()+1)) { | |
560 m_uncommitted->setWide(true); | |
561 } | |
562 } | |
563 | |
564 QString prevDate; | |
565 int changeRow = 0; | |
566 | |
567 bool even = false; | |
568 int n = 0; | |
569 | |
570 for (int row = minrow; row <= maxrow; ++row) { | |
571 | |
572 QString date = m_rowDates[row]; | |
573 n++; | |
574 | |
575 if (date != prevDate) { | |
576 if (prevDate != "") { | |
577 DateItem *item = new DateItem(); | |
578 item->setDateString(prevDate); | |
579 item->setCols(datemincol, datemaxcol - datemincol + 1); | |
580 item->setRows(changeRow, n); | |
581 item->setEven(even); | |
582 item->setZValue(-2); | |
583 m_scene->addDateItem(item); | |
584 even = !even; | |
585 } | |
586 prevDate = date; | |
587 changeRow = row; | |
588 n = 0; | |
589 } | |
590 } | |
591 | |
592 if (n > 0) { | |
593 DateItem *item = new DateItem(); | |
594 item->setDateString(prevDate); | |
595 item->setCols(datemincol, datemaxcol - datemincol + 1); | |
596 item->setRows(changeRow, n+1); | |
597 item->setEven(even); | |
598 item->setZValue(-2); | |
599 m_scene->addDateItem(item); | |
600 even = !even; | |
601 } | |
602 } | |
603 |