PageRenderTime 57ms CodeModel.GetById 12ms app.highlight 38ms RepoModel.GetById 2ms app.codeStats 0ms

/tortoisehg/hgqt/repomodel.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 553 lines | 512 code | 24 blank | 17 comment | 38 complexity | 3014438ee78748d4c2156cb514457aa5 MD5 | raw file
  1# Copyright (c) 2009-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
 17from mercurial import util, error
 18from mercurial.util import propertycache
 19
 20from tortoisehg.util import hglib
 21from tortoisehg.hgqt.graph import Graph
 22from tortoisehg.hgqt.graph import revision_grapher
 23from tortoisehg.hgqt import qtlib
 24
 25from tortoisehg.hgqt.i18n import _
 26
 27from PyQt4.QtCore import *
 28from PyQt4.QtGui import *
 29
 30nullvariant = QVariant()
 31
 32# TODO: Remove these two when we adopt GTK author color scheme
 33COLORS = [ "blue", "darkgreen", "red", "green", "darkblue", "purple",
 34           "cyan", Qt.darkYellow, "magenta", "darkred", "darkmagenta",
 35           "darkcyan", "gray", "yellow", ]
 36COLORS = [str(QColor(x).name()) for x in COLORS]
 37
 38ALLCOLUMNS = ('Graph', 'Rev', 'Branch', 'Description', 'Author', 'Tags', 'Node',
 39              'Age', 'LocalTime', 'UTCTime', 'Changes')
 40
 41UNAPPLIED_PATCH_COLOR = '#999999'
 42
 43def get_color(n, ignore=()):
 44    """
 45    Return a color at index 'n' rotating in the available
 46    colors. 'ignore' is a list of colors not to be chosen.
 47    """
 48    ignore = [str(QColor(x).name()) for x in ignore]
 49    colors = [x for x in COLORS if x not in ignore]
 50    if not colors: # ghh, no more available colors...
 51        colors = COLORS
 52    return colors[n % len(colors)]
 53
 54class HgRepoListModel(QAbstractTableModel):
 55    """
 56    Model used for displaying the revisions of a Hg *local* repository
 57    """
 58    showMessage = pyqtSignal(unicode)
 59    filled = pyqtSignal()
 60    loaded = pyqtSignal()
 61
 62    _columns = ('Graph', 'Rev', 'Branch', 'Description', 'Author', 'Age', 'Tags',)
 63    _stretchs = {'Description': 1, }
 64    _mqtags = ('qbase', 'qtip', 'qparent')
 65
 66    def __init__(self, repo, branch, revset, rfilter, parent):
 67        """
 68        repo is a hg repo instance
 69        """
 70        QAbstractTableModel.__init__(self, parent)
 71        self._cache = []
 72        self.graph = None
 73        self.timerHandle = None
 74        self.dotradius = 8
 75        self.rowheight = 20
 76        self.rowcount = 0
 77        self.repo = repo
 78        self.revset = revset
 79        self.filterbyrevset = rfilter
 80
 81        # To be deleted
 82        self._user_colors = {}
 83        self._branch_colors = {}
 84
 85        self._columnmap = {
 86            'Rev':      lambda ctx, gnode: type(ctx.rev()) is int and \
 87                                           str(ctx.rev()) or "",
 88            'Node':     lambda ctx, gnode: str(ctx),
 89            'Graph':    lambda ctx, gnode: "",
 90            'Description': self.getlog,
 91            'Author':   self.getauthor,
 92            'Tags':     self.gettags,
 93            'Branch':   self.getbranch,
 94            'Filename': lambda ctx, gnode: gnode.extra[0],
 95            'Age':      lambda ctx, gnode: hglib.age(ctx.date()),
 96            'LocalTime':lambda ctx, gnode: hglib.displaytime(ctx.date()),
 97            'UTCTime':  lambda ctx, gnode: hglib.utctime(ctx.date()),
 98            'Changes':  self.getchanges,
 99        }
100
101        if repo:
102            self.reloadConfig()
103            self.updateColumns()
104            self.setBranch(branch)
105
106    def setBranch(self, branch=None, allparents=True):
107        self.filterbranch = branch
108        self.invalidateCache()
109        if self.revset and self.filterbyrevset:
110            grapher = revision_grapher(self.repo, revset=self.revset)
111            self.graph = Graph(self.repo, grapher, include_mq=False)
112        else:
113            grapher = revision_grapher(self.repo, branch=branch,
114                                       allparents=allparents)
115            self.graph = Graph(self.repo, grapher, include_mq=True)
116        self.rowcount = 0
117        self.layoutChanged.emit()
118        self.ensureBuilt(row=0)
119        self.showMessage.emit('')
120        QTimer.singleShot(0, lambda: self.filled.emit())
121
122    def reloadConfig(self):
123        _ui = self.repo.ui
124        self.fill_step = int(_ui.config('tortoisehg', 'graphlimit', 500))
125        self.authorcolor = _ui.configbool('tortoisehg', 'authorcolor')
126        self.maxauthor = 'author name'
127
128    def updateColumns(self):
129        s = QSettings()
130        cols = s.value('workbench/columns').toStringList()
131        cols = [str(col) for col in cols]
132        # Fixup older names for columns
133        if 'Log' in cols:
134            cols[cols.index('Log')] = 'Description'
135            s.setValue('workbench/columns', cols)
136        if 'ID' in cols:
137            cols[cols.index('ID')] = 'Rev'
138            s.setValue('workbench/columns', cols)
139        validcols = [col for col in cols if col in ALLCOLUMNS]
140        if validcols:
141            self._columns = tuple(validcols)
142            self.invalidateCache()
143            self.layoutChanged.emit()
144
145    def invalidate(self):
146        self.reloadConfig()
147        self.invalidateCache()
148        self.layoutChanged.emit()
149
150    def branch(self):
151        return self.filterbranch
152
153    def ensureBuilt(self, rev=None, row=None):
154        """
155        Make sure rev data is available (graph element created).
156
157        """
158        if self.graph.isfilled():
159            return
160        required = 0
161        buildrev = rev
162        n = len(self.graph)
163        if rev is not None:
164            if n and self.graph[-1].rev <= rev:
165                buildrev = None
166            else:
167                required = self.fill_step/2
168        elif row is not None and row > (n - self.fill_step / 2):
169            required = row - n + self.fill_step
170        if required or buildrev:
171            self.graph.build_nodes(nnodes=required, rev=buildrev)
172            self.updateRowCount()
173
174        if self.rowcount >= len(self.graph):
175            return  # no need to update row count
176        if row and row > self.rowcount:
177            # asked row was already built, but views where not aware of this
178            self.updateRowCount()
179        elif rev is not None and rev <= self.graph[self.rowcount].rev:
180            # asked rev was already built, but views where not aware of this
181            self.updateRowCount()
182
183    def loadall(self):
184        self.timerHandle = self.startTimer(1)
185
186    def timerEvent(self, event):
187        if event.timerId() == self.timerHandle:
188            self.showMessage.emit(_('filling (%d)')%(len(self.graph)))
189            if self.graph.isfilled():
190                self.killTimer(self.timerHandle)
191                self.timerHandle = None
192                self.showMessage.emit('')
193                self.loaded.emit()
194            # we only fill the graph data structures without telling
195            # views until the model is loaded, to keep maximal GUI
196            # reactivity
197            elif not self.graph.build_nodes():
198                self.killTimer(self.timerHandle)
199                self.timerHandle = None
200                self.updateRowCount()
201                self.showMessage.emit('')
202                self.loaded.emit()
203
204    def updateRowCount(self):
205        currentlen = self.rowcount
206        newlen = len(self.graph)
207
208        sauthors = [hglib.username(user) for user in list(self.graph.authors)]
209        sauthors.append(self.maxauthor)
210        self.maxauthor = sorted(sauthors, key=lambda x: len(x))[-1]
211
212        if newlen > self.rowcount:
213            self.beginInsertRows(QModelIndex(), currentlen, newlen-1)
214            self.rowcount = newlen
215            self.endInsertRows()
216
217    def rowCount(self, parent):
218        if parent.isValid():
219            return 0
220        return self.rowcount
221
222    def columnCount(self, parent):
223        if parent.isValid():
224            return 0
225        return len(self._columns)
226
227    def maxWidthValueForColumn(self, col):
228        if self.graph is None:
229            return 'XXXX'
230        column = self._columns[col]
231        if column == 'Rev':
232            return str(len(self.repo))
233        if column == 'Node':
234            return str(self.repo['.'])
235        if column in ('Age', 'LocalTime', 'UTCTime'):
236            return hglib.displaytime(util.makedate())
237        if column == 'Tags':
238            try:
239                return sorted(self.repo.tags().keys(), key=lambda x: len(x))[-1][:10]
240            except IndexError:
241                pass
242        if column == 'Branch':
243            try:
244                return sorted(self.repo.branchtags().keys(), key=lambda x: len(x))[-1]
245            except IndexError:
246                pass
247        if column == 'Author':
248            return self.maxauthor
249        if column == 'Filename':
250            return self.filename
251        if column == 'Graph':
252            res = self.col2x(self.graph.max_cols)
253            return min(res, 150)
254        if column == 'Changes':
255            return 'Changes'
256        # Fall through for Description
257        return None
258
259    def user_color(self, user):
260        'deprecated, please replace with hgtk color scheme'
261        if user not in self._user_colors:
262            self._user_colors[user] = get_color(len(self._user_colors),
263                                                self._user_colors.values())
264        return self._user_colors[user]
265
266    def namedbranch_color(self, branch):
267        'deprecated, please replace with hgtk color scheme'
268        if branch not in self._branch_colors:
269            self._branch_colors[branch] = get_color(len(self._branch_colors))
270        return self._branch_colors[branch]
271
272    def col2x(self, col):
273        return 2 * self.dotradius * col + self.dotradius/2 + 8
274
275    def graphctx(self, ctx, gnode):
276        w = self.col2x(gnode.cols) + 10
277        h = self.rowheight
278
279        dot_y = h / 2
280
281        pix = QPixmap(w, h)
282        pix.fill(QColor(0,0,0,0))
283        painter = QPainter(pix)
284        painter.setRenderHint(QPainter.Antialiasing)
285
286        pen = QPen(Qt.blue)
287        pen.setWidth(2)
288        painter.setPen(pen)
289
290        lpen = QPen(pen)
291        lpen.setColor(Qt.black)
292        painter.setPen(lpen)
293        for y1, y4, lines in ((dot_y, dot_y + h, gnode.bottomlines),
294                              (dot_y - h, dot_y, gnode.toplines)):
295            y2 = y1 + 1 * (y4 - y1)/4
296            ymid = (y1 + y4)/2
297            y3 = y1 + 3 * (y4 - y1)/4
298
299            for start, end, color in lines:
300                lpen = QPen(pen)
301                lpen.setColor(QColor(get_color(color)))
302                lpen.setWidth(2)
303                painter.setPen(lpen)
304                x1 = self.col2x(start)
305                x2 = self.col2x(end)
306                path = QPainterPath()
307                path.moveTo(x1, y1)
308                path.cubicTo(x1, y2,
309                             x1, y2,
310                             (x1 + x2)/2, ymid)
311                path.cubicTo(x2, y3,
312                             x2, y3,
313                             x2, y4)
314                painter.drawPath(path)
315
316        # Draw node
317        dot_color = QColor(self.namedbranch_color(ctx.branch()))
318        dotcolor = dot_color.lighter()
319        pencolor = dot_color.darker()
320        white = QColor("white")
321        fillcolor = gnode.rev is None and white or dotcolor
322
323        pen = QPen(pencolor)
324        pen.setWidthF(1.5)
325        painter.setPen(pen)
326
327        radius = self.dotradius
328        centre_x = self.col2x(gnode.x)
329        centre_y = h/2
330
331        def circle(r):
332            rect = QRectF(centre_x - r,
333                          centre_y - r,
334                          2 * r, 2 * r)
335            painter.drawEllipse(rect)
336
337        def diamond(r):
338            poly = QPolygonF([QPointF(centre_x - r, centre_y),
339                              QPointF(centre_x, centre_y - r),
340                              QPointF(centre_x + r, centre_y),
341                              QPointF(centre_x, centre_y + r),
342                              QPointF(centre_x - r, centre_y),])
343            painter.drawPolygon(poly)
344
345        if ctx.thgmqappliedpatch():  # diamonds for patches
346            if ctx.thgwdparent():
347                painter.setBrush(white)
348                diamond(2 * 0.9 * radius / 1.5)
349            painter.setBrush(fillcolor)
350            diamond(radius / 1.5)
351        elif ctx.thgmqunappliedpatch():
352            patchcolor = QColor('#dddddd')
353            painter.setBrush(patchcolor)
354            painter.setPen(patchcolor)
355            diamond(radius / 1.5)
356        else:  # circles for normal revisions
357            if ctx.thgwdparent():
358                painter.setBrush(white)
359                circle(0.9 * radius)
360            painter.setBrush(fillcolor)
361            circle(0.5 * radius)
362
363        painter.end()
364        return QVariant(pix)
365
366    def invalidateCache(self):
367        self._cache = []
368        for a in ('_roleoffsets',):
369            if hasattr(self, a):
370                delattr(self, a)
371
372    @propertycache
373    def _roleoffsets(self):
374        return {Qt.DisplayRole : 0,
375                Qt.ForegroundRole : len(self._columns),
376                Qt.DecorationRole : len(self._columns) * 2}
377
378    def data(self, index, role):
379        if not index.isValid():
380            return nullvariant
381        if role in self._roleoffsets:
382            offset = self._roleoffsets[role]
383        else:
384            return nullvariant
385        row = index.row()
386        self.ensureBuilt(row=row)
387        graphlen = len(self.graph)
388        cachelen = len(self._cache)
389        if graphlen > cachelen:
390            self._cache.extend([None,] * (graphlen-cachelen))
391        data = self._cache[row]
392        if data is None:
393            data = [None,] * (self._roleoffsets[Qt.DecorationRole]+1)
394        column = self._columns[index.column()]
395        if role == Qt.DecorationRole:
396            if column != 'Graph':
397                return nullvariant
398            if data[offset] is None:
399                gnode = self.graph[row]
400                ctx = self.repo.changectx(gnode.rev)
401                data[offset] = self.graphctx(ctx, gnode)
402                self._cache[row] = data
403            return data[offset]
404        else:
405            idx = index.column() + offset
406            if data[idx] is None:
407                try:
408                    result = self.rawdata(row, column, role)
409                except util.Abort:
410                    result = nullvariant
411                data[idx] = result
412                self._cache[row] = data
413            return data[idx]
414
415    def rawdata(self, row, column, role):
416        gnode = self.graph[row]
417        ctx = self.repo.changectx(gnode.rev)
418
419        if role == Qt.DisplayRole:
420            text = self._columnmap[column](ctx, gnode)
421            if not isinstance(text, (QString, unicode)):
422                text = hglib.tounicode(text)
423            return QVariant(text)
424        elif role == Qt.ForegroundRole:
425            if ctx.thgmqunappliedpatch():
426                return QColor(UNAPPLIED_PATCH_COLOR)
427            if column == 'Author':
428                if self.authorcolor:
429                    return QVariant(QColor(self.user_color(ctx.user())))
430                return nullvariant
431            if column == 'Branch':
432                return QVariant(QColor(self.namedbranch_color(ctx.branch())))
433        return nullvariant
434
435    def flags(self, index):
436        if not index.isValid():
437            return 0
438        if not self.revset:
439            return Qt.ItemIsSelectable | Qt.ItemIsEnabled
440
441        row = index.row()
442        self.ensureBuilt(row=row)
443        gnode = self.graph[row]
444        ctx = self.repo.changectx(gnode.rev)
445
446        if ctx.node() not in self.revset:
447            return Qt.ItemFlags(0)
448        return Qt.ItemIsSelectable | Qt.ItemIsEnabled
449
450    def headerData(self, section, orientation, role):
451        if orientation == Qt.Horizontal:
452            if role == Qt.DisplayRole:
453                return QVariant(self._columns[section])
454            if role == Qt.TextAlignmentRole:
455                return QVariant(Qt.AlignLeft)
456        return nullvariant
457
458    def rowFromRev(self, rev):
459        row = self.graph.index(rev)
460        if row == -1:
461            row = None
462        return row
463
464    def indexFromRev(self, rev):
465        if self.graph is None:
466            return None
467        self.ensureBuilt(rev=rev)
468        row = self.rowFromRev(rev)
469        if row is not None:
470            return self.index(row, 0)
471        return None
472
473    def clear(self):
474        'empty the list'
475        self.graph = None
476        self.datacache = {}
477        self.layoutChanged.emit()
478
479    def getbranch(self, ctx, gnode):
480        return hglib.tounicode(ctx.branch())
481
482    def gettags(self, ctx, gnode):
483        if ctx.rev() is None:
484            return ''
485        tags = [t for t in ctx.tags() if t not in self._mqtags]
486        return hglib.tounicode(','.join(tags))
487
488    def getauthor(self, ctx, gnode):
489        try:
490            return hglib.username(ctx.user())
491        except error.Abort:
492            return _('Mercurial User')
493
494    def getlog(self, ctx, gnode):
495        if ctx.rev() is None:
496            # The Unicode symbol is a black star:
497            return u'\u2605 ' + _('Working Directory') + u' \u2605'
498
499        msg = ctx.longsummary()
500
501        if ctx.thgmqunappliedpatch():
502            effects = qtlib.geteffect('log.unapplied_patch')
503            text = qtlib.applyeffects(' %s ' % ctx._patchname, effects)
504            # qtlib.markup(msg, fg=UNAPPLIED_PATCH_COLOR)
505            return hglib.tounicode(text + ' ' + msg)
506
507        parts = []
508        if ctx.thgbranchhead():
509            branchu = hglib.tounicode(ctx.branch())
510            effects = qtlib.geteffect('log.branch')
511            parts.append(qtlib.applyeffects(u' %s ' % branchu, effects))
512
513        # in the near future, I expect bookmarks to be available from the
514        # repository via a separate API, making this logic more efficient.
515        bookmarks = self.repo.bookmarks.keys()
516        curbookmark = self.repo.bookmarkcurrent
517        for tag in ctx.thgtags():
518            if self.repo.thgmqtag(tag):
519                style = 'log.patch'
520            elif tag == curbookmark:
521                style = 'log.curbookmark'
522            elif tag in bookmarks:
523                style = 'log.bookmark'
524            else:
525                style = 'log.tag'
526            tagu = hglib.tounicode(tag)
527            effects = qtlib.geteffect(style)
528            parts.append(qtlib.applyeffects(u' %s ' % tagu, effects))
529
530        if msg:
531            if ctx.thgwdparent():
532                msg = qtlib.markup(msg, weight='bold')
533            else:
534                msg = qtlib.markup(msg)
535            parts.append(hglib.tounicode(msg))
536
537        return ' '.join(parts)
538
539    def getchanges(self, ctx, gnode):
540        """Return the MAR status for the given ctx."""
541        changes = []
542        M, A, R = ctx.changesToParent(0)
543        def addtotal(files, style):
544            effects = qtlib.geteffect(style)
545            text = qtlib.applyeffects(' %s ' % len(files), effects)
546            changes.append(text)
547        if M:
548            addtotal(M, 'log.modified')
549        if A:
550            addtotal(A, 'log.added')
551        if R:
552            addtotal(R, 'log.removed')
553        return ''.join(changes)