/tortoisehg/hgqt/rejects.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 252 lines · 215 code · 31 blank · 6 comment · 20 complexity · f59d4ebfda04ab1e7be1cde2922e213d MD5 · raw file

  1. # rejects.py - TortoiseHg patch reject editor
  2. #
  3. # Copyright 2011 Steve Borho <steve@borho.org>
  4. #
  5. # This software may be used and distributed according to the terms
  6. # of the GNU General Public License, incorporated herein by reference.
  7. import cStringIO
  8. import os
  9. from mercurial import hg, util, patch, commands
  10. from hgext import record
  11. from tortoisehg.util import hglib
  12. from tortoisehg.util.patchctx import patchctx
  13. from tortoisehg.hgqt.i18n import _
  14. from tortoisehg.hgqt import qtlib, qscilib, lexers
  15. from PyQt4.QtCore import *
  16. from PyQt4.QtGui import *
  17. from PyQt4 import Qsci
  18. qsci = Qsci.QsciScintilla
  19. class RejectsDialog(QDialog):
  20. def __init__(self, path, parent):
  21. super(RejectsDialog, self).__init__(parent)
  22. self.setWindowTitle(_('Merge rejected patch chunks into %s') %
  23. hglib.tounicode(path))
  24. self.path = path
  25. self.setLayout(QVBoxLayout())
  26. editor = qscilib.Scintilla()
  27. editor.setBraceMatching(qsci.SloppyBraceMatch)
  28. editor.setMarginLineNumbers(1, True)
  29. editor.setMarginWidth(1, '000')
  30. editor.setFolding(qsci.BoxedTreeFoldStyle)
  31. editor.installEventFilter(qscilib.KeyPressInterceptor(self))
  32. editor.setContextMenuPolicy(Qt.CustomContextMenu)
  33. editor.customContextMenuRequested.connect(self.menuRequested)
  34. self.baseLineColor = editor.markerDefine(qsci.Background, -1)
  35. editor.setMarkerBackgroundColor(QColor('lightblue'), self.baseLineColor)
  36. self.layout().addWidget(editor, 3)
  37. searchbar = qscilib.SearchToolBar(self, hidable=True)
  38. searchbar.searchRequested.connect(editor.find)
  39. searchbar.conditionChanged.connect(editor.highlightText)
  40. searchbar.hide()
  41. def showsearchbar():
  42. searchbar.show()
  43. searchbar.setFocus(Qt.OtherFocusReason)
  44. QShortcut(QKeySequence.Find, self, showsearchbar)
  45. self.layout().addWidget(searchbar)
  46. hbox = QHBoxLayout()
  47. hbox.setContentsMargins(2, 2, 2, 2)
  48. self.layout().addLayout(hbox, 1)
  49. self.chunklist = QListWidget(self)
  50. self.updating = True
  51. self.chunklist.currentRowChanged.connect(self.showChunk)
  52. hbox.addWidget(self.chunklist, 1)
  53. bvbox = QVBoxLayout()
  54. bvbox.setContentsMargins(2, 2, 2, 2)
  55. self.resolved = tb = QToolButton()
  56. tb.setIcon(qtlib.geticon('success'))
  57. tb.setToolTip(_('Mark this chunk as resolved, goto next unresolved'))
  58. tb.pressed.connect(self.resolveCurrentChunk)
  59. self.unresolved = tb = QToolButton()
  60. tb.setIcon(qtlib.geticon('warning'))
  61. tb.setToolTip(_('Mark this chunk as unresolved'))
  62. tb.pressed.connect(self.unresolveCurrentChunk)
  63. bvbox.addStretch(1)
  64. bvbox.addWidget(self.resolved, 0)
  65. bvbox.addWidget(self.unresolved, 0)
  66. bvbox.addStretch(1)
  67. hbox.addLayout(bvbox, 0)
  68. self.editor = editor
  69. self.rejectbrowser = RejectBrowser(self)
  70. hbox.addWidget(self.rejectbrowser, 5)
  71. BB = QDialogButtonBox
  72. bb = QDialogButtonBox(BB.Save|BB.Cancel)
  73. bb.accepted.connect(self.accept)
  74. bb.rejected.connect(self.reject)
  75. self.layout().addWidget(bb)
  76. self.saveButton = bb.button(BB.Save)
  77. s = QSettings()
  78. self.restoreGeometry(s.value('rejects/geometry').toByteArray())
  79. self.editor.loadSettings(s, 'rejects/editor')
  80. self.rejectbrowser.loadSettings(s, 'rejects/rejbrowse')
  81. f = QFile(path)
  82. f.open(QIODevice.ReadOnly)
  83. editor.read(f)
  84. editor.setModified(False)
  85. lexer = lexers.get_lexer(path, f.readData(1024), self)
  86. editor.setLexer(lexer)
  87. buf = cStringIO.StringIO()
  88. try:
  89. buf.write('diff -r aaaaaaaaaaaa -r bbbbbbbbbbb %s\n' % path)
  90. buf.write(open(path + '.rej', 'r').read())
  91. buf.seek(0)
  92. except IOError, e:
  93. pass
  94. try:
  95. header = record.parsepatch(buf)[0]
  96. self.chunks = header.hunks
  97. except (patch.PatchError, IndexError), e:
  98. self.chunks = []
  99. for chunk in self.chunks:
  100. chunk.resolved = False
  101. self.updateChunkList()
  102. self.saveButton.setDisabled(len(self.chunks))
  103. self.resolved.setDisabled(True)
  104. self.unresolved.setDisabled(True)
  105. QTimer.singleShot(0, lambda: self.chunklist.setCurrentRow(0))
  106. def menuRequested(self, point):
  107. point = self.editor.mapToGlobal(point)
  108. return self.editor.createStandardContextMenu().exec_(point)
  109. def updateChunkList(self):
  110. self.updating = True
  111. self.chunklist.clear()
  112. for chunk in self.chunks:
  113. self.chunklist.addItem('@@ %d %s' % (chunk.fromline,
  114. chunk.resolved and '(resolved)' or '(unresolved)'))
  115. self.updating = False
  116. @pyqtSlot()
  117. def resolveCurrentChunk(self):
  118. row = self.chunklist.currentRow()
  119. chunk = self.chunks[row]
  120. chunk.resolved = True
  121. self.updateChunkList()
  122. for i, chunk in enumerate(self.chunks):
  123. if not chunk.resolved:
  124. self.chunklist.setCurrentRow(i)
  125. return
  126. else:
  127. self.chunklist.setCurrentRow(row)
  128. self.saveButton.setEnabled(True)
  129. @pyqtSlot()
  130. def unresolveCurrentChunk(self):
  131. row = self.chunklist.currentRow()
  132. chunk = self.chunks[row]
  133. chunk.resolved = False
  134. self.updateChunkList()
  135. self.chunklist.setCurrentRow(row)
  136. self.saveButton.setEnabled(False)
  137. @pyqtSlot(int)
  138. def showChunk(self, row):
  139. if row == -1 or self.updating:
  140. return
  141. buf = cStringIO.StringIO()
  142. chunk = self.chunks[row]
  143. chunk.write(buf)
  144. self.rejectbrowser.showChunk(buf.getvalue().splitlines()[1:])
  145. self.editor.setCursorPosition(chunk.fromline-1, 0)
  146. self.editor.ensureLineVisible(chunk.fromline-1)
  147. self.editor.markerDeleteAll(-1)
  148. self.editor.markerAdd(chunk.fromline-1, self.baseLineColor)
  149. self.resolved.setEnabled(not chunk.resolved)
  150. self.unresolved.setEnabled(chunk.resolved)
  151. def saveSettings(self):
  152. s = QSettings()
  153. s.setValue('rejects/geometry', self.saveGeometry())
  154. self.editor.saveSettings(s, 'rejects/editor')
  155. self.rejectbrowser.saveSettings(s, 'rejects/rejbrowse')
  156. def accept(self):
  157. f = QFile(self.path)
  158. f.open(QIODevice.WriteOnly)
  159. self.editor.write(f)
  160. self.saveSettings()
  161. super(RejectsDialog, self).accept()
  162. def reject(self):
  163. self.saveSettings()
  164. super(RejectsDialog, self).reject()
  165. class RejectBrowser(qscilib.Scintilla):
  166. 'Display a rejected diff hunk in an easily copy/pasted format'
  167. def __init__(self, parent):
  168. super(RejectBrowser, self).__init__(parent)
  169. self.setFrameStyle(0)
  170. self.setReadOnly(True)
  171. self.setUtf8(True)
  172. self.installEventFilter(qscilib.KeyPressInterceptor(self))
  173. self.setContextMenuPolicy(Qt.CustomContextMenu)
  174. self.customContextMenuRequested.connect(self.menuRequested)
  175. self.setCaretLineVisible(False)
  176. self.setMarginType(1, qsci.SymbolMargin)
  177. self.setMarginLineNumbers(1, False)
  178. self.setMarginWidth(1, QFontMetrics(self.font()).width('XX'))
  179. self.setMarginSensitivity(1, True)
  180. self.addedMark = self.markerDefine(qsci.Plus, -1)
  181. self.removedMark = self.markerDefine(qsci.Minus, -1)
  182. self.addedColor = self.markerDefine(qsci.Background, -1)
  183. self.removedColor = self.markerDefine(qsci.Background, -1)
  184. self.setMarkerBackgroundColor(QColor('lightgreen'), self.addedColor)
  185. self.setMarkerBackgroundColor(QColor('cyan'), self.removedColor)
  186. mask = (1 << self.addedMark) | (1 << self.removedMark) | \
  187. (1 << self.addedColor) | (1 << self.removedColor)
  188. self.setMarginMarkerMask(1, mask)
  189. lexer = lexers.get_diff_lexer(self)
  190. self.setLexer(lexer)
  191. def menuRequested(self, point):
  192. point = self.mapToGlobal(point)
  193. return self.createStandardContextMenu().exec_(point)
  194. def showChunk(self, lines):
  195. utext = []
  196. added = []
  197. removed = []
  198. for i, line in enumerate(lines):
  199. utext.append(hglib.tounicode(line[1:]))
  200. if line[0] == '+':
  201. added.append(i)
  202. elif line[0] == '-':
  203. removed.append(i)
  204. self.markerDeleteAll(-1)
  205. self.setText(u'\n'.join(utext))
  206. for i in added:
  207. self.markerAdd(i, self.addedMark)
  208. self.markerAdd(i, self.addedColor)
  209. for i in removed:
  210. self.markerAdd(i, self.removedMark)
  211. self.markerAdd(i, self.removedColor)
  212. def run(ui, *pats, **opts):
  213. if len(pats) != 1:
  214. qtlib.ErrorMsgBox(_('Filename required'),
  215. _('You must provide the path to a file'))
  216. import sys; sys.exit()
  217. path = pats[0]
  218. if path.endswith('.rej'):
  219. path = path[:-4]
  220. dlg = RejectsDialog(path, None)
  221. return dlg