eric7/UI/DiffDialog.py

branch
eric7
changeset 8312
800c432b34c8
parent 8243
cc717c2ae956
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2004 - 2021 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 time
12 import contextlib
13 from difflib import unified_diff, context_diff
14
15 from PyQt5.QtCore import QFileInfo, QEvent, pyqtSlot
16 from PyQt5.QtGui import QTextCursor
17 from PyQt5.QtWidgets import QWidget, QApplication, QDialogButtonBox
18
19 from E5Gui import E5MessageBox, E5FileDialog
20 from E5Gui.E5MainWindow import E5MainWindow
21 from E5Gui.E5PathPicker import E5PathPickerModes
22
23 from .Ui_DiffDialog import Ui_DiffDialog
24 from .DiffHighlighter import DiffHighlighter
25
26 import Utilities
27 import Preferences
28
29
30 class DiffDialog(QWidget, Ui_DiffDialog):
31 """
32 Class implementing a dialog to compare two files.
33 """
34 def __init__(self, parent=None):
35 """
36 Constructor
37
38 @param parent reference to the parent widget (QWidget)
39 """
40 super().__init__(parent)
41 self.setupUi(self)
42
43 self.file1Picker.setMode(E5PathPickerModes.OpenFileMode)
44 self.file2Picker.setMode(E5PathPickerModes.OpenFileMode)
45
46 self.diffButton = self.buttonBox.addButton(
47 self.tr("Compare"), QDialogButtonBox.ButtonRole.ActionRole)
48 self.diffButton.setToolTip(
49 self.tr("Press to perform the comparison of the two files"))
50 self.saveButton = self.buttonBox.addButton(
51 self.tr("Save"), QDialogButtonBox.ButtonRole.ActionRole)
52 self.saveButton.setToolTip(
53 self.tr("Save the output to a patch file"))
54 self.diffButton.setEnabled(False)
55 self.saveButton.setEnabled(False)
56 self.diffButton.setDefault(True)
57
58 self.searchWidget.attachTextEdit(self.contents)
59
60 self.filename1 = ''
61 self.filename2 = ''
62
63 self.updateInterval = 20 # update every 20 lines
64
65 font = Preferences.getEditorOtherFonts("MonospacedFont")
66 self.contents.document().setDefaultFont(font)
67
68 self.highlighter = DiffHighlighter(self.contents.document())
69
70 # connect some of our widgets explicitly
71 self.file1Picker.textChanged.connect(self.__fileChanged)
72 self.file2Picker.textChanged.connect(self.__fileChanged)
73
74 def show(self, filename=None):
75 """
76 Public slot to show the dialog.
77
78 @param filename name of a file to use as the first file (string)
79 """
80 if filename:
81 self.file1Picker.setText(filename)
82 super().show()
83
84 def on_buttonBox_clicked(self, button):
85 """
86 Private slot called by a button of the button box clicked.
87
88 @param button button that was clicked (QAbstractButton)
89 """
90 if button == self.diffButton:
91 self.on_diffButton_clicked()
92 elif button == self.saveButton:
93 self.on_saveButton_clicked()
94
95 @pyqtSlot()
96 def on_saveButton_clicked(self):
97 """
98 Private slot to handle the Save button press.
99
100 It saves the diff shown in the dialog to a file in the local
101 filesystem.
102 """
103 dname, fname = Utilities.splitPath(self.filename2)
104 fname = "{0}.diff".format(self.filename2) if fname != '.' else dname
105
106 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
107 self,
108 self.tr("Save Diff"),
109 fname,
110 self.tr("Patch Files (*.diff)"),
111 None,
112 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
113
114 if not fname:
115 return
116
117 ext = QFileInfo(fname).suffix()
118 if not ext:
119 ex = selectedFilter.split("(*")[1].split(")")[0]
120 if ex:
121 fname += ex
122 if QFileInfo(fname).exists():
123 res = E5MessageBox.yesNo(
124 self,
125 self.tr("Save Diff"),
126 self.tr("<p>The patch file <b>{0}</b> already exists."
127 " Overwrite it?</p>").format(fname),
128 icon=E5MessageBox.Warning)
129 if not res:
130 return
131 fname = Utilities.toNativeSeparators(fname)
132
133 txt = self.contents.toPlainText()
134 try:
135 with open(fname, "w", encoding="utf-8") as f, \
136 contextlib.suppress(UnicodeError):
137 f.write(txt)
138 except OSError as why:
139 E5MessageBox.critical(
140 self, self.tr('Save Diff'),
141 self.tr(
142 '<p>The patch file <b>{0}</b> could not be saved.<br />'
143 'Reason: {1}</p>').format(fname, str(why)))
144
145 @pyqtSlot()
146 def on_diffButton_clicked(self):
147 """
148 Private slot to handle the Compare button press.
149 """
150 self.filename1 = Utilities.toNativeSeparators(self.file1Picker.text())
151 try:
152 filemtime1 = time.ctime(os.stat(self.filename1).st_mtime)
153 except OSError:
154 filemtime1 = ""
155 try:
156 with open(self.filename1, "r", encoding="utf-8") as f1:
157 lines1 = f1.readlines()
158 except OSError:
159 E5MessageBox.critical(
160 self,
161 self.tr("Compare Files"),
162 self.tr(
163 """<p>The file <b>{0}</b> could not be read.</p>""")
164 .format(self.filename1))
165 return
166
167 self.filename2 = Utilities.toNativeSeparators(self.file2Picker.text())
168 try:
169 filemtime2 = time.ctime(os.stat(self.filename2).st_mtime)
170 except OSError:
171 filemtime2 = ""
172 try:
173 with open(self.filename2, "r", encoding="utf-8") as f2:
174 lines2 = f2.readlines()
175 except OSError:
176 E5MessageBox.critical(
177 self,
178 self.tr("Compare Files"),
179 self.tr(
180 """<p>The file <b>{0}</b> could not be read.</p>""")
181 .format(self.filename2))
182 return
183
184 self.contents.clear()
185 self.highlighter.regenerateRules()
186 self.saveButton.setEnabled(False)
187
188 if self.unifiedRadioButton.isChecked():
189 self.__generateUnifiedDiff(
190 lines1, lines2, self.filename1, self.filename2,
191 filemtime1, filemtime2)
192 else:
193 self.__generateContextDiff(
194 lines1, lines2, self.filename1, self.filename2,
195 filemtime1, filemtime2)
196
197 tc = self.contents.textCursor()
198 tc.movePosition(QTextCursor.MoveOperation.Start)
199 self.contents.setTextCursor(tc)
200 self.contents.ensureCursorVisible()
201
202 self.saveButton.setEnabled(True)
203
204 def __appendText(self, txt):
205 """
206 Private method to append text to the end of the contents pane.
207
208 @param txt text to insert (string)
209 """
210 tc = self.contents.textCursor()
211 tc.movePosition(QTextCursor.MoveOperation.End)
212 self.contents.setTextCursor(tc)
213 self.contents.insertPlainText(txt)
214
215 def __generateUnifiedDiff(self, a, b, fromfile, tofile,
216 fromfiledate, tofiledate):
217 """
218 Private slot to generate a unified diff output.
219
220 @param a first sequence of lines (list of strings)
221 @param b second sequence of lines (list of strings)
222 @param fromfile filename of the first file (string)
223 @param tofile filename of the second file (string)
224 @param fromfiledate modification time of the first file (string)
225 @param tofiledate modification time of the second file (string)
226 """
227 for paras, line in enumerate(
228 unified_diff(a, b, fromfile, tofile, fromfiledate, tofiledate)
229 ):
230 self.__appendText(line)
231 if not (paras % self.updateInterval):
232 QApplication.processEvents()
233
234 if self.contents.toPlainText().strip() == "":
235 self.__appendText(self.tr('There is no difference.'))
236
237 def __generateContextDiff(self, a, b, fromfile, tofile,
238 fromfiledate, tofiledate):
239 """
240 Private slot to generate a context diff output.
241
242 @param a first sequence of lines (list of strings)
243 @param b second sequence of lines (list of strings)
244 @param fromfile filename of the first file (string)
245 @param tofile filename of the second file (string)
246 @param fromfiledate modification time of the first file (string)
247 @param tofiledate modification time of the second file (string)
248 """
249 for paras, line in enumerate(
250 context_diff(a, b, fromfile, tofile, fromfiledate, tofiledate)
251 ):
252 self.__appendText(line)
253 if not (paras % self.updateInterval):
254 QApplication.processEvents()
255
256 if self.contents.toPlainText().strip() == "":
257 self.__appendText(self.tr('There is no difference.'))
258
259 def __fileChanged(self):
260 """
261 Private slot to enable/disable the Compare button.
262 """
263 if (
264 not self.file1Picker.text() or
265 not self.file2Picker.text()
266 ):
267 self.diffButton.setEnabled(False)
268 else:
269 self.diffButton.setEnabled(True)
270
271
272 class DiffWindow(E5MainWindow):
273 """
274 Main window class for the standalone dialog.
275 """
276 def __init__(self, parent=None):
277 """
278 Constructor
279
280 @param parent reference to the parent widget (QWidget)
281 """
282 super().__init__(parent)
283
284 self.setStyle(Preferences.getUI("Style"),
285 Preferences.getUI("StyleSheet"))
286
287 self.cw = DiffDialog(self)
288 self.cw.installEventFilter(self)
289 size = self.cw.size()
290 self.setCentralWidget(self.cw)
291 self.resize(size)
292
293 def eventFilter(self, obj, event):
294 """
295 Public method to filter events.
296
297 @param obj reference to the object the event is meant for (QObject)
298 @param event reference to the event object (QEvent)
299 @return flag indicating, whether the event was handled (boolean)
300 """
301 if event.type() == QEvent.Type.Close:
302 QApplication.exit()
303 return True
304
305 return False

eric ide

mercurial