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