PageRenderTime 54ms CodeModel.GetById 12ms app.highlight 35ms RepoModel.GetById 3ms app.codeStats 0ms

/tortoisehg/hgqt/filedialogs.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 551 lines | 502 code | 20 blank | 29 comment | 26 complexity | 6c07dc8693f09d6cef064893c4758b04 MD5 | raw file
  1# Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE).
  2# http://www.logilab.fr/ -- mailto:contact@logilab.fr
  3#
  4# This program is free software; you can redistribute it and/or modify it under
  5# the terms of the GNU General Public License as published by the Free Software
  6# Foundation; either version 2 of the License, or (at your option) any later
  7# version.
  8#
  9# This program is distributed in the hope that it will be useful, but WITHOUT
 10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 11# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 12#
 13# You should have received a copy of the GNU General Public License along with
 14# this program; if not, write to the Free Software Foundation, Inc.,
 15# 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 16"""
 17Qt4 dialogs to display hg revisions of a file
 18"""
 19
 20import difflib
 21
 22from PyQt4.QtCore import *
 23from PyQt4.QtGui import *
 24from PyQt4.Qsci import QsciScintilla
 25
 26from mercurial import hg
 27from tortoisehg.util import hglib
 28from tortoisehg.hgqt.i18n import _
 29from tortoisehg.hgqt.qtlib import geticon, getfont
 30from tortoisehg.hgqt.filerevmodel import FileRevModel
 31from tortoisehg.hgqt.blockmatcher import BlockList, BlockMatch
 32from tortoisehg.hgqt.lexers import get_lexer
 33from tortoisehg.hgqt.fileview import HgFileView
 34from tortoisehg.hgqt.repoview import HgRepoView
 35from tortoisehg.hgqt.revpanel import RevPanelWidget
 36
 37sides = ('left', 'right')
 38otherside = {'left': 'right', 'right': 'left'}
 39
 40class _AbstractFileDialog(QMainWindow):
 41    def __init__(self, repo, filename, repoviewer=None):
 42        QMainWindow.__init__(self)
 43        self.repo = repo
 44
 45        self._font = getfont('fontdiff').font()
 46        self.setupUi(self)
 47        self.setRepoViewer(repoviewer)
 48        self._show_rev = None
 49
 50        self.filename = filename
 51        self.findLexer()
 52
 53        self.createActions()
 54        self.setupToolbars()
 55
 56        self.setupViews()
 57        self.setupModels()
 58
 59    def setRepoViewer(self, repoviewer=None):
 60        self.repoviewer = repoviewer
 61        if repoviewer:
 62            repoviewer.finished.connect(lambda x: self.setRepoViewer(None))
 63
 64    def reload(self):
 65        'Reload toolbar action handler'
 66        self.repo.thginvalidate()
 67        self.setupModels()
 68
 69    def findLexer(self):
 70        # try to find a lexer for our file.
 71        f = self.repo.file(self.filename)
 72        head = f.heads()[0]
 73        if f.size(f.rev(head)) < 1e6:
 74            data = f.read(head)
 75        else:
 76            data = '' # too big
 77        lexer = get_lexer(self.filename, data, self)
 78        if lexer:
 79            lexer.setDefaultFont(self._font)
 80            lexer.setFont(self._font)
 81        self.lexer = lexer
 82
 83    def revisionActivated(self, rev):
 84        """
 85        Callback called when a revision is double-clicked in the revisions table
 86        """
 87        if self.repoviewer is None:
 88            # prevent recursive import
 89            from workbench import Workbench
 90            self.repoviewer = Workbench()
 91        self.repoviewer.show()
 92        self.repoviewer.activateWindow()
 93        self.repoviewer.raise_()
 94        self.repoviewer.showRepo(hglib.tounicode(self.repo.root))
 95        self.repoviewer.goto(self.repo.root, rev)
 96
 97class FileLogDialog(_AbstractFileDialog):
 98    """
 99    A dialog showing a revision graph for a file.
100    """
101    def __init__(self, repo, filename, repoviewer=None):
102        super(FileLogDialog, self).__init__(repo, filename, repoviewer)
103        self._readSettings()
104
105    def closeEvent(self, event):
106        self._writeSettings()
107        super(FileLogDialog, self).closeEvent(event)
108
109    def _readSettings(self):
110        s = QSettings()
111        s.beginGroup('filelog')
112        try:
113            self.textView.loadSettings(s, 'fileview')
114            self.restoreGeometry(s.value('geom').toByteArray())
115            self.splitter.restoreState(s.value('splitter').toByteArray())
116            self.revpanel.set_expanded(s.value('revpanel.expanded').toBool())
117        finally:
118            s.endGroup()
119
120    def _writeSettings(self):
121        s = QSettings()
122        s.beginGroup('filelog')
123        try:
124            self.textView.saveSettings(s, 'fileview')
125            s.setValue('revpanel.expanded', self.revpanel.is_expanded())
126            s.setValue('geom', self.saveGeometry())
127            s.setValue('splitter', self.splitter.saveState())
128        finally:
129            s.endGroup()
130
131    def setupUi(self, o):
132        self.editToolbar = QToolBar(self)
133        self.addToolBar(Qt.ToolBarArea(Qt.TopToolBarArea), self.editToolbar)
134        self.actionClose = QAction(self, shortcut=QKeySequence.Close)
135        self.actionReload = QAction(self, shortcut=QKeySequence.Refresh)
136        self.editToolbar.addAction(self.actionReload)
137        self.addAction(self.actionClose)
138
139        self.splitter = QSplitter(Qt.Vertical)
140        self.setCentralWidget(self.splitter)
141        self.repoview = HgRepoView(self.repo, self.splitter)
142        self.contentframe = QFrame(self.splitter)
143
144        vbox = QVBoxLayout()
145        vbox.setSpacing(0)
146        vbox.setMargin(0)
147        self.contentframe.setLayout(vbox)
148
149        self.revpanel = RevPanelWidget(self.repo)
150        self.revpanel.linkActivated.connect(self.linkActivated)
151        vbox.addWidget(self.revpanel, 0)
152
153        self.textView = HgFileView(self.repo, self)
154        self.textView.forceMode('file')
155        vbox.addWidget(self.textView, 1)
156
157    @pyqtSlot(unicode)
158    def linkActivated(self, link):
159        link = unicode(link)
160        if ':' in link:
161            scheme, param = link.split(':', 1)
162            if scheme == 'cset':
163                rev = self.repo[param].rev()
164                return self.goto(rev)
165        QDesktopServices.openUrl(QUrl(link))
166
167    def setupViews(self):
168        self.textView.setFont(self._font)
169        self.textView.showMessage.connect(self.statusBar().showMessage)
170
171    def setupToolbars(self):
172        self.editToolbar.addSeparator()
173        self.editToolbar.addAction(self.actionBack)
174        self.editToolbar.addAction(self.actionForward)
175
176    def setupModels(self):
177        self.filerevmodel = FileRevModel(self.repo, parent=self)
178        self.repoview.setModel(self.filerevmodel)
179        self.repoview.revisionSelected.connect(self.revisionSelected)
180        self.repoview.revisionActivated.connect(self.revisionActivated)
181        self.filerevmodel.showMessage.connect(self.statusBar().showMessage)
182        self.filerevmodel.filled.connect(self.modelFilled)
183        self.filerevmodel.setFilename(self.filename)
184
185    def createActions(self):
186        self.actionClose.triggered.connect(self.close)
187        self.actionReload.triggered.connect(self.reload)
188        self.actionReload.setIcon(geticon('reload'))
189
190        self.actionBack = QAction(_('Back'), self, enabled=False,
191                                  icon=geticon('back'))
192        self.actionForward = QAction(_('Forward'), self, enabled=False,
193                                     icon=geticon('forward'))
194        self.repoview.revisionSelected.connect(self._updateHistoryActions)
195        self.actionBack.triggered.connect(self.repoview.back)
196        self.actionForward.triggered.connect(self.repoview.forward)
197
198    @pyqtSlot()
199    def _updateHistoryActions(self):
200        self.actionBack.setEnabled(self.repoview.canGoBack())
201        self.actionForward.setEnabled(self.repoview.canGoForward())
202
203    def modelFilled(self):
204        self.repoview.resizeColumns()
205        if self._show_rev is not None:
206            index = self.filerevmodel.indexFromRev(self._show_rev)
207            self._show_rev = None
208        else:
209            index = self.filerevmodel.index(0,0)
210        if index is not None:
211            self.repoview.setCurrentIndex(index)
212
213    def revisionSelected(self, rev):
214        pos = self.textView.verticalScrollBar().value()
215        ctx = self.filerevmodel.repo.changectx(rev)
216        self.textView.setContext(ctx)
217        self.textView.displayFile(self.filerevmodel.graph.filename(rev))
218        self.textView.verticalScrollBar().setValue(pos)
219        self.revpanel.set_revision(rev)
220        self.revpanel.update(repo = self.repo)
221
222    def goto(self, rev):
223        index = self.filerevmodel.indexFromRev(rev)
224        if index is not None:
225            self.repoview.setCurrentIndex(index)
226        else:
227            self._show_rev = rev
228
229
230
231class FileDiffDialog(_AbstractFileDialog):
232    """
233    Qt4 dialog to display diffs between different mercurial revisions of a file.
234    """
235    def __init__(self, repo, filename, repoviewer=None):
236        super(FileDiffDialog, self).__init__(repo, filename, repoviewer)
237        self._readSettings()
238
239    def closeEvent(self, event):
240        self._writeSettings()
241        super(FileDiffDialog, self).closeEvent(event)
242
243    def _readSettings(self):
244        s = QSettings()
245        s.beginGroup('filediff')
246        try:
247            self.restoreGeometry(s.value('geom').toByteArray())
248            self.splitter.restoreState(s.value('splitter').toByteArray())
249        finally:
250            s.endGroup()
251
252    def _writeSettings(self):
253        s = QSettings()
254        s.beginGroup('filediff')
255        try:
256            s.setValue('geom', self.saveGeometry())
257            s.setValue('splitter', self.splitter.saveState())
258        finally:
259            s.endGroup()
260
261    def setupUi(self, o):
262        self.editToolbar = QToolBar(self)
263        self.addToolBar(Qt.ToolBarArea(Qt.TopToolBarArea), self.editToolbar)
264        self.actionClose = QAction(self, shortcut=QKeySequence.Close)
265        self.actionReload = QAction(self, shortcut=QKeySequence.Refresh)
266        self.editToolbar.addAction(self.actionReload)
267        self.addAction(self.actionClose)
268
269        def layouttowidget(layout):
270            w = QWidget()
271            w.setLayout(layout)
272            return w
273
274        self.splitter = QSplitter(Qt.Vertical)
275        self.setCentralWidget(self.splitter)
276        self.horizontalLayout = QHBoxLayout()
277        self.tableView_revisions_left = HgRepoView(self.repo, self)
278        self.tableView_revisions_right = HgRepoView(self.repo, self)
279        self.horizontalLayout.addWidget(self.tableView_revisions_left)
280        self.horizontalLayout.addWidget(self.tableView_revisions_right)
281        self.frame = QFrame()
282        self.splitter.addWidget(layouttowidget(self.horizontalLayout))
283        self.splitter.addWidget(self.frame)
284
285    def setupViews(self):
286        self.tableViews = {'left': self.tableView_revisions_left,
287                           'right': self.tableView_revisions_right}
288        # viewers are Scintilla editors
289        self.viewers = {}
290        # block are diff-block displayers
291        self.block = {}
292        self.diffblock = BlockMatch(self.frame)
293        lay = QHBoxLayout(self.frame)
294        lay.setSpacing(0)
295        lay.setContentsMargins(0, 0, 0, 0)
296        for side, idx  in (('left', 0), ('right', 3)):
297            sci = QsciScintilla(self.frame)
298            sci.setFont(self._font)
299            sci.verticalScrollBar().setFocusPolicy(Qt.StrongFocus)
300            sci.setFocusProxy(sci.verticalScrollBar())
301            sci.verticalScrollBar().installEventFilter(self)
302            sci.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
303            sci.setFrameShape(QFrame.NoFrame)
304            sci.setMarginLineNumbers(1, True)
305            sci.SendScintilla(sci.SCI_SETSELEOLFILLED, True)
306            if self.lexer:
307                sci.setLexer(self.lexer)
308
309            sci.setReadOnly(True)
310            lay.addWidget(sci)
311
312            # hide margin 0 (markers)
313            sci.SendScintilla(sci.SCI_SETMARGINTYPEN, 0, 0)
314            sci.SendScintilla(sci.SCI_SETMARGINWIDTHN, 0, 0)
315            # setup margin 1 for line numbers only
316            sci.SendScintilla(sci.SCI_SETMARGINTYPEN, 1, 1)
317            sci.SendScintilla(sci.SCI_SETMARGINWIDTHN, 1, 20)
318            sci.SendScintilla(sci.SCI_SETMARGINMASKN, 1, 0)
319
320            # define markers for colorize zones of diff
321            self.markerplus = sci.markerDefine(QsciScintilla.Background)
322            sci.SendScintilla(sci.SCI_MARKERSETBACK, self.markerplus, 0xB0FFA0)
323            self.markerminus = sci.markerDefine(QsciScintilla.Background)
324            sci.SendScintilla(sci.SCI_MARKERSETBACK, self.markerminus, 0xA0A0FF)
325            self.markertriangle = sci.markerDefine(QsciScintilla.Background)
326            sci.SendScintilla(sci.SCI_MARKERSETBACK, self.markertriangle, 0xFFA0A0)
327
328            self.viewers[side] = sci
329            blk = BlockList(self.frame)
330            blk.linkScrollBar(sci.verticalScrollBar())
331            self.diffblock.linkScrollBar(sci.verticalScrollBar(), side)
332            lay.insertWidget(idx, blk)
333            self.block[side] = blk
334        lay.insertWidget(2, self.diffblock)
335
336        for side in sides:
337            table = getattr(self, 'tableView_revisions_%s' % side)
338            table.setTabKeyNavigation(False)
339            #table.installEventFilter(self)
340            table.revisionSelected.connect(self.revisionSelected)
341            table.revisionActivated.connect(self.revisionActivated)
342
343            self.viewers[side].verticalScrollBar().valueChanged.connect(
344                    lambda value, side=side: self.vbar_changed(value, side))
345
346        self.setTabOrder(table, self.viewers['left'])
347        self.setTabOrder(self.viewers['left'], self.viewers['right'])
348
349        # timer used to fill viewers with diff block markers during GUI idle time
350        self.timer = QTimer()
351        self.timer.setSingleShot(False)
352        self.timer.timeout.connect(self.idle_fill_files)
353
354    def setupModels(self):
355        self.filedata = {'left': None, 'right': None}
356        self._invbarchanged = False
357        self.filerevmodel = FileRevModel(self.repo, self.filename, parent=self)
358        self.filerevmodel.filled.connect(self.modelFilled)
359        self.tableView_revisions_left.setModel(self.filerevmodel)
360        self.tableView_revisions_right.setModel(self.filerevmodel)
361
362    def createActions(self):
363        self.actionClose.triggered.connect(self.close)
364        self.actionReload.triggered.connect(self.reload)
365        self.actionReload.setIcon(geticon('reload'))
366
367        self.actionNextDiff = QAction(geticon('down'), 'Next diff', self)
368        self.actionNextDiff.setShortcut('Alt+Down')
369        self.actionNextDiff.triggered.connect(self.nextDiff)
370
371        self.actionPrevDiff = QAction(geticon('up'), 'Previous diff', self)
372        self.actionPrevDiff.setShortcut('Alt+Up')
373        self.actionPrevDiff.triggered.connect(self.prevDiff)
374
375        self.actionNextDiff.setEnabled(False)
376        self.actionPrevDiff.setEnabled(False)
377
378    def setupToolbars(self):
379        self.editToolbar.addSeparator()
380        self.editToolbar.addAction(self.actionNextDiff)
381        self.editToolbar.addAction(self.actionPrevDiff)
382
383    def modelFilled(self):
384        self.tableView_revisions_left.resizeColumns()
385        self.tableView_revisions_right.resizeColumns()
386        if self._show_rev is not None:
387            rev = self._show_rev
388            self._show_rev = None
389        else:
390            rev = self.filerevmodel.graph[0].rev
391        self.goto(rev)
392
393    def revisionSelected(self, rev):
394        if self.sender() is self.tableView_revisions_right:
395            side = 'right'
396        else:
397            side = 'left'
398        path = self.filerevmodel.graph.nodesdict[rev].extra[0]
399        fc = self.repo.changectx(rev).filectx(path)
400        self.filedata[side] = fc.data().splitlines()
401        self.update_diff(keeppos=otherside[side])
402
403    def goto(self, rev):
404        index = self.filerevmodel.indexFromRev(rev)
405        if index is not None:
406            if index.row() == 0:
407                index = self.filerevmodel.index(1, 0)
408            self.tableView_revisions_left.setCurrentIndex(index)
409            index = self.filerevmodel.index(0, 0)
410            self.tableView_revisions_right.setCurrentIndex(index)
411        else:
412            self._show_rev = rev
413
414    def setDiffNavActions(self, pos=0):
415        hasdiff = (self.diffblock.nDiffs() > 0)
416        self.actionNextDiff.setEnabled(hasdiff and pos != 1)
417        self.actionPrevDiff.setEnabled(hasdiff and pos != -1)
418
419    def nextDiff(self):
420        self.setDiffNavActions(self.diffblock.nextDiff())
421
422    def prevDiff(self):
423        self.setDiffNavActions(self.diffblock.prevDiff())
424
425    def update_page_steps(self, keeppos=None):
426        for side in sides:
427            self.block[side].syncPageStep()
428        self.diffblock.syncPageStep()
429        if keeppos:
430            side, pos = keeppos
431            self.viewers[side].verticalScrollBar().setValue(pos)
432
433    def idle_fill_files(self):
434        # we make a burst of diff-lines computed at once, but we
435        # disable GUI updates for efficiency reasons, then only
436        # refresh GUI at the end of the burst
437        for side in sides:
438            self.viewers[side].setUpdatesEnabled(False)
439            self.block[side].setUpdatesEnabled(False)
440        self.diffblock.setUpdatesEnabled(False)
441
442        for n in range(30): # burst pool
443            if self._diff is None or not self._diff.get_opcodes():
444                self._diff = None
445                self.timer.stop()
446                self.setDiffNavActions(-1)
447                break
448
449            tag, alo, ahi, blo, bhi = self._diff.get_opcodes().pop(0)
450
451            w = self.viewers['left']
452            cposl = w.SendScintilla(w.SCI_GETENDSTYLED)
453            w = self.viewers['right']
454            cposr = w.SendScintilla(w.SCI_GETENDSTYLED)
455            if tag == 'replace':
456                self.block['left'].addBlock('x', alo, ahi)
457                self.block['right'].addBlock('x', blo, bhi)
458                self.diffblock.addBlock('x', alo, ahi, blo, bhi)
459
460                w = self.viewers['left']
461                for i in range(alo, ahi):
462                    w.markerAdd(i, self.markertriangle)
463
464                w = self.viewers['right']
465                for i in range(blo, bhi):
466                    w.markerAdd(i, self.markertriangle)
467
468            elif tag == 'delete':
469                self.block['left'].addBlock('-', alo, ahi)
470                self.diffblock.addBlock('-', alo, ahi, blo, bhi)
471
472                w = self.viewers['left']
473                for i in range(alo, ahi):
474                    w.markerAdd(i, self.markerminus)
475
476            elif tag == 'insert':
477                self.block['right'].addBlock('+', blo, bhi)
478                self.diffblock.addBlock('+', alo, ahi, blo, bhi)
479
480                w = self.viewers['right']
481                for i in range(blo, bhi):
482                    w.markerAdd(i, self.markerplus)
483
484            elif tag == 'equal':
485                pass
486
487            else:
488                raise ValueError, 'unknown tag %r' % (tag,)
489
490        # ok, let's enable GUI refresh for code viewers and diff-block displayers
491        for side in sides:
492            self.viewers[side].setUpdatesEnabled(True)
493            self.block[side].setUpdatesEnabled(True)
494        self.diffblock.setUpdatesEnabled(True)
495
496    def update_diff(self, keeppos=None):
497        """
498        Recompute the diff, display files and starts the timer
499        responsible for filling diff markers
500        """
501        if keeppos:
502            pos = self.viewers[keeppos].verticalScrollBar().value()
503            keeppos = (keeppos, pos)
504
505        for side in sides:
506            self.viewers[side].clear()
507            self.block[side].clear()
508        self.diffblock.clear()
509
510        if None not in self.filedata.values():
511            if self.timer.isActive():
512                self.timer.stop()
513            for side in sides:
514                self.viewers[side].setMarginWidth(1, "00%s" % len(self.filedata[side]))
515
516            self._diff = difflib.SequenceMatcher(None, self.filedata['left'],
517                                                 self.filedata['right'])
518            blocks = self._diff.get_opcodes()[:]
519
520            self._diffmatch = {'left': [x[1:3] for x in blocks],
521                               'right': [x[3:5] for x in blocks]}
522            for side in sides:
523                self.viewers[side].setText('\n'.join(self.filedata[side]))
524            self.update_page_steps(keeppos)
525            self.timer.start()
526
527    def vbar_changed(self, value, side):
528        """
529        Callback called when the vertical scrollbar of a file viewer
530        is changed, so we can update the position of the other file
531        viewer.
532        """
533        if self._invbarchanged:
534            # prevent loops in changes (left -> right -> left ...)
535            return
536        self._invbarchanged = True
537        oside = otherside[side]
538
539        for i, (lo, hi) in enumerate(self._diffmatch[side]):
540            if lo <= value < hi:
541                break
542        dv = value - lo
543
544        blo, bhi = self._diffmatch[oside][i]
545        vbar = self.viewers[oside].verticalScrollBar()
546        if (dv) < (bhi - blo):
547            bvalue = blo + dv
548        else:
549            bvalue = bhi
550        vbar.setValue(bvalue)
551        self._invbarchanged = False