UI/CompareDialog.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 and show the result side by side.
8 """
9
10 import os
11 import sys
12 import re
13 from difflib import _mdiff, IS_CHARACTER_JUNK
14
15 from PyQt4.QtCore import *
16 from PyQt4.QtGui import *
17
18 from E4Gui.E4Completers import E4FileCompleter
19
20 import UI.PixmapCache
21
22 from Ui_CompareDialog import Ui_CompareDialog
23
24 import Utilities
25
26 def sbsdiff(a, b, linenumberwidth = 4):
27 """
28 Compare two sequences of lines; generate the delta for display side by side.
29
30 @param a first sequence of lines (list of strings)
31 @param b second sequence of lines (list of strings)
32 @param linenumberwidth width (in characters) of the linenumbers (integer)
33 @return a generator yielding tuples of differences. The tuple is composed
34 of strings as follows.
35 <ul>
36 <li>opcode -- one of e, d, i, r for equal, delete, insert, replace</li>
37 <li>lineno a -- linenumber of sequence a</li>
38 <li>line a -- line of sequence a</li>
39 <li>lineno b -- linenumber of sequence b</li>
40 <li>line b -- line of sequence b</li>
41 </ul>
42 """
43 def removeMarkers(line):
44 """
45 Internal function to remove all diff markers.
46
47 @param line line to work on (string)
48 @return line without diff markers (string)
49 """
50 return line\
51 .replace('\0+', "")\
52 .replace('\0-', "")\
53 .replace('\0^', "")\
54 .replace('\1', "")
55
56 linenumberformat = "%%%dd" % linenumberwidth
57 emptylineno = ' ' * linenumberwidth
58
59 for (ln1, l1), (ln2, l2), flag in _mdiff(a, b, None, None, IS_CHARACTER_JUNK):
60 if not flag:
61 yield ('e', str(linenumberformat % ln1), l1,
62 str(linenumberformat % ln2), l2)
63 continue
64 if ln2 == "" and l2 == "\n":
65 yield ('d', str(linenumberformat % ln1), removeMarkers(l1),
66 emptylineno, '\n')
67 continue
68 if ln1 == "" and l1 == "\n":
69 yield ('i', emptylineno, '\n',
70 str(linenumberformat % ln2), removeMarkers(l2))
71 continue
72 yield ('r', str(linenumberformat % ln1), l1,
73 str(linenumberformat % ln2), l2)
74
75 class CompareDialog(QWidget, Ui_CompareDialog):
76 """
77 Class implementing a dialog to compare two files and show the result side by side.
78 """
79 def __init__(self, files = [], parent = None):
80 """
81 Constructor
82
83 @param files list of files to compare and their label
84 (list of two tuples of two strings)
85 @param parent parent widget (QWidget)
86 """
87 QWidget.__init__(self,parent)
88 self.setupUi(self)
89
90 self.file1Completer = E4FileCompleter(self.file1Edit)
91 self.file2Completer = E4FileCompleter(self.file2Edit)
92
93 self.diffButton = \
94 self.buttonBox.addButton(self.trUtf8("Compare"), QDialogButtonBox.ActionRole)
95 self.diffButton.setToolTip(\
96 self.trUtf8("Press to perform the comparison of the two files"))
97 self.diffButton.setEnabled(False)
98 self.diffButton.setDefault(True)
99
100 self.firstButton.setIcon(UI.PixmapCache.getIcon("2uparrow.png"))
101 self.upButton.setIcon(UI.PixmapCache.getIcon("1uparrow.png"))
102 self.downButton.setIcon(UI.PixmapCache.getIcon("1downarrow.png"))
103 self.lastButton.setIcon(UI.PixmapCache.getIcon("2downarrow.png"))
104
105 self.totalLabel.setText(self.trUtf8('Total: {0}').format(0))
106 self.changedLabel.setText(self.trUtf8('Changed: {0}').format(0))
107 self.addedLabel.setText(self.trUtf8('Added: {0}').format(0))
108 self.deletedLabel.setText(self.trUtf8('Deleted: {0}').format(0))
109
110 self.updateInterval = 20 # update every 20 lines
111
112 self.vsb1 = self.contents_1.verticalScrollBar()
113 self.hsb1 = self.contents_1.horizontalScrollBar()
114 self.vsb2 = self.contents_2.verticalScrollBar()
115 self.hsb2 = self.contents_2.horizontalScrollBar()
116
117 self.on_synchronizeCheckBox_toggled(True)
118
119 if Utilities.isWindowsPlatform():
120 self.contents_1.setFontFamily("Lucida Console")
121 self.contents_2.setFontFamily("Lucida Console")
122 else:
123 self.contents_1.setFontFamily("Monospace")
124 self.contents_2.setFontFamily("Monospace")
125 self.fontHeight = QFontMetrics(self.contents_1.currentFont()).height()
126
127 self.cNormalFormat = self.contents_1.currentCharFormat()
128 self.cInsertedFormat = self.contents_1.currentCharFormat()
129 self.cInsertedFormat.setBackground(QBrush(QColor(190, 237, 190)))
130 self.cDeletedFormat = self.contents_1.currentCharFormat()
131 self.cDeletedFormat.setBackground(QBrush(QColor(237, 190, 190)))
132 self.cReplacedFormat = self.contents_1.currentCharFormat()
133 self.cReplacedFormat.setBackground(QBrush(QColor(190, 190, 237)))
134
135 # connect some of our widgets explicitly
136 self.connect(self.file1Edit, SIGNAL("textChanged(const QString &)"),
137 self.__fileChanged)
138 self.connect(self.file2Edit, SIGNAL("textChanged(const QString &)"),
139 self.__fileChanged)
140 self.connect(self.synchronizeCheckBox, SIGNAL("toggled(bool)"),
141 self.on_synchronizeCheckBox_toggled)
142 self.connect(self.vsb1, SIGNAL("valueChanged(int)"), self.__scrollBarMoved)
143 self.connect(self.vsb1, SIGNAL('valueChanged(int)'),
144 self.vsb2, SLOT('setValue(int)'))
145 self.connect(self.vsb2, SIGNAL('valueChanged(int)'),
146 self.vsb1, SLOT('setValue(int)'))
147
148 self.diffParas = []
149 self.currentDiffPos = -1
150
151 self.markerPattern = "\0\+|\0\^|\0\-"
152
153 if len(files) == 2:
154 self.filesGroup.hide()
155 self.file1Edit.setText(files[0][1])
156 self.file2Edit.setText(files[1][1])
157 self.file1Label.setText(files[0][0])
158 self.file2Label.setText(files[1][0])
159 self.diffButton.hide()
160 QTimer.singleShot(0, self.on_diffButton_clicked)
161 else:
162 self.file1Label.hide()
163 self.file2Label.hide()
164
165 def show(self, filename = None):
166 """
167 Public slot to show the dialog.
168
169 @param filename name of a file to use as the first file (string)
170 """
171 if filename:
172 self.file1Edit.setText(filename)
173 QWidget.show(self)
174
175 def __appendText(self, pane, linenumber, line, format, interLine = False):
176 """
177 Private method to append text to the end of the contents pane.
178
179 @param pane text edit widget to append text to (QTextedit)
180 @param linenumber number of line to insert (string)
181 @param line text to insert (string)
182 @param format text format to be used (QTextCharFormat)
183 @param interLine flag indicating interline changes (boolean)
184 """
185 tc = pane.textCursor()
186 tc.movePosition(QTextCursor.End)
187 pane.setTextCursor(tc)
188 pane.setCurrentCharFormat(format)
189 if interLine:
190 pane.insertPlainText("%s " % linenumber)
191 for txt in re.split(self.markerPattern, line):
192 if txt:
193 if txt.count('\1'):
194 txt1, txt = txt.split('\1', 1)
195 tc = pane.textCursor()
196 tc.movePosition(QTextCursor.End)
197 pane.setTextCursor(tc)
198 pane.setCurrentCharFormat(format)
199 pane.insertPlainText(txt1)
200 tc = pane.textCursor()
201 tc.movePosition(QTextCursor.End)
202 pane.setTextCursor(tc)
203 pane.setCurrentCharFormat(self.cNormalFormat)
204 pane.insertPlainText(txt)
205 else:
206 pane.insertPlainText("%s %s" % (linenumber, line))
207
208 def on_buttonBox_clicked(self, button):
209 """
210 Private slot called by a button of the button box clicked.
211
212 @param button button that was clicked (QAbstractButton)
213 """
214 if button == self.diffButton:
215 self.on_diffButton_clicked()
216
217 @pyqtSlot()
218 def on_diffButton_clicked(self):
219 """
220 Private slot to handle the Compare button press.
221 """
222 filename1 = Utilities.toNativeSeparators(self.file1Edit.text())
223 try:
224 f1 = open(filename1, "rb")
225 lines1 = f1.readlines()
226 f1.close()
227 except IOError:
228 QMessageBox.critical(self,
229 self.trUtf8("Compare Files"),
230 self.trUtf8("""<p>The file <b>{0}</b> could not be read.</p>""")
231 .format(filename1))
232 return
233
234 filename2 = Utilities.toNativeSeparators(self.file2Edit.text())
235 try:
236 f2 = open(filename2, "rb")
237 lines2 = f2.readlines()
238 f2.close()
239 except IOError:
240 QMessageBox.critical(self,
241 self.trUtf8("Compare Files"),
242 self.trUtf8("""<p>The file <b>{0}</b> could not be read.</p>""")
243 .format(filename2))
244 return
245
246 self.contents_1.clear()
247 self.contents_2.clear()
248
249 # counters for changes
250 added = 0
251 deleted = 0
252 changed = 0
253
254 paras = 1
255 self.diffParas = []
256 self.currentDiffPos = -1
257 oldOpcode = ''
258 for opcode, ln1, l1, ln2, l2 in sbsdiff(lines1, lines2):
259 if opcode in 'idr':
260 if oldOpcode != opcode:
261 oldOpcode = opcode
262 self.diffParas.append(paras)
263 # update counters
264 if opcode == 'i':
265 added += 1
266 elif opcode == 'd':
267 deleted += 1
268 elif opcode == 'r':
269 changed += 1
270 if opcode == 'i':
271 format1 = self.cNormalFormat
272 format2 = self.cInsertedFormat
273 elif opcode == 'd':
274 format1 = self.cDeletedFormat
275 format2 = self.cNormalFormat
276 elif opcode == 'r':
277 if ln1.strip():
278 format1 = self.cReplacedFormat
279 else:
280 format1 = self.cNormalFormat
281 if ln2.strip():
282 format2 = self.cReplacedFormat
283 else:
284 format2 = self.cNormalFormat
285 else:
286 oldOpcode = ''
287 format1 = self.cNormalFormat
288 format2 = self.cNormalFormat
289 self.__appendText(self.contents_1, ln1, l1, format1, opcode == 'r')
290 self.__appendText(self.contents_2, ln2, l2, format2, opcode == 'r')
291 paras += 1
292 if not (paras % self.updateInterval):
293 QApplication.processEvents()
294
295 self.vsb1.setValue(0)
296 self.vsb2.setValue(0)
297 self.firstButton.setEnabled(False)
298 self.upButton.setEnabled(False)
299 self.downButton.setEnabled(len(self.diffParas) > 0)
300 self.lastButton.setEnabled(len(self.diffParas) > 0)
301
302 self.totalLabel.setText(self.trUtf8('Total: {0}')\
303 .format(added + deleted + changed))
304 self.changedLabel.setText(self.trUtf8('Changed: {0}').format(changed))
305 self.addedLabel.setText(self.trUtf8('Added: {0}').format(added))
306 self.deletedLabel.setText(self.trUtf8('Deleted: {0}').format(deleted))
307
308 def __moveTextToCurrentDiffPos(self):
309 """
310 Private slot to move the text display to the current diff position.
311 """
312 value = (self.diffParas[self.currentDiffPos] - 1) * self.fontHeight
313 self.vsb1.setValue(value)
314 self.vsb2.setValue(value)
315
316 def __scrollBarMoved(self, value):
317 """
318 Private slot to enable the buttons and set the current diff position
319 depending on scrollbar position.
320
321 @param value scrollbar position (integer)
322 """
323 tPos = value / self.fontHeight + 1
324 bPos = (value + self.vsb1.pageStep()) / self.fontHeight + 1
325
326 self.currentDiffPos = -1
327
328 if self.diffParas:
329 self.firstButton.setEnabled(tPos > self.diffParas[0])
330 self.upButton.setEnabled(tPos > self.diffParas[0])
331 self.downButton.setEnabled(bPos < self.diffParas[-1])
332 self.lastButton.setEnabled(bPos < self.diffParas[-1])
333
334 if tPos >= self.diffParas[0]:
335 for diffPos in self.diffParas:
336 self.currentDiffPos += 1
337 if tPos <= diffPos:
338 break
339
340 @pyqtSlot()
341 def on_upButton_clicked(self):
342 """
343 Private slot to go to the previous difference.
344 """
345 self.currentDiffPos -= 1
346 self.__moveTextToCurrentDiffPos()
347
348 @pyqtSlot()
349 def on_downButton_clicked(self):
350 """
351 Private slot to go to the next difference.
352 """
353 self.currentDiffPos += 1
354 self.__moveTextToCurrentDiffPos()
355
356 @pyqtSlot()
357 def on_firstButton_clicked(self):
358 """
359 Private slot to go to the first difference.
360 """
361 self.currentDiffPos = 0
362 self.__moveTextToCurrentDiffPos()
363
364 @pyqtSlot()
365 def on_lastButton_clicked(self):
366 """
367 Private slot to go to the last difference.
368 """
369 self.currentDiffPos = len(self.diffParas) - 1
370 self.__moveTextToCurrentDiffPos()
371
372 def __fileChanged(self):
373 """
374 Private slot to enable/disable the Compare button.
375 """
376 if not self.file1Edit.text() or \
377 not self.file2Edit.text():
378 self.diffButton.setEnabled(False)
379 else:
380 self.diffButton.setEnabled(True)
381
382 def __selectFile(self, lineEdit):
383 """
384 Private slot to display a file selection dialog.
385
386 @param lineEdit field for the display of the selected filename
387 (QLineEdit)
388 """
389 filename = QFileDialog.getOpenFileName(
390 self,
391 self.trUtf8("Select file to compare"),
392 lineEdit.text(),
393 "")
394
395 if filename:
396 lineEdit.setText(Utilities.toNativeSeparators(filename))
397
398 @pyqtSlot()
399 def on_file1Button_clicked(self):
400 """
401 Private slot to handle the file 1 file selection button press.
402 """
403 self.__selectFile(self.file1Edit)
404
405 @pyqtSlot()
406 def on_file2Button_clicked(self):
407 """
408 Private slot to handle the file 2 file selection button press.
409 """
410 self.__selectFile(self.file2Edit)
411
412 @pyqtSlot(bool)
413 def on_synchronizeCheckBox_toggled(self, sync):
414 """
415 Private slot to connect or disconnect the scrollbars of the displays.
416
417 @param sync flag indicating synchronisation status (boolean)
418 """
419 if sync:
420 self.hsb2.setValue(self.hsb1.value())
421 self.connect(self.hsb1, SIGNAL('valueChanged(int)'),
422 self.hsb2, SLOT('setValue(int)'))
423 self.connect(self.hsb2, SIGNAL('valueChanged(int)'),
424 self.hsb1, SLOT('setValue(int)'))
425 else:
426 self.disconnect(self.hsb1, SIGNAL('valueChanged(int)'),
427 self.hsb2, SLOT('setValue(int)'))
428 self.disconnect(self.hsb2, SIGNAL('valueChanged(int)'),
429 self.hsb1, SLOT('setValue(int)'))
430
431 class CompareWindow(QMainWindow):
432 """
433 Main window class for the standalone dialog.
434 """
435 def __init__(self, files = [], parent = None):
436 """
437 Constructor
438
439 @param files list of files to compare and their label
440 (list of two tuples of two strings)
441 @param parent reference to the parent widget (QWidget)
442 """
443 QMainWindow.__init__(self, parent)
444 self.cw = CompareDialog(files, self)
445 self.cw.installEventFilter(self)
446 size = self.cw.size()
447 self.setCentralWidget(self.cw)
448 self.resize(size)
449
450 def eventFilter(self, obj, event):
451 """
452 Public method to filter events.
453
454 @param obj reference to the object the event is meant for (QObject)
455 @param event reference to the event object (QEvent)
456 @return flag indicating, whether the event was handled (boolean)
457 """
458 if event.type() == QEvent.Close:
459 QApplication.exit()
460 return True
461
462 return False

eric ide

mercurial