eric6/Plugins/VcsPlugins/vcsMercurial/ShelveExtension/HgShelveBrowserDialog.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7192
a22eee00b052
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2014 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing Mercurial shelve browser dialog.
8 """
9
10 from __future__ import unicode_literals
11 try:
12 str = unicode
13 except NameError:
14 pass
15
16 import os
17
18 from PyQt5.QtCore import pyqtSlot, Qt, QPoint, QProcess, QTimer
19 from PyQt5.QtGui import QCursor
20 from PyQt5.QtWidgets import QWidget, QDialogButtonBox, QTreeWidgetItem, \
21 QAbstractButton, QMenu, QHeaderView, QApplication, QLineEdit
22
23 from E5Gui import E5MessageBox
24
25 from .Ui_HgShelveBrowserDialog import Ui_HgShelveBrowserDialog
26
27 from Globals import strToQByteArray
28
29
30 class HgShelveBrowserDialog(QWidget, Ui_HgShelveBrowserDialog):
31 """
32 Class implementing Mercurial shelve browser dialog.
33 """
34 NameColumn = 0
35 AgeColumn = 1
36 MessageColumn = 2
37
38 def __init__(self, vcs, parent=None):
39 """
40 Constructor
41
42 @param vcs reference to the vcs object
43 @param parent parent widget (QWidget)
44 """
45 super(HgShelveBrowserDialog, self).__init__(parent)
46 self.setupUi(self)
47
48 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
49 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
50
51 self.__position = QPoint()
52
53 self.__fileStatisticsRole = Qt.UserRole
54 self.__totalStatisticsRole = Qt.UserRole + 1
55
56 self.shelveList.header().setSortIndicator(0, Qt.AscendingOrder)
57
58 self.refreshButton = self.buttonBox.addButton(
59 self.tr("&Refresh"), QDialogButtonBox.ActionRole)
60 self.refreshButton.setToolTip(
61 self.tr("Press to refresh the list of shelves"))
62 self.refreshButton.setEnabled(False)
63
64 self.vcs = vcs
65 self.__hgClient = vcs.getClient()
66 self.__resetUI()
67
68 if self.__hgClient:
69 self.process = None
70 else:
71 self.process = QProcess()
72 self.process.finished.connect(self.__procFinished)
73 self.process.readyReadStandardOutput.connect(self.__readStdout)
74 self.process.readyReadStandardError.connect(self.__readStderr)
75
76 self.__contextMenu = QMenu()
77 self.__unshelveAct = self.__contextMenu.addAction(
78 self.tr("Restore selected shelve"), self.__unshelve)
79 self.__deleteAct = self.__contextMenu.addAction(
80 self.tr("Delete selected shelves"), self.__deleteShelves)
81 self.__contextMenu.addAction(
82 self.tr("Delete all shelves"), self.__cleanupShelves)
83
84 def closeEvent(self, e):
85 """
86 Protected slot implementing a close event handler.
87
88 @param e close event (QCloseEvent)
89 """
90 if self.__hgClient:
91 if self.__hgClient.isExecuting():
92 self.__hgClient.cancel()
93 else:
94 if self.process is not None and \
95 self.process.state() != QProcess.NotRunning:
96 self.process.terminate()
97 QTimer.singleShot(2000, self.process.kill)
98 self.process.waitForFinished(3000)
99
100 self.__position = self.pos()
101
102 e.accept()
103
104 def show(self):
105 """
106 Public slot to show the dialog.
107 """
108 if not self.__position.isNull():
109 self.move(self.__position)
110 self.__resetUI()
111
112 super(HgShelveBrowserDialog, self).show()
113
114 def __resetUI(self):
115 """
116 Private method to reset the user interface.
117 """
118 self.shelveList.clear()
119
120 def __resizeColumnsShelves(self):
121 """
122 Private method to resize the shelve list columns.
123 """
124 self.shelveList.header().resizeSections(QHeaderView.ResizeToContents)
125 self.shelveList.header().setStretchLastSection(True)
126
127 def __generateShelveEntry(self, name, age, message, fileStatistics,
128 totals):
129 """
130 Private method to generate the shelve items.
131
132 @param name name of the shelve (string)
133 @param age age of the shelve (string)
134 @param message shelve message (string)
135 @param fileStatistics per file change statistics (tuple of
136 four strings with file name, number of changes, number of
137 added lines and number of deleted lines)
138 @param totals overall statistics (tuple of three strings with
139 number of changed files, number of added lines and number
140 of deleted lines)
141 """
142 itm = QTreeWidgetItem(self.shelveList, [name, age, message])
143 itm.setData(0, self.__fileStatisticsRole, fileStatistics)
144 itm.setData(0, self.__totalStatisticsRole, totals)
145
146 def __getShelveEntries(self):
147 """
148 Private method to retrieve the list of shelves.
149 """
150 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
151 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
152 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
153 QApplication.processEvents()
154
155 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
156 QApplication.processEvents()
157
158 self.buf = []
159 self.errors.clear()
160 self.intercept = False
161
162 args = self.vcs.initCommand("shelve")
163 args.append("--list")
164 args.append("--stat")
165
166 if self.__hgClient:
167 self.inputGroup.setEnabled(False)
168 self.inputGroup.hide()
169
170 out, err = self.__hgClient.runcommand(args)
171 self.buf = out.splitlines(True)
172 if err:
173 self.__showError(err)
174 self.__processBuffer()
175 self.__finish()
176 else:
177 self.process.kill()
178
179 self.process.setWorkingDirectory(self.repodir)
180
181 self.inputGroup.setEnabled(True)
182 self.inputGroup.show()
183
184 self.process.start('hg', args)
185 procStarted = self.process.waitForStarted(5000)
186 if not procStarted:
187 self.inputGroup.setEnabled(False)
188 self.inputGroup.hide()
189 E5MessageBox.critical(
190 self,
191 self.tr('Process Generation Error'),
192 self.tr(
193 'The process {0} could not be started. '
194 'Ensure, that it is in the search path.'
195 ).format('hg'))
196
197 def start(self, projectDir):
198 """
199 Public slot to start the hg shelve command.
200
201 @param projectDir name of the project directory (string)
202 """
203 self.errorGroup.hide()
204 QApplication.processEvents()
205
206 self.__projectDir = projectDir
207
208 # find the root of the repo
209 self.repodir = self.__projectDir
210 while not os.path.isdir(os.path.join(self.repodir, self.vcs.adminDir)):
211 self.repodir = os.path.dirname(self.repodir)
212 if os.path.splitdrive(self.repodir)[1] == os.sep:
213 return
214
215 self.activateWindow()
216 self.raise_()
217
218 self.shelveList.clear()
219 self.__started = True
220 self.__getShelveEntries()
221
222 def __procFinished(self, exitCode, exitStatus):
223 """
224 Private slot connected to the finished signal.
225
226 @param exitCode exit code of the process (integer)
227 @param exitStatus exit status of the process (QProcess.ExitStatus)
228 """
229 self.__processBuffer()
230 self.__finish()
231
232 def __finish(self):
233 """
234 Private slot called when the process finished or the user pressed
235 the button.
236 """
237 if self.process is not None and \
238 self.process.state() != QProcess.NotRunning:
239 self.process.terminate()
240 QTimer.singleShot(2000, self.process.kill)
241 self.process.waitForFinished(3000)
242
243 QApplication.restoreOverrideCursor()
244
245 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
246 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
247 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
248
249 self.inputGroup.setEnabled(False)
250 self.inputGroup.hide()
251 self.refreshButton.setEnabled(True)
252
253 def __processBuffer(self):
254 """
255 Private method to process the buffered output of the hg shelve command.
256 """
257 lastWasFileStats = False
258 firstLine = True
259 itemData = {}
260 for line in self.buf:
261 if firstLine:
262 name, line = line.split("(", 1)
263 age, message = line.split(")", 1)
264 itemData["name"] = name.strip()
265 itemData["age"] = age.strip()
266 itemData["message"] = message.strip()
267 itemData["files"] = []
268 firstLine = False
269 elif '|' in line:
270 # file stats: foo.py | 3 ++-
271 file, changes = line.strip().split("|", 1)
272 if changes.strip().endswith(("+", "-")):
273 total, addDelete = changes.strip().split(None, 1)
274 additions = str(addDelete.count("+"))
275 deletions = str(addDelete.count("-"))
276 else:
277 total = changes.strip()
278 additions = '0'
279 deletions = '0'
280 itemData["files"].append((file, total, additions, deletions))
281 lastWasFileStats = True
282 elif lastWasFileStats:
283 # summary line
284 # 2 files changed, 15 insertions(+), 1 deletions(-)
285 total, added, deleted = line.strip().split(",", 2)
286 total = total.split()[0]
287 added = added.split()[0]
288 deleted = deleted.split()[0]
289 itemData["summary"] = (total, added, deleted)
290
291 self.__generateShelveEntry(
292 itemData["name"], itemData["age"], itemData["message"],
293 itemData["files"], itemData["summary"])
294
295 lastWasFileStats = False
296 firstLine = True
297 itemData = {}
298
299 self.__resizeColumnsShelves()
300
301 if self.__started:
302 self.shelveList.setCurrentItem(self.shelveList.topLevelItem(0))
303 self.__started = False
304
305 def __readStdout(self):
306 """
307 Private slot to handle the readyReadStandardOutput signal.
308
309 It reads the output of the process and inserts it into a buffer.
310 """
311 self.process.setReadChannel(QProcess.StandardOutput)
312
313 while self.process.canReadLine():
314 line = str(self.process.readLine(), self.vcs.getEncoding(),
315 'replace')
316 self.buf.append(line)
317
318 def __readStderr(self):
319 """
320 Private slot to handle the readyReadStandardError signal.
321
322 It reads the error output of the process and inserts it into the
323 error pane.
324 """
325 if self.process is not None:
326 s = str(self.process.readAllStandardError(),
327 self.vcs.getEncoding(), 'replace')
328 self.__showError(s)
329
330 def __showError(self, out):
331 """
332 Private slot to show some error.
333
334 @param out error to be shown (string)
335 """
336 self.errorGroup.show()
337 self.errors.insertPlainText(out)
338 self.errors.ensureCursorVisible()
339
340 @pyqtSlot(QAbstractButton)
341 def on_buttonBox_clicked(self, button):
342 """
343 Private slot called by a button of the button box clicked.
344
345 @param button button that was clicked (QAbstractButton)
346 """
347 if button == self.buttonBox.button(QDialogButtonBox.Close):
348 self.close()
349 elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
350 self.cancelled = True
351 if self.__hgClient:
352 self.__hgClient.cancel()
353 else:
354 self.__finish()
355 elif button == self.refreshButton:
356 self.on_refreshButton_clicked()
357
358 @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
359 def on_shelveList_currentItemChanged(self, current, previous):
360 """
361 Private slot called, when the current item of the shelve list changes.
362
363 @param current reference to the new current item (QTreeWidgetItem)
364 @param previous reference to the old current item (QTreeWidgetItem)
365 """
366 self.statisticsList.clear()
367 if current:
368 for dataSet in current.data(0, self.__fileStatisticsRole):
369 QTreeWidgetItem(self.statisticsList, list(dataSet))
370 self.statisticsList.header().resizeSections(
371 QHeaderView.ResizeToContents)
372 self.statisticsList.header().setStretchLastSection(True)
373
374 totals = current.data(0, self.__totalStatisticsRole)
375 self.filesLabel.setText(
376 self.tr("%n file(s) changed", None, int(totals[0])))
377 self.insertionsLabel.setText(
378 self.tr("%n line(s) inserted", None, int(totals[1])))
379 self.deletionsLabel.setText(
380 self.tr("%n line(s) deleted", None, int(totals[2])))
381 else:
382 self.filesLabel.setText("")
383 self.insertionsLabel.setText("")
384 self.deletionsLabel.setText("")
385
386 @pyqtSlot(QPoint)
387 def on_shelveList_customContextMenuRequested(self, pos):
388 """
389 Private slot to show the context menu of the shelve list.
390
391 @param pos position of the mouse pointer (QPoint)
392 """
393 selectedItemsCount = len(self.shelveList.selectedItems())
394 self.__unshelveAct.setEnabled(selectedItemsCount == 1)
395 self.__deleteAct.setEnabled(selectedItemsCount > 0)
396
397 self.__contextMenu.popup(self.mapToGlobal(pos))
398
399 @pyqtSlot()
400 def on_refreshButton_clicked(self):
401 """
402 Private slot to refresh the list of shelves.
403 """
404 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
405 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
406 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
407
408 self.inputGroup.setEnabled(True)
409 self.inputGroup.show()
410 self.refreshButton.setEnabled(False)
411
412 self.start(self.__projectDir)
413
414 def on_passwordCheckBox_toggled(self, isOn):
415 """
416 Private slot to handle the password checkbox toggled.
417
418 @param isOn flag indicating the status of the check box (boolean)
419 """
420 if isOn:
421 self.input.setEchoMode(QLineEdit.Password)
422 else:
423 self.input.setEchoMode(QLineEdit.Normal)
424
425 @pyqtSlot()
426 def on_sendButton_clicked(self):
427 """
428 Private slot to send the input to the mercurial process.
429 """
430 inputTxt = self.input.text()
431 inputTxt += os.linesep
432
433 if self.passwordCheckBox.isChecked():
434 self.errors.insertPlainText(os.linesep)
435 self.errors.ensureCursorVisible()
436 else:
437 self.errors.insertPlainText(inputTxt)
438 self.errors.ensureCursorVisible()
439 self.errorGroup.show()
440
441 self.process.write(strToQByteArray(inputTxt))
442
443 self.passwordCheckBox.setChecked(False)
444 self.input.clear()
445
446 def on_input_returnPressed(self):
447 """
448 Private slot to handle the press of the return key in the input field.
449 """
450 self.intercept = True
451 self.on_sendButton_clicked()
452
453 def keyPressEvent(self, evt):
454 """
455 Protected slot to handle a key press event.
456
457 @param evt the key press event (QKeyEvent)
458 """
459 if self.intercept:
460 self.intercept = False
461 evt.accept()
462 return
463 super(HgShelveBrowserDialog, self).keyPressEvent(evt)
464
465 def __unshelve(self):
466 """
467 Private slot to restore the selected shelve of changes.
468 """
469 itm = self.shelveList.selectedItems()[0]
470 if itm is not None:
471 name = itm.text(self.NameColumn)
472 self.vcs.getExtensionObject("shelve")\
473 .hgUnshelve(self.__projectDir, shelveName=name)
474 self.on_refreshButton_clicked()
475
476 def __deleteShelves(self):
477 """
478 Private slot to delete the selected shelves.
479 """
480 shelveNames = []
481 for itm in self.shelveList.selectedItems():
482 shelveNames.append(itm.text(self.NameColumn))
483 if shelveNames:
484 self.vcs.getExtensionObject("shelve")\
485 .hgDeleteShelves(self.__projectDir, shelveNames=shelveNames)
486 self.on_refreshButton_clicked()
487
488 def __cleanupShelves(self):
489 """
490 Private slot to delete all shelves.
491 """
492 self.vcs.getExtensionObject("shelve")\
493 .hgCleanupShelves(self.__projectDir)
494 self.on_refreshButton_clicked()

eric ide

mercurial