|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2014 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog to show a list of files which had or still have |
|
8 conflicts. |
|
9 """ |
|
10 |
|
11 from __future__ import unicode_literals |
|
12 |
|
13 import os |
|
14 |
|
15 from PyQt5.QtCore import pyqtSlot, Qt, QPoint, QProcess, QTimer |
|
16 from PyQt5.QtWidgets import QAbstractButton, QDialogButtonBox, QHeaderView, \ |
|
17 QTreeWidgetItem, QLineEdit, QApplication, QWidget |
|
18 |
|
19 from E5Gui import E5MessageBox |
|
20 from E5Gui.E5Application import e5App |
|
21 |
|
22 from .Ui_HgConflictsListDialog import Ui_HgConflictsListDialog |
|
23 |
|
24 import Utilities.MimeTypes |
|
25 from Globals import strToQByteArray |
|
26 |
|
27 |
|
28 class HgConflictsListDialog(QWidget, Ui_HgConflictsListDialog): |
|
29 """ |
|
30 Class implementing a dialog to show a list of files which had or still |
|
31 have conflicts. |
|
32 """ |
|
33 StatusRole = Qt.UserRole + 1 |
|
34 FilenameRole = Qt.UserRole + 2 |
|
35 |
|
36 def __init__(self, vcs, parent=None): |
|
37 """ |
|
38 Constructor |
|
39 |
|
40 @param vcs reference to the vcs object |
|
41 @param parent parent widget (QWidget) |
|
42 """ |
|
43 super(HgConflictsListDialog, self).__init__(parent) |
|
44 self.setupUi(self) |
|
45 |
|
46 self.__position = QPoint() |
|
47 |
|
48 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) |
|
49 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) |
|
50 |
|
51 self.conflictsList.headerItem().setText( |
|
52 self.conflictsList.columnCount(), "") |
|
53 self.conflictsList.header().setSortIndicator(0, Qt.AscendingOrder) |
|
54 |
|
55 self.refreshButton = self.buttonBox.addButton( |
|
56 self.tr("&Refresh"), QDialogButtonBox.ActionRole) |
|
57 self.refreshButton.setToolTip( |
|
58 self.tr("Press to refresh the list of conflicts")) |
|
59 self.refreshButton.setEnabled(False) |
|
60 |
|
61 self.vcs = vcs |
|
62 self.project = e5App().getObject("Project") |
|
63 |
|
64 self.__hgClient = vcs.getClient() |
|
65 if self.__hgClient: |
|
66 self.process = None |
|
67 else: |
|
68 self.process = QProcess() |
|
69 self.process.finished.connect(self.__procFinished) |
|
70 self.process.readyReadStandardOutput.connect(self.__readStdout) |
|
71 self.process.readyReadStandardError.connect(self.__readStderr) |
|
72 |
|
73 def closeEvent(self, e): |
|
74 """ |
|
75 Protected slot implementing a close event handler. |
|
76 |
|
77 @param e close event (QCloseEvent) |
|
78 """ |
|
79 if self.__hgClient: |
|
80 if self.__hgClient.isExecuting(): |
|
81 self.__hgClient.cancel() |
|
82 else: |
|
83 if self.process is not None and \ |
|
84 self.process.state() != QProcess.NotRunning: |
|
85 self.process.terminate() |
|
86 QTimer.singleShot(2000, self.process.kill) |
|
87 self.process.waitForFinished(3000) |
|
88 |
|
89 self.__position = self.pos() |
|
90 |
|
91 e.accept() |
|
92 |
|
93 def show(self): |
|
94 """ |
|
95 Public slot to show the dialog. |
|
96 """ |
|
97 if not self.__position.isNull(): |
|
98 self.move(self.__position) |
|
99 |
|
100 super(HgConflictsListDialog, self).show() |
|
101 |
|
102 def start(self, path): |
|
103 """ |
|
104 Public slot to start the tags command. |
|
105 |
|
106 @param path name of directory to list conflicts for (string) |
|
107 """ |
|
108 self.errorGroup.hide() |
|
109 QApplication.processEvents() |
|
110 |
|
111 self.intercept = False |
|
112 dname, fname = self.vcs.splitPath(path) |
|
113 |
|
114 # find the root of the repo |
|
115 self.__repodir = dname |
|
116 while not os.path.isdir( |
|
117 os.path.join(self.__repodir, self.vcs.adminDir)): |
|
118 self.__repodir = os.path.dirname(self.__repodir) |
|
119 if os.path.splitdrive(self.__repodir)[1] == os.sep: |
|
120 return |
|
121 |
|
122 self.activateWindow() |
|
123 self.raise_() |
|
124 |
|
125 self.conflictsList.clear() |
|
126 self.__started = True |
|
127 self.__getEntries() |
|
128 |
|
129 def __getEntries(self): |
|
130 """ |
|
131 Private method to get the conflict entries. |
|
132 """ |
|
133 args = self.vcs.initCommand("resolve") |
|
134 args.append('--list') |
|
135 |
|
136 if self.__hgClient: |
|
137 self.inputGroup.setEnabled(False) |
|
138 self.inputGroup.hide() |
|
139 |
|
140 out, err = self.__hgClient.runcommand(args) |
|
141 if err: |
|
142 self.__showError(err) |
|
143 if out: |
|
144 for line in out.splitlines(): |
|
145 self.__processOutputLine(line) |
|
146 if self.__hgClient.wasCanceled(): |
|
147 break |
|
148 self.__finish() |
|
149 else: |
|
150 self.process.kill() |
|
151 self.process.setWorkingDirectory(self.__repodir) |
|
152 |
|
153 self.process.start('hg', args) |
|
154 procStarted = self.process.waitForStarted(5000) |
|
155 if not procStarted: |
|
156 self.inputGroup.setEnabled(False) |
|
157 self.inputGroup.hide() |
|
158 E5MessageBox.critical( |
|
159 self, |
|
160 self.tr('Process Generation Error'), |
|
161 self.tr( |
|
162 'The process {0} could not be started. ' |
|
163 'Ensure, that it is in the search path.' |
|
164 ).format('hg')) |
|
165 else: |
|
166 self.inputGroup.setEnabled(True) |
|
167 self.inputGroup.show() |
|
168 |
|
169 def __finish(self): |
|
170 """ |
|
171 Private slot called when the process finished or the user pressed |
|
172 the button. |
|
173 """ |
|
174 if self.process is not None and \ |
|
175 self.process.state() != QProcess.NotRunning: |
|
176 self.process.terminate() |
|
177 QTimer.singleShot(2000, self.process.kill) |
|
178 self.process.waitForFinished(3000) |
|
179 |
|
180 QApplication.restoreOverrideCursor() |
|
181 |
|
182 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) |
|
183 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) |
|
184 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) |
|
185 |
|
186 self.inputGroup.setEnabled(False) |
|
187 self.inputGroup.hide() |
|
188 self.refreshButton.setEnabled(True) |
|
189 |
|
190 self.__resizeColumns() |
|
191 self.__resort() |
|
192 self.on_conflictsList_itemSelectionChanged() |
|
193 |
|
194 @pyqtSlot(QAbstractButton) |
|
195 def on_buttonBox_clicked(self, button): |
|
196 """ |
|
197 Private slot called by a button of the button box clicked. |
|
198 |
|
199 @param button button that was clicked (QAbstractButton) |
|
200 """ |
|
201 if button == self.buttonBox.button(QDialogButtonBox.Close): |
|
202 self.close() |
|
203 elif button == self.buttonBox.button(QDialogButtonBox.Cancel): |
|
204 if self.__hgClient: |
|
205 self.__hgClient.cancel() |
|
206 else: |
|
207 self.__finish() |
|
208 elif button == self.refreshButton: |
|
209 self.on_refreshButton_clicked() |
|
210 |
|
211 def __procFinished(self, exitCode, exitStatus): |
|
212 """ |
|
213 Private slot connected to the finished signal. |
|
214 |
|
215 @param exitCode exit code of the process (integer) |
|
216 @param exitStatus exit status of the process (QProcess.ExitStatus) |
|
217 """ |
|
218 self.__finish() |
|
219 |
|
220 def __resort(self): |
|
221 """ |
|
222 Private method to resort the tree. |
|
223 """ |
|
224 self.conflictsList.sortItems( |
|
225 self.conflictsList.sortColumn(), |
|
226 self.conflictsList.header().sortIndicatorOrder()) |
|
227 |
|
228 def __resizeColumns(self): |
|
229 """ |
|
230 Private method to resize the list columns. |
|
231 """ |
|
232 self.conflictsList.header().resizeSections( |
|
233 QHeaderView.ResizeToContents) |
|
234 self.conflictsList.header().setStretchLastSection(True) |
|
235 |
|
236 def __generateItem(self, status, name): |
|
237 """ |
|
238 Private method to generate a tag item in the tag list. |
|
239 |
|
240 @param status status of the file (string) |
|
241 @param name name of the file (string) |
|
242 """ |
|
243 itm = QTreeWidgetItem(self.conflictsList) |
|
244 if status == "U": |
|
245 itm.setText(0, self.tr("Unresolved")) |
|
246 elif status == "R": |
|
247 itm.setText(0, self.tr("Resolved")) |
|
248 else: |
|
249 itm.setText(0, self.tr("Unknown Status")) |
|
250 itm.setText(1, name) |
|
251 |
|
252 itm.setData(0, self.StatusRole, status) |
|
253 itm.setData(0, self.FilenameRole, self.project.getAbsolutePath(name)) |
|
254 |
|
255 def __readStdout(self): |
|
256 """ |
|
257 Private slot to handle the readyReadStdout signal. |
|
258 |
|
259 It reads the output of the process, formats it and inserts it into |
|
260 the contents pane. |
|
261 """ |
|
262 self.process.setReadChannel(QProcess.StandardOutput) |
|
263 |
|
264 while self.process.canReadLine(): |
|
265 s = str(self.process.readLine(), self.vcs.getEncoding(), |
|
266 'replace').strip() |
|
267 self.__processOutputLine(s) |
|
268 |
|
269 def __processOutputLine(self, line): |
|
270 """ |
|
271 Private method to process the lines of output. |
|
272 |
|
273 @param line output line to be processed (string) |
|
274 """ |
|
275 status, filename = line.strip().split(None, 1) |
|
276 self.__generateItem(status, filename) |
|
277 |
|
278 @pyqtSlot() |
|
279 def on_refreshButton_clicked(self): |
|
280 """ |
|
281 Private slot to refresh the log. |
|
282 """ |
|
283 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) |
|
284 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) |
|
285 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) |
|
286 |
|
287 self.inputGroup.setEnabled(True) |
|
288 self.inputGroup.show() |
|
289 self.refreshButton.setEnabled(False) |
|
290 self.start(self.__repodir) |
|
291 |
|
292 def __readStderr(self): |
|
293 """ |
|
294 Private slot to handle the readyReadStderr signal. |
|
295 |
|
296 It reads the error output of the process and inserts it into the |
|
297 error pane. |
|
298 """ |
|
299 if self.process is not None: |
|
300 s = str(self.process.readAllStandardError(), |
|
301 self.vcs.getEncoding(), 'replace') |
|
302 self.__showError(s) |
|
303 |
|
304 def __showError(self, out): |
|
305 """ |
|
306 Private slot to show some error. |
|
307 |
|
308 @param out error to be shown (string) |
|
309 """ |
|
310 self.errorGroup.show() |
|
311 self.errors.insertPlainText(out) |
|
312 self.errors.ensureCursorVisible() |
|
313 |
|
314 def on_passwordCheckBox_toggled(self, isOn): |
|
315 """ |
|
316 Private slot to handle the password checkbox toggled. |
|
317 |
|
318 @param isOn flag indicating the status of the check box (boolean) |
|
319 """ |
|
320 if isOn: |
|
321 self.input.setEchoMode(QLineEdit.Password) |
|
322 else: |
|
323 self.input.setEchoMode(QLineEdit.Normal) |
|
324 |
|
325 @pyqtSlot() |
|
326 def on_sendButton_clicked(self): |
|
327 """ |
|
328 Private slot to send the input to the subversion process. |
|
329 """ |
|
330 inputTxt = self.input.text() |
|
331 inputTxt += os.linesep |
|
332 |
|
333 if self.passwordCheckBox.isChecked(): |
|
334 self.errors.insertPlainText(os.linesep) |
|
335 self.errors.ensureCursorVisible() |
|
336 else: |
|
337 self.errors.insertPlainText(inputTxt) |
|
338 self.errors.ensureCursorVisible() |
|
339 |
|
340 self.process.write(strToQByteArray(inputTxt)) |
|
341 |
|
342 self.passwordCheckBox.setChecked(False) |
|
343 self.input.clear() |
|
344 |
|
345 def on_input_returnPressed(self): |
|
346 """ |
|
347 Private slot to handle the press of the return key in the input field. |
|
348 """ |
|
349 self.intercept = True |
|
350 self.on_sendButton_clicked() |
|
351 |
|
352 def keyPressEvent(self, evt): |
|
353 """ |
|
354 Protected slot to handle a key press event. |
|
355 |
|
356 @param evt the key press event (QKeyEvent) |
|
357 """ |
|
358 if self.intercept: |
|
359 self.intercept = False |
|
360 evt.accept() |
|
361 return |
|
362 super(HgConflictsListDialog, self).keyPressEvent(evt) |
|
363 |
|
364 @pyqtSlot(QTreeWidgetItem, int) |
|
365 def on_conflictsList_itemDoubleClicked(self, item, column): |
|
366 """ |
|
367 Private slot to open the double clicked entry. |
|
368 |
|
369 @param item reference to the double clicked item (QTreeWidgetItem) |
|
370 @param column column that was double clicked (integer) |
|
371 """ |
|
372 self.on_editButton_clicked() |
|
373 |
|
374 @pyqtSlot() |
|
375 def on_conflictsList_itemSelectionChanged(self): |
|
376 """ |
|
377 Private slot to handle a change of selected conflict entries. |
|
378 """ |
|
379 selectedCount = len(self.conflictsList.selectedItems()) |
|
380 unresolved = resolved = 0 |
|
381 for itm in self.conflictsList.selectedItems(): |
|
382 status = itm.data(0, self.StatusRole) |
|
383 if status == "U": |
|
384 unresolved += 1 |
|
385 elif status == "R": |
|
386 resolved += 1 |
|
387 |
|
388 self.resolvedButton.setEnabled(unresolved > 0) |
|
389 self.unresolvedButton.setEnabled(resolved > 0) |
|
390 self.reMergeButton.setEnabled(unresolved > 0) |
|
391 self.editButton.setEnabled( |
|
392 selectedCount == 1 and |
|
393 Utilities.MimeTypes.isTextFile( |
|
394 self.conflictsList.selectedItems()[0].data( |
|
395 0, self.FilenameRole))) |
|
396 |
|
397 @pyqtSlot() |
|
398 def on_resolvedButton_clicked(self): |
|
399 """ |
|
400 Private slot to mark the selected entries as resolved. |
|
401 """ |
|
402 names = [ |
|
403 itm.data(0, self.FilenameRole) |
|
404 for itm in self.conflictsList.selectedItems() |
|
405 if itm.data(0, self.StatusRole) == "U" |
|
406 ] |
|
407 if names: |
|
408 self.vcs.hgResolved(names) |
|
409 self.on_refreshButton_clicked() |
|
410 |
|
411 @pyqtSlot() |
|
412 def on_unresolvedButton_clicked(self): |
|
413 """ |
|
414 Private slot to mark the selected entries as unresolved. |
|
415 """ |
|
416 names = [ |
|
417 itm.data(0, self.FilenameRole) |
|
418 for itm in self.conflictsList.selectedItems() |
|
419 if itm.data(0, self.StatusRole) == "R" |
|
420 ] |
|
421 if names: |
|
422 self.vcs.hgResolved(names, unresolve=True) |
|
423 self.on_refreshButton_clicked() |
|
424 |
|
425 @pyqtSlot() |
|
426 def on_reMergeButton_clicked(self): |
|
427 """ |
|
428 Private slot to re-merge the selected entries. |
|
429 """ |
|
430 names = [ |
|
431 itm.data(0, self.FilenameRole) |
|
432 for itm in self.conflictsList.selectedItems() |
|
433 if itm.data(0, self.StatusRole) == "U" |
|
434 ] |
|
435 if names: |
|
436 self.vcs.hgReMerge(names) |
|
437 |
|
438 @pyqtSlot() |
|
439 def on_editButton_clicked(self): |
|
440 """ |
|
441 Private slot to open the selected file in an editor. |
|
442 """ |
|
443 itm = self.conflictsList.selectedItems()[0] |
|
444 filename = itm.data(0, self.FilenameRole) |
|
445 if Utilities.MimeTypes.isTextFile(filename): |
|
446 e5App().getObject("ViewManager").getEditor(filename) |