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 |
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 |
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: |