PageRenderTime 43ms CodeModel.GetById 17ms app.highlight 22ms RepoModel.GetById 1ms app.codeStats 0ms

/tortoisehg/hgqt/rejects.py

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