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