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