UI/DiffDialog.py

changeset 0
de9c2efb9d02
child 12
1d8dd9706f46
equal deleted inserted replaced
-1:000000000000 0:de9c2efb9d02
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2004 - 2009 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog to compare two files.
8 """
9
10 import os
11 import sys
12 import time
13
14 from PyQt4.QtCore import *
15 from PyQt4.QtGui import *
16
17 from E4Gui.E4Completers import E4FileCompleter
18
19 from Ui_DiffDialog import Ui_DiffDialog
20 import Utilities
21
22 from difflib import SequenceMatcher
23
24 # This function is copied from python 2.3 and slightly modified.
25 # The header lines contain a tab after the filename.
26 def unified_diff(a, b, fromfile='', tofile='', fromfiledate='',
27 tofiledate='', n=3, lineterm='\n'):
28 """
29 Compare two sequences of lines; generate the delta as a unified diff.
30
31 Unified diffs are a compact way of showing line changes and a few
32 lines of context. The number of context lines is set by 'n' which
33 defaults to three.
34
35 By default, the diff control lines (those with ---, +++, or @@) are
36 created with a trailing newline. This is helpful so that inputs
37 created from file.readlines() result in diffs that are suitable for
38 file.writelines() since both the inputs and outputs have trailing
39 newlines.
40
41 For inputs that do not have trailing newlines, set the lineterm
42 argument to "" so that the output will be uniformly newline free.
43
44 The unidiff format normally has a header for filenames and modification
45 times. Any or all of these may be specified using strings for
46 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'. The modification
47 times are normally expressed in the format returned by time.ctime().
48
49 Example:
50
51 <pre>
52 &gt;&gt;&gt; for line in unified_diff('one two three four'.split(),
53 ... 'zero one tree four'.split(), 'Original', 'Current',
54 ... 'Sat Jan 26 23:30:50 1991', 'Fri Jun 06 10:20:52 2003',
55 ... lineterm=''):
56 ... print line
57 --- Original Sat Jan 26 23:30:50 1991
58 +++ Current Fri Jun 06 10:20:52 2003
59 @@ -1,4 +1,4 @@
60 +zero
61 one
62 -two
63 -three
64 +tree
65 four
66 </pre>
67
68 @param a first sequence of lines (list of strings)
69 @param b second sequence of lines (list of strings)
70 @param fromfile filename of the first file (string)
71 @param tofile filename of the second file (string)
72 @param fromfiledate modification time of the first file (string)
73 @param tofiledate modification time of the second file (string)
74 @param n number of lines of context (integer)
75 @param lineterm line termination string (string)
76 @return a generator yielding lines of differences
77 """
78 started = False
79 for group in SequenceMatcher(None,a,b).get_grouped_opcodes(n):
80 if not started:
81 yield '--- %s\t%s%s' % (fromfile, fromfiledate, lineterm)
82 yield '+++ %s\t%s%s' % (tofile, tofiledate, lineterm)
83 started = True
84 i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
85 yield "@@ -%d,%d +%d,%d @@%s" % (i1+1, i2-i1, j1+1, j2-j1, lineterm)
86 for tag, i1, i2, j1, j2 in group:
87 if tag == 'equal':
88 for line in a[i1:i2]:
89 yield ' ' + line
90 continue
91 if tag == 'replace' or tag == 'delete':
92 for line in a[i1:i2]:
93 yield '-' + line
94 if tag == 'replace' or tag == 'insert':
95 for line in b[j1:j2]:
96 yield '+' + line
97
98 # This function is copied from python 2.3 and slightly modified.
99 # The header lines contain a tab after the filename.
100 def context_diff(a, b, fromfile='', tofile='',
101 fromfiledate='', tofiledate='', n=3, lineterm='\n'):
102 """
103 Compare two sequences of lines; generate the delta as a context diff.
104
105 Context diffs are a compact way of showing line changes and a few
106 lines of context. The number of context lines is set by 'n' which
107 defaults to three.
108
109 By default, the diff control lines (those with *** or ---) are
110 created with a trailing newline. This is helpful so that inputs
111 created from file.readlines() result in diffs that are suitable for
112 file.writelines() since both the inputs and outputs have trailing
113 newlines.
114
115 For inputs that do not have trailing newlines, set the lineterm
116 argument to "" so that the output will be uniformly newline free.
117
118 The context diff format normally has a header for filenames and
119 modification times. Any or all of these may be specified using
120 strings for 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'.
121 The modification times are normally expressed in the format returned
122 by time.ctime(). If not specified, the strings default to blanks.
123
124 Example:
125
126 <pre>
127 &gt;&gt;&gt; print ''.join(context_diff('one\ntwo\nthree\nfour\n'.splitlines(1),
128 ... 'zero\none\ntree\nfour\n'.splitlines(1), 'Original', 'Current',
129 ... 'Sat Jan 26 23:30:50 1991', 'Fri Jun 06 10:22:46 2003')),
130 *** Original Sat Jan 26 23:30:50 1991
131 --- Current Fri Jun 06 10:22:46 2003
132 ***************
133 *** 1,4 ****
134 one
135 ! two
136 ! three
137 four
138 --- 1,4 ----
139 + zero
140 one
141 ! tree
142 four
143 </pre>
144
145 @param a first sequence of lines (list of strings)
146 @param b second sequence of lines (list of strings)
147 @param fromfile filename of the first file (string)
148 @param tofile filename of the second file (string)
149 @param fromfiledate modification time of the first file (string)
150 @param tofiledate modification time of the second file (string)
151 @param n number of lines of context (integer)
152 @param lineterm line termination string (string)
153 @return a generator yielding lines of differences
154 """
155
156 started = False
157 prefixmap = {'insert':'+ ', 'delete':'- ', 'replace':'! ', 'equal':' '}
158 for group in SequenceMatcher(None,a,b).get_grouped_opcodes(n):
159 if not started:
160 yield '*** %s\t%s%s' % (fromfile, fromfiledate, lineterm)
161 yield '--- %s\t%s%s' % (tofile, tofiledate, lineterm)
162 started = True
163
164 yield '***************%s' % (lineterm,)
165 if group[-1][2] - group[0][1] >= 2:
166 yield '*** %d,%d ****%s' % (group[0][1]+1, group[-1][2], lineterm)
167 else:
168 yield '*** %d ****%s' % (group[-1][2], lineterm)
169 visiblechanges = [e for e in group if e[0] in ('replace', 'delete')]
170 if visiblechanges:
171 for tag, i1, i2, _, _ in group:
172 if tag != 'insert':
173 for line in a[i1:i2]:
174 yield prefixmap[tag] + line
175
176 if group[-1][4] - group[0][3] >= 2:
177 yield '--- %d,%d ----%s' % (group[0][3]+1, group[-1][4], lineterm)
178 else:
179 yield '--- %d ----%s' % (group[-1][4], lineterm)
180 visiblechanges = [e for e in group if e[0] in ('replace', 'insert')]
181 if visiblechanges:
182 for tag, _, _, j1, j2 in group:
183 if tag != 'delete':
184 for line in b[j1:j2]:
185 yield prefixmap[tag] + line
186
187 class DiffDialog(QWidget, Ui_DiffDialog):
188 """
189 Class implementing a dialog to compare two files.
190 """
191 def __init__(self,parent = None):
192 """
193 Constructor
194 """
195 QWidget.__init__(self,parent)
196 self.setupUi(self)
197
198 self.file1Completer = E4FileCompleter(self.file1Edit)
199 self.file2Completer = E4FileCompleter(self.file2Edit)
200
201 self.diffButton = \
202 self.buttonBox.addButton(self.trUtf8("Compare"), QDialogButtonBox.ActionRole)
203 self.diffButton.setToolTip(\
204 self.trUtf8("Press to perform the comparison of the two files"))
205 self.saveButton = \
206 self.buttonBox.addButton(self.trUtf8("Save"), QDialogButtonBox.ActionRole)
207 self.saveButton.setToolTip(self.trUtf8("Save the output to a patch file"))
208 self.diffButton.setEnabled(False)
209 self.saveButton.setEnabled(False)
210 self.diffButton.setDefault(True)
211
212 self.filename1 = ''
213 self.filename2 = ''
214
215 self.updateInterval = 20 # update every 20 lines
216
217 if Utilities.isWindowsPlatform():
218 self.contents.setFontFamily("Lucida Console")
219 else:
220 self.contents.setFontFamily("Monospace")
221
222 self.cNormalFormat = self.contents.currentCharFormat()
223 self.cAddedFormat = self.contents.currentCharFormat()
224 self.cAddedFormat.setBackground(QBrush(QColor(190, 237, 190)))
225 self.cRemovedFormat = self.contents.currentCharFormat()
226 self.cRemovedFormat.setBackground(QBrush(QColor(237, 190, 190)))
227 self.cReplacedFormat = self.contents.currentCharFormat()
228 self.cReplacedFormat.setBackground(QBrush(QColor(190, 190, 237)))
229 self.cLineNoFormat = self.contents.currentCharFormat()
230 self.cLineNoFormat.setBackground(QBrush(QColor(255, 220, 168)))
231
232 # connect some of our widgets explicitly
233 self.connect(self.file1Edit, SIGNAL("textChanged(const QString &)"),
234 self.__fileChanged)
235 self.connect(self.file2Edit, SIGNAL("textChanged(const QString &)"),
236 self.__fileChanged)
237
238 def show(self, filename = None):
239 """
240 Public slot to show the dialog.
241
242 @param filename name of a file to use as the first file (string)
243 """
244 if filename:
245 self.file1Edit.setText(filename)
246 QWidget.show(self)
247
248 def on_buttonBox_clicked(self, button):
249 """
250 Private slot called by a button of the button box clicked.
251
252 @param button button that was clicked (QAbstractButton)
253 """
254 if button == self.diffButton:
255 self.on_diffButton_clicked()
256 elif button == self.saveButton:
257 self.on_saveButton_clicked()
258
259 @pyqtSlot()
260 def on_saveButton_clicked(self):
261 """
262 Private slot to handle the Save button press.
263
264 It saves the diff shown in the dialog to a file in the local
265 filesystem.
266 """
267 dname, fname = Utilities.splitPath(self.filename2)
268 if fname != '.':
269 fname = "%s.diff" % self.filename2
270 else:
271 fname = dname
272
273 fname, selectedFilter = QFileDialog.getSaveFileNameAndFilter(
274 self,
275 self.trUtf8("Save Diff"),
276 fname,
277 self.trUtf8("Patch Files (*.diff)"),
278 None,
279 QFileDialog.Options(QFileDialog.DontConfirmOverwrite))
280
281 if not fname:
282 return
283
284 ext = QFileInfo(fname).suffix()
285 if not ext:
286 ex = selectedFilter.split("(*")[1].split(")")[0]
287 if ex:
288 fileName += ex
289 if QFileInfo(fname).exists():
290 res = QMessageBox.warning(self,
291 self.trUtf8("Save Diff"),
292 self.trUtf8("<p>The patch file <b>{0}</b> already exists.</p>")
293 .format(fname),
294 QMessageBox.StandardButtons(\
295 QMessageBox.Abort | \
296 QMessageBox.Save),
297 QMessageBox.Abort)
298 if res != QMessageBox.Save:
299 return
300 fname = Utilities.toNativeSeparators(fname)
301
302 try:
303 f = open(fname, "wb")
304 txt = self.contents.toPlainText()
305 try:
306 f.write(txt)
307 except UnicodeError:
308 pass
309 f.close()
310 except IOError, why:
311 QMessageBox.critical(self, self.trUtf8('Save Diff'),
312 self.trUtf8('<p>The patch file <b>{0}</b> could not be saved.<br />'
313 'Reason: {1}</p>')
314 .format(fname, unicode(why)))
315
316 @pyqtSlot()
317 def on_diffButton_clicked(self):
318 """
319 Private slot to handle the Compare button press.
320 """
321 self.filename1 = Utilities.toNativeSeparators(self.file1Edit.text())
322 try:
323 filemtime1 = time.ctime(os.stat(self.filename1).st_mtime)
324 except IOError:
325 filemtime1 = ""
326 try:
327 f1 = open(self.filename1, "rb")
328 lines1 = f1.readlines()
329 f1.close()
330 except IOError:
331 QMessageBox.critical(self,
332 self.trUtf8("Compare Files"),
333 self.trUtf8("""<p>The file <b>{0}</b> could not be read.</p>""")
334 .format(self.filename1))
335 return
336
337 self.filename2 = Utilities.toNativeSeparators(self.file2Edit.text())
338 try:
339 filemtime2 = time.ctime(os.stat(self.filename2).st_mtime)
340 except IOError:
341 filemtime2 = ""
342 try:
343 f2 = open(self.filename2, "rb")
344 lines2 = f2.readlines()
345 f2.close()
346 except IOError:
347 QMessageBox.critical(self,
348 self.trUtf8("Compare Files"),
349 self.trUtf8("""<p>The file <b>{0}</b> could not be read.</p>""")
350 .format(self.filename2))
351 return
352
353 self.contents.clear()
354 self.saveButton.setEnabled(False)
355
356 if self.unifiedRadioButton.isChecked():
357 self.__generateUnifiedDiff(lines1, lines2, self.filename1, self.filename2,
358 filemtime1, filemtime2)
359 else:
360 self.__generateContextDiff(lines1, lines2, self.filename1, self.filename2,
361 filemtime1, filemtime2)
362
363 tc = self.contents.textCursor()
364 tc.movePosition(QTextCursor.Start)
365 self.contents.setTextCursor(tc)
366 self.contents.ensureCursorVisible()
367
368 self.saveButton.setEnabled(True)
369
370 def __appendText(self, txt, format):
371 """
372 Private method to append text to the end of the contents pane.
373
374 @param txt text to insert (string)
375 @param format text format to be used (QTextCharFormat)
376 """
377 tc = self.contents.textCursor()
378 tc.movePosition(QTextCursor.End)
379 self.contents.setTextCursor(tc)
380 self.contents.setCurrentCharFormat(format)
381 self.contents.insertPlainText(txt)
382
383 def __generateUnifiedDiff(self, a, b, fromfile, tofile,
384 fromfiledate, tofiledate):
385 """
386 Private slot to generate a unified diff output.
387
388 @param a first sequence of lines (list of strings)
389 @param b second sequence of lines (list of strings)
390 @param fromfile filename of the first file (string)
391 @param tofile filename of the second file (string)
392 @param fromfiledate modification time of the first file (string)
393 @param tofiledate modification time of the second file (string)
394 """
395 paras = 0
396 for line in unified_diff(a, b, fromfile, tofile,
397 fromfiledate, tofiledate):
398 if line.startswith('+') or line.startswith('>'):
399 format = self.cAddedFormat
400 elif line.startswith('-') or line.startswith('<'):
401 format = self.cRemovedFormat
402 elif line.startswith('@@'):
403 format = self.cLineNoFormat
404 else:
405 format = self.cNormalFormat
406 self.__appendText(line, format)
407 paras += 1
408 if not (paras % self.updateInterval):
409 QApplication.processEvents()
410
411 if paras == 0:
412 self.__appendText(self.trUtf8('There is no difference.'), self.cNormalFormat)
413
414 def __generateContextDiff(self, a, b, fromfile, tofile,
415 fromfiledate, tofiledate):
416 """
417 Private slot to generate a context diff output.
418
419 @param a first sequence of lines (list of strings)
420 @param b second sequence of lines (list of strings)
421 @param fromfile filename of the first file (string)
422 @param tofile filename of the second file (string)
423 @param fromfiledate modification time of the first file (string)
424 @param tofiledate modification time of the second file (string)
425 """
426 paras = 0
427 for line in context_diff(a, b, fromfile, tofile,
428 fromfiledate, tofiledate):
429 if line.startswith('+ '):
430 format = self.cAddedFormat
431 elif line.startswith('- '):
432 format = self.cRemovedFormat
433 elif line.startswith('! '):
434 format = self.cReplacedFormat
435 elif (line.startswith('*** ') or line.startswith('--- ')) and paras > 1:
436 format = self.cLineNoFormat
437 else:
438 format = self.cNormalFormat
439 self.__appendText(line, format)
440 paras += 1
441 if not (paras % self.updateInterval):
442 QApplication.processEvents()
443
444 if paras == 0:
445 self.__appendText(self.trUtf8('There is no difference.'), self.cNormalFormat)
446
447 def __fileChanged(self):
448 """
449 Private slot to enable/disable the Compare button.
450 """
451 if not self.file1Edit.text() or \
452 not self.file2Edit.text():
453 self.diffButton.setEnabled(False)
454 else:
455 self.diffButton.setEnabled(True)
456
457 def __selectFile(self, lineEdit):
458 """
459 Private slot to display a file selection dialog.
460
461 @param lineEdit field for the display of the selected filename
462 (QLineEdit)
463 """
464 filename = QFileDialog.getOpenFileName(
465 self,
466 self.trUtf8("Select file to compare"),
467 lineEdit.text(),
468 "")
469
470 if filename:
471 lineEdit.setText(Utilities.toNativeSeparators(filename))
472
473 @pyqtSlot()
474 def on_file1Button_clicked(self):
475 """
476 Private slot to handle the file 1 file selection button press.
477 """
478 self.__selectFile(self.file1Edit)
479
480 @pyqtSlot("")
481 def on_file2Button_clicked(self):
482 """
483 Private slot to handle the file 2 file selection button press.
484 """
485 self.__selectFile(self.file2Edit)
486
487 class DiffWindow(QMainWindow):
488 """
489 Main window class for the standalone dialog.
490 """
491 def __init__(self, parent = None):
492 """
493 Constructor
494
495 @param parent reference to the parent widget (QWidget)
496 """
497 QMainWindow.__init__(self, parent)
498 self.cw = DiffDialog(self)
499 self.cw.installEventFilter(self)
500 size = self.cw.size()
501 self.setCentralWidget(self.cw)
502 self.resize(size)
503
504 def eventFilter(self, obj, event):
505 """
506 Public method to filter events.
507
508 @param obj reference to the object the event is meant for (QObject)
509 @param event reference to the event object (QEvent)
510 @return flag indicating, whether the event was handled (boolean)
511 """
512 if event.type() == QEvent.Close:
513 QApplication.exit()
514 return True
515
516 return False

eric ide

mercurial