Plugins/VcsPlugins/vcsMercurial/HgLogBrowserDialog.py

changeset 180
40ac468c2558
parent 179
09260f69bf37
child 181
4af57f97c1bc
equal deleted inserted replaced
179:09260f69bf37 180:40ac468c2558
7 Module implementing a dialog to browse the log history. 7 Module implementing a dialog to browse the log history.
8 """ 8 """
9 9
10 import os 10 import os
11 11
12 from PyQt4.QtCore import pyqtSlot, SIGNAL, Qt, QDate, QProcess, QTimer, QRegExp 12 from PyQt4.QtCore import pyqtSlot, SIGNAL, Qt, QDate, QProcess, QTimer, QRegExp, QSize
13 from PyQt4.QtGui import QDialog, QDialogButtonBox, QHeaderView, QTreeWidgetItem, \ 13 from PyQt4.QtGui import QDialog, QDialogButtonBox, QHeaderView, QTreeWidgetItem, \
14 QApplication, QMessageBox, QCursor, QWidget, QLineEdit 14 QApplication, QMessageBox, QCursor, QWidget, QLineEdit, QColor, QPixmap, \
15 QPainter, QPen, QBrush, QIcon
15 16
16 from .Ui_HgLogBrowserDialog import Ui_HgLogBrowserDialog 17 from .Ui_HgLogBrowserDialog import Ui_HgLogBrowserDialog
17 from .HgDiffDialog import HgDiffDialog 18 from .HgDiffDialog import HgDiffDialog
18 19
19 import UI.PixmapCache 20 import UI.PixmapCache
20 21
21 import Preferences 22 import Preferences
23
24 COLORNAMES = ["blue", "darkgreen", "red", "green", "darkblue", "purple",
25 "cyan", "olive", "magenta", "darkred", "darkmagenta",
26 "darkcyan", "gray", "yellow"]
27 COLORS = [str(QColor(x).name()) for x in COLORNAMES]
22 28
23 class HgLogBrowserDialog(QDialog, Ui_HgLogBrowserDialog): 29 class HgLogBrowserDialog(QDialog, Ui_HgLogBrowserDialog):
24 """ 30 """
25 Class implementing a dialog to browse the log history. 31 Class implementing a dialog to browse the log history.
26 """ 32 """
33 IconColumn = 0
34 BranchColumn = 1
35 RevisionColumn = 2
36 AuthorColumn = 3
37 DateColumn = 4
38 MessageColumn = 5
39 TagsColumn = 6
40
27 def __init__(self, vcs, parent = None): 41 def __init__(self, vcs, parent = None):
28 """ 42 """
29 Constructor 43 Constructor
30 44
31 @param vcs reference to the vcs object 45 @param vcs reference to the vcs object
55 self.limitSpinBox.setValue(self.vcs.getPlugin().getPreferences("LogLimit")) 69 self.limitSpinBox.setValue(self.vcs.getPlugin().getPreferences("LogLimit"))
56 self.stopCheckBox.setChecked(self.vcs.getPlugin().getPreferences("StopLogOnCopy")) 70 self.stopCheckBox.setChecked(self.vcs.getPlugin().getPreferences("StopLogOnCopy"))
57 71
58 self.__messageRole = Qt.UserRole 72 self.__messageRole = Qt.UserRole
59 self.__changesRole = Qt.UserRole + 1 73 self.__changesRole = Qt.UserRole + 1
60 self.__parentsRole = Qt.UserRole + 2 74 self.__edgesRole = Qt.UserRole + 2
61 75
62 self.process = QProcess() 76 self.process = QProcess()
63 self.connect(self.process, SIGNAL('finished(int, QProcess::ExitStatus)'), 77 self.connect(self.process, SIGNAL('finished(int, QProcess::ExitStatus)'),
64 self.__procFinished) 78 self.__procFinished)
65 self.connect(self.process, SIGNAL('readyReadStandardOutput()'), 79 self.connect(self.process, SIGNAL('readyReadStandardOutput()'),
75 89
76 self.buf = [] # buffer for stdout 90 self.buf = [] # buffer for stdout
77 self.diff = None 91 self.diff = None
78 self.__started = False 92 self.__started = False
79 self.__lastRev = 0 93 self.__lastRev = 0
94
95 # attributes to store log graph data
96 self.__revs = []
97 self.__revColors = {}
98 self.__revColor = 0
99
100 self.__dotRadius = 8
101 self.__rowHeight = 20
102
103 self.__branchColors = {}
104
105 self.logTree.setIconSize(QSize(100 * self.__rowHeight, self.__rowHeight))
80 106
81 def closeEvent(self, e): 107 def closeEvent(self, e):
82 """ 108 """
83 Private slot implementing a close event handler. 109 Private slot implementing a close event handler.
84 110
97 Private method to resize the log tree columns. 123 Private method to resize the log tree columns.
98 """ 124 """
99 self.logTree.header().resizeSections(QHeaderView.ResizeToContents) 125 self.logTree.header().resizeSections(QHeaderView.ResizeToContents)
100 self.logTree.header().setStretchLastSection(True) 126 self.logTree.header().setStretchLastSection(True)
101 127
102 def __resortLog(self):
103 """
104 Private method to resort the log tree.
105 """
106 self.logTree.sortItems(self.logTree.sortColumn(),
107 self.logTree.header().sortIndicatorOrder())
108
109 def __resizeColumnsFiles(self): 128 def __resizeColumnsFiles(self):
110 """ 129 """
111 Private method to resize the changed files tree columns. 130 Private method to resize the changed files tree columns.
112 """ 131 """
113 self.filesTree.header().resizeSections(QHeaderView.ResizeToContents) 132 self.filesTree.header().resizeSections(QHeaderView.ResizeToContents)
121 self.filesTree.sortItems(1, 140 self.filesTree.sortItems(1,
122 self.filesTree.header().sortIndicatorOrder()) 141 self.filesTree.header().sortIndicatorOrder())
123 self.filesTree.sortItems(sortColumn, 142 self.filesTree.sortItems(sortColumn,
124 self.filesTree.header().sortIndicatorOrder()) 143 self.filesTree.header().sortIndicatorOrder())
125 144
126 def __generateLogItem(self, author, date, message, revision, changedPaths, parents): 145 def __getColor(self, n):
146 """
147 Private method to get the (rotating) name of the color given an index.
148
149 @param n color index (integer)
150 @return color name (string)
151 """
152 return COLORS[n % len(COLORS)]
153
154 def __branchColor(self, branchName):
155 """
156 Private method to calculate a color for a given branch name.
157
158 @param branchName name of the branch (string)
159 @return name of the color to use (string)
160 """
161 if branchName not in self.__branchColors:
162 self.__branchColors[branchName] = self.__getColor(len(self.__branchColors))
163 return self.__branchColors[branchName]
164
165 def __generateEdges(self, rev, parents):
166 """
167 Private method to generate edge info for the give data.
168
169 @param rev revision to calculate edge info for (integer)
170 @param parents list of parent revisions (list of integers)
171 @return tuple containing the column and color index for
172 the given node and a list of tuples indicating the edges
173 between the given node and its parents
174 (integer, integer, [(integer, integer, integer), ...])
175 """
176 if not parents:
177 parents = [rev - 1]
178
179 if rev not in self.__revs:
180 # new head
181 self.__revs.append(rev)
182 self.__revColors[rev] = self.__revColor
183 self.__revColor += 1
184
185 col = self.__revs.index(rev)
186 color = self.__revColors.pop(rev)
187 next = self.__revs[:]
188
189 # add parents to next
190 addparents = [p for p in parents if p not in next]
191 next[col:col + 1] = addparents
192
193 # set colors for the parents
194 for i, p in enumerate(addparents):
195 if not i:
196 self.__revColors[p] = color
197 else:
198 self.__revColors[p] = self.__revColor
199 self.__revColor += 1
200
201 # add edges to the graph
202 edges = []
203 if rev:
204 for ecol, erev in enumerate(self.__revs):
205 if erev in next:
206 edges.append((ecol, next.index(erev), self.__revColors[erev]))
207 elif erev == rev:
208 for p in parents:
209 edges.append((ecol, next.index(p), self.__revColors[p]))
210
211 self.__revs = next
212 return col, color, edges
213
214 def __generateIcon(self, column, color, bottomedges, topedges, dotColor):
215 """
216 Private method to generate an icon containing the revision tree for the
217 given data.
218
219 @param column column index of the revision (integer)
220 @param color color of the node (integer)
221 @param bottomedges list of edges for the bottom of the node
222 (list of tuples of three integers)
223 @param topedges list of edges for the top of the node
224 (list of tuples of three integers)
225 @param dotColor color to be used for the dot (QColor)
226 @return icon for the node (QIcon)
227 """
228 def col2x(col, radius):
229 return int(1.2 * radius) * col + radius // 2 + 3
230
231 radius = self.__dotRadius
232 w = len(bottomedges) * radius + 20
233 h = self.__rowHeight
234
235 dot_x = col2x(column, radius) - radius // 2
236 dot_y = h // 2
237
238 pix = QPixmap(w, h)
239 pix.fill(QColor(0, 0, 0, 0))
240 painter = QPainter(pix)
241 painter.setRenderHint(QPainter.Antialiasing)
242
243 pen = QPen(Qt.blue)
244 pen.setWidth(2)
245 painter.setPen(pen)
246
247 lpen = QPen(pen)
248 lpen.setColor(Qt.black)
249 painter.setPen(lpen)
250
251 for y1, y2, lines in ((0, h, bottomedges),
252 (-h, 0, topedges)):
253 if lines:
254 for start, end, ecolor in lines:
255 lpen = QPen(pen)
256 lpen.setColor(QColor(self.__getColor(ecolor)))
257 lpen.setWidth(2)
258 painter.setPen(lpen)
259 x1 = col2x(start, radius)
260 x2 = col2x(end, radius)
261 painter.drawLine(x1, dot_y + y1, x2, dot_y + y2)
262
263 penradius = 1
264 pencolor = Qt.black
265
266 dot_y = (h // 2) - radius // 2
267
268 painter.setBrush(dotColor)
269 pen = QPen(pencolor)
270 pen.setWidth(penradius)
271 painter.setPen(pen)
272 painter.drawEllipse(dot_x, dot_y, radius, radius)
273 painter.end()
274 return QIcon(pix)
275
276 def __generateLogItem(self, author, date, message, revision, changedPaths, parents,
277 branches, tags):
127 """ 278 """
128 Private method to generate a log tree entry. 279 Private method to generate a log tree entry.
129 280
130 @param author author info (string) 281 @param author author info (string)
131 @param date date info (string) 282 @param date date info (string)
132 @param message text of the log message (list of strings) 283 @param message text of the log message (list of strings)
133 @param revision revision info (string) 284 @param revision revision info (string)
134 @param changedPaths list of dictionary objects containing 285 @param changedPaths list of dictionary objects containing
135 info about the changed files/directories 286 info about the changed files/directories
136 @param parents list of parent revisions (list of integers) 287 @param parents list of parent revisions (list of integers)
288 @param branches list of branches (list of strings)
289 @param tags list of tags (string)
137 @return reference to the generated item (QTreeWidgetItem) 290 @return reference to the generated item (QTreeWidgetItem)
138 """ 291 """
139 msg = [] 292 msg = []
140 for line in message: 293 for line in message:
141 msg.append(line.strip()) 294 msg.append(line.strip())
142 295
143 rev, node = revision.split(":") 296 rev, node = revision.split(":")
144 itm = QTreeWidgetItem(self.logTree, [ 297 itm = QTreeWidgetItem(self.logTree, [
298 "",
299 branches[0],
145 "{0:>7}:{1}".format(rev, node), 300 "{0:>7}:{1}".format(rev, node),
146 author, 301 author,
147 date, 302 date,
148 " ".join(msg), 303 " ".join(msg[:1]),
304 ", ".join(tags),
149 ]) 305 ])
306
307 itm.setForeground(self.BranchColumn,
308 QBrush(QColor(self.__branchColor(branches[0]))))
309
310 column, color, edges = self.__generateEdges(int(rev), parents)
150 311
151 itm.setData(0, self.__messageRole, message) 312 itm.setData(0, self.__messageRole, message)
152 itm.setData(0, self.__changesRole, changedPaths) 313 itm.setData(0, self.__changesRole, changedPaths)
153 itm.setData(0, self.__parentsRole, parents) 314 itm.setData(0, self.__edgesRole, edges)
154 315
155 itm.setTextAlignment(0, Qt.AlignLeft) 316 if self.fname == "." and self.dname == self.repodir:
156 itm.setTextAlignment(1, Qt.AlignLeft) 317 if self.logTree.topLevelItemCount() > 1:
157 itm.setTextAlignment(2, Qt.AlignLeft) 318 topedges = \
158 itm.setTextAlignment(3, Qt.AlignLeft) 319 self.logTree.topLevelItem(self.logTree.indexOfTopLevelItem(itm) - 1)\
159 itm.setTextAlignment(4, Qt.AlignLeft) 320 .data(0, self.__edgesRole)
321 else:
322 topedges = None
323
324 icon = self.__generateIcon(column, color, edges, topedges,
325 QColor(self.__branchColor(branches[0])))
326 itm.setIcon(0, icon)
160 327
161 try: 328 try:
162 self.__lastRev = int(revision.split(":")[0]) 329 self.__lastRev = int(revision.split(":")[0])
163 except ValueError: 330 except ValueError:
164 self.__lastRev = 0 331 self.__lastRev = 0
215 args.append('{0}:0'.format(startRev)) 382 args.append('{0}:0'.format(startRev))
216 if not self.stopCheckBox.isChecked(): 383 if not self.stopCheckBox.isChecked():
217 args.append('--follow') 384 args.append('--follow')
218 args.append('--template') 385 args.append('--template')
219 args.append("change|{rev}:{node|short}\n" 386 args.append("change|{rev}:{node|short}\n"
220 "user|{author}\n" 387 "user|{email}\n"
221 "parents|{parents}\n" 388 "parents|{parents}\n"
222 "date|{date|isodate}\n" 389 "date|{date|isodate}\n"
223 "description|{desc}\n" 390 "description|{desc}\n"
224 "file_adds|{file_adds}\n" 391 "file_adds|{file_adds}\n"
225 "files_mods|{file_mods}\n" 392 "files_mods|{file_mods}\n"
226 "file_dels|{file_dels}\n" 393 "file_dels|{file_dels}\n"
394 "branches|{branches}\n"
395 "tags|{tags}\n"
227 "@@@\n") 396 "@@@\n")
228 if self.fname != "." or self.dname != self.repodir: 397 if self.fname != "." or self.dname != self.repodir:
229 args.append(self.filename) 398 args.append(self.filename)
230 399
231 self.process.setWorkingDirectory(self.repodir) 400 self.process.setWorkingDirectory(self.repodir)
313 if key == "change": 482 if key == "change":
314 log["revision"] = value.strip() 483 log["revision"] = value.strip()
315 elif key == "user": 484 elif key == "user":
316 log["author"] = value.strip() 485 log["author"] = value.strip()
317 elif key == "parents": 486 elif key == "parents":
318 log["parents"] = [int(x) for x in value.strip().split()] 487 log["parents"] = \
488 [int(x.split(":", 1)[0]) for x in value.strip().split()]
319 elif key == "date": 489 elif key == "date":
320 log["date"] = " ".join(value.strip().split()[:2]) 490 log["date"] = " ".join(value.strip().split()[:2])
321 elif key == "description": 491 elif key == "description":
322 log["message"].append(value.strip()) 492 log["message"].append(value.strip())
323 elif key == "file_adds": 493 elif key == "file_adds":
339 for f in value.strip().split(): 509 for f in value.strip().split():
340 changedPaths.append({\ 510 changedPaths.append({\
341 "action" : "D", 511 "action" : "D",
342 "path" : f, 512 "path" : f,
343 }) 513 })
514 elif key == "branches":
515 if value.strip():
516 log["branches"] = value.strip().split()
517 else:
518 log["branches"] = ["default"]
519 elif key == "tags":
520 log["tags"] = value.strip().split()
344 else: 521 else:
345 if value.strip(): 522 if value.strip():
346 log["message"].append(value.strip()) 523 log["message"].append(value.strip())
347 else: 524 else:
348 if len(log) > 1: 525 if len(log) > 1:
349 self.__generateLogItem(log["author"], log["date"], 526 self.__generateLogItem(log["author"], log["date"],
350 log["message"], log["revision"], changedPaths, 527 log["message"], log["revision"], changedPaths,
351 log["parents"]) 528 log["parents"], log["branches"], log["tags"])
352 dt = QDate.fromString(log["date"], Qt.ISODate) 529 dt = QDate.fromString(log["date"], Qt.ISODate)
353 if not self.__maxDate.isValid() and not self.__minDate.isValid(): 530 if not self.__maxDate.isValid() and not self.__minDate.isValid():
354 self.__maxDate = dt 531 self.__maxDate = dt
355 self.__minDate = dt 532 self.__minDate = dt
356 else: 533 else:
362 log = {"message" : []} 539 log = {"message" : []}
363 changedPaths = [] 540 changedPaths = []
364 541
365 self.logTree.doItemsLayout() 542 self.logTree.doItemsLayout()
366 self.__resizeColumnsLog() 543 self.__resizeColumnsLog()
367 self.__resortLog()
368 544
369 if self.__started: 545 if self.__started:
370 self.logTree.setCurrentItem(self.logTree.topLevelItem(0)) 546 self.logTree.setCurrentItem(self.logTree.topLevelItem(0))
371 self.__started = False 547 self.__started = False
372 548
484 """ 660 """
485 itm = self.logTree.currentItem() 661 itm = self.logTree.currentItem()
486 if itm is None: 662 if itm is None:
487 self.diffPreviousButton.setEnabled(False) 663 self.diffPreviousButton.setEnabled(False)
488 return 664 return
489 rev2 = int(itm.text(0).split(":")[0]) 665 rev2 = int(itm.text(self.RevisionColumn).split(":")[0])
490 666
491 itm = self.logTree.topLevelItem(self.logTree.indexOfTopLevelItem(itm) + 1) 667 itm = self.logTree.topLevelItem(self.logTree.indexOfTopLevelItem(itm) + 1)
492 if itm is None: 668 if itm is None:
493 self.diffPreviousButton.setEnabled(False) 669 self.diffPreviousButton.setEnabled(False)
494 return 670 return
495 rev1 = int(itm.text(0).split(":")[0]) 671 rev1 = int(itm.text(self.RevisionColumn).split(":")[0])
496 672
497 self.__diffRevisions(rev1, rev2) 673 self.__diffRevisions(rev1, rev2)
498 674
499 @pyqtSlot() 675 @pyqtSlot()
500 def on_diffRevisionsButton_clicked(self): 676 def on_diffRevisionsButton_clicked(self):
504 items = self.logTree.selectedItems() 680 items = self.logTree.selectedItems()
505 if len(items) != 2: 681 if len(items) != 2:
506 self.diffRevisionsButton.setEnabled(False) 682 self.diffRevisionsButton.setEnabled(False)
507 return 683 return
508 684
509 rev2 = int(items[0].text(0).split(":")[0]) 685 rev2 = int(items[0].text(self.RevisionColumn).split(":")[0])
510 rev1 = int(items[1].text(0).split(":")[0]) 686 rev1 = int(items[1].text(self.RevisionColumn).split(":")[0])
511 687
512 self.__diffRevisions(min(rev1, rev2), max(rev1, rev2)) 688 self.__diffRevisions(min(rev1, rev2), max(rev1, rev2))
513 689
514 @pyqtSlot(QDate) 690 @pyqtSlot(QDate)
515 def on_fromDate_dateChanged(self, date): 691 def on_fromDate_dateChanged(self, date):
554 if self.__filterLogsEnabled: 730 if self.__filterLogsEnabled:
555 from_ = self.fromDate.date().toString("yyyy-MM-dd") 731 from_ = self.fromDate.date().toString("yyyy-MM-dd")
556 to_ = self.toDate.date().addDays(1).toString("yyyy-MM-dd") 732 to_ = self.toDate.date().addDays(1).toString("yyyy-MM-dd")
557 txt = self.fieldCombo.currentText() 733 txt = self.fieldCombo.currentText()
558 if txt == self.trUtf8("Author"): 734 if txt == self.trUtf8("Author"):
559 fieldIndex = 1 735 fieldIndex = self.AuthorColumn
560 searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive) 736 searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive)
561 elif txt == self.trUtf8("Revision"): 737 elif txt == self.trUtf8("Revision"):
562 fieldIndex = 0 738 fieldIndex = self.RevisionColumn
563 txt = self.rxEdit.text() 739 txt = self.rxEdit.text()
564 if txt.startswith("^"): 740 if txt.startswith("^"):
565 searchRx = QRegExp("^\s*%s" % txt[1:], Qt.CaseInsensitive) 741 searchRx = QRegExp("^\s*%s" % txt[1:], Qt.CaseInsensitive)
566 else: 742 else:
567 searchRx = QRegExp(txt, Qt.CaseInsensitive) 743 searchRx = QRegExp(txt, Qt.CaseInsensitive)
568 else: 744 else:
569 fieldIndex = 3 745 fieldIndex = self.MessageColumn
570 searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive) 746 searchRx = QRegExp(self.rxEdit.text(), Qt.CaseInsensitive)
571 747
572 currentItem = self.logTree.currentItem() 748 currentItem = self.logTree.currentItem()
573 for topIndex in range(self.logTree.topLevelItemCount()): 749 for topIndex in range(self.logTree.topLevelItemCount()):
574 topItem = self.logTree.topLevelItem(topIndex) 750 topItem = self.logTree.topLevelItem(topIndex)
575 if topItem.text(2) <= to_ and topItem.text(2) >= from_ and \ 751 if topItem.text(self.DateColumn) <= to_ and \
752 topItem.text(self.DateColumn) >= from_ and \
576 searchRx.indexIn(topItem.text(fieldIndex)) > -1: 753 searchRx.indexIn(topItem.text(fieldIndex)) > -1:
577 topItem.setHidden(False) 754 topItem.setHidden(False)
578 if topItem is currentItem: 755 if topItem is currentItem:
579 self.on_logTree_currentItemChanged(topItem, None) 756 self.on_logTree_currentItemChanged(topItem, None)
580 else: 757 else:

eric ide

mercurial