PageRenderTime 29ms CodeModel.GetById 2ms app.highlight 22ms RepoModel.GetById 2ms app.codeStats 0ms

/tortoisehg/hgqt/manifestmodel.py

https://bitbucket.org/tortoisehg/hgtk/
Python | 290 lines | 263 code | 12 blank | 15 comment | 10 complexity | c51582bc498840c04e4426a523e64b59 MD5 | raw file
  1# manifestmodel.py - Model for TortoiseHg manifest view
  2#
  3# Copyright (C) 2009-2010 LOGILAB S.A. <http://www.logilab.fr/>
  4# Copyright (C) 2010 Yuya Nishihara <yuya@tcha.org>
  5#
  6# This program is free software; you can redistribute it and/or modify it under
  7# the terms of the GNU General Public License as published by the Free Software
  8# Foundation; either version 2 of the License, or (at your option) any later
  9# version.
 10
 11import os, itertools
 12
 13from PyQt4.QtCore import *
 14from PyQt4.QtGui import *
 15
 16from mercurial import util
 17from tortoisehg.util import hglib
 18from tortoisehg.hgqt import qtlib, status, visdiff
 19
 20class ManifestModel(QAbstractItemModel):
 21    """
 22    Qt model to display a hg manifest, ie. the tree of files at a
 23    given revision. To be used with a QTreeView.
 24    """
 25
 26    StatusRole = Qt.UserRole + 1
 27    """Role for file change status"""
 28
 29    def __init__(self, repo, rev, statusfilter='MAC', parent=None):
 30        QAbstractItemModel.__init__(self, parent)
 31
 32        self._repo = repo
 33        self._rev = rev
 34
 35        assert util.all(c in 'MARC' for c in statusfilter)
 36        self._statusfilter = statusfilter
 37
 38    def data(self, index, role=Qt.DisplayRole):
 39        if not index.isValid():
 40            return
 41
 42        if role == Qt.DecorationRole:
 43            return self.fileIcon(index)
 44        if role == self.StatusRole:
 45            return self.fileStatus(index)
 46
 47        e = index.internalPointer()
 48        if role == Qt.DisplayRole:
 49            return e.name
 50
 51    def filePath(self, index):
 52        """Return path at the given index [unicode]"""
 53        if not index.isValid():
 54            return ''
 55
 56        return index.internalPointer().path
 57
 58    def fileIcon(self, index):
 59        ic = QApplication.style().standardIcon(
 60            self.isDir(index) and QStyle.SP_DirIcon or QStyle.SP_FileIcon)
 61        if not index.isValid():
 62            return ic
 63        e = index.internalPointer()
 64        if not e.status:
 65            return ic
 66        st = status.statusTypes[e.status]
 67        if st.icon:
 68            ic = _overlaidicon(ic, qtlib.geticon(st.icon.rstrip('.ico')))  # XXX
 69        return ic
 70
 71    def fileStatus(self, index):
 72        """Return the change status of the specified file"""
 73        if not index.isValid():
 74            return
 75        e = index.internalPointer()
 76        return e.status
 77
 78    def isDir(self, index):
 79        if not index.isValid():
 80            return True  # root entry must be a directory
 81        e = index.internalPointer()
 82        return len(e) != 0
 83
 84    def mimeData(self, indexes):
 85        def preparefiles():
 86            files = [self.filePath(i) for i in indexes if i.isValid()]
 87            if self._rev is not None:
 88                base, _fns = visdiff.snapshot(self._repo, files,
 89                                              self._repo[self._rev])
 90            else:  # working copy
 91                base = self._repo.root
 92            return iter(os.path.join(base, e) for e in files)
 93
 94        m = QMimeData()
 95        m.setUrls([QUrl.fromLocalFile(e) for e in preparefiles()])
 96        return m
 97
 98    def mimeTypes(self):
 99        return ['text/uri-list']
100
101    def flags(self, index):
102        if not index.isValid():
103            return Qt.ItemIsEnabled
104        f = Qt.ItemIsEnabled | Qt.ItemIsSelectable
105        if not (self.isDir(index) or self.fileStatus(index) == 'R'):
106            f |= Qt.ItemIsDragEnabled
107        return f
108
109    def index(self, row, column, parent=QModelIndex()):
110        try:
111            return self.createIndex(row, column,
112                                    self._parententry(parent).at(row))
113        except IndexError:
114            return QModelIndex()
115
116    def indexFromPath(self, path, column=0):
117        """Return index for the specified path if found [unicode]
118
119        If not found, returns invalid index.
120        """
121        if not path:
122            return QModelIndex()
123
124        e = self._rootentry
125        paths = path and unicode(path).split('/') or []
126        try:
127            for p in paths:
128                e = e[p]
129        except KeyError:
130            return QModelIndex()
131
132        return self.createIndex(e.parent.index(e.name), column, e)
133
134    def parent(self, index):
135        if not index.isValid():
136            return QModelIndex()
137
138        e = index.internalPointer()
139        if e.path:
140            return self.indexFromPath(e.parent.path, index.column())
141        else:
142            return QModelIndex()
143
144    def _parententry(self, parent):
145        if parent.isValid():
146            return parent.internalPointer()
147        else:
148            return self._rootentry
149
150    def rowCount(self, parent=QModelIndex()):
151        return len(self._parententry(parent))
152
153    def columnCount(self, parent=QModelIndex()):
154        return 1
155
156    @pyqtSlot(str)
157    def setStatusFilter(self, status):
158        """Filter file tree by change status 'MARC'"""
159        status = str(status)
160        assert util.all(c in 'MARC' for c in status)
161        if self._statusfilter == status:
162            return  # for performance reason
163        self._statusfilter = status
164        self._buildrootentry()
165
166    @property
167    def statusFilter(self):
168        """Return the current status filter"""
169        return self._statusfilter
170
171    @property
172    def _rootentry(self):
173        try:
174            return self.__rootentry
175        except AttributeError:
176            self._buildrootentry()
177            return self.__rootentry
178
179    def _buildrootentry(self):
180        """Rebuild the tree of files and directories"""
181        roote = _Entry()
182        ctx = self._repo[self._rev]
183
184        status = dict(zip(('M', 'A', 'R'),
185                          (set(a) for a in self._repo.status(ctx.parents()[0],
186                                                             ctx)[:3])))
187        uncleanpaths = status['M'] | status['A'] | status['R']
188        def pathinstatus(path):
189            """Test path is included by the status filter"""
190            if util.any(c in self._statusfilter and path in e
191                        for c, e in status.iteritems()):
192                return True
193            if 'C' in self._statusfilter and path not in uncleanpaths:
194                return True
195            return False
196
197        for path in itertools.chain(ctx.manifest(), status['R']):
198            if not pathinstatus(path):
199                continue
200
201            e = roote
202            for p in hglib.tounicode(path).split('/'):
203                if not p in e:
204                    e.addchild(p)
205                e = e[p]
206
207            for st, files in status.iteritems():
208                if path in files:
209                    # TODO: what if added & removed at once?
210                    e.setstatus(st)
211                    break
212            else:
213                e.setstatus('C')
214
215        roote.sort()
216
217        self.beginResetModel()
218        self.__rootentry = roote
219        self.endResetModel()
220
221def _overlaidicon(base, overlay):
222    """Generate overlaid icon"""
223    # TODO: generalize this function as a utility
224    pixmap = base.pixmap(16, 16)
225    painter = QPainter(pixmap)
226    painter.setCompositionMode(QPainter.CompositionMode_SourceOver)
227    painter.drawPixmap(0, 0, overlay.pixmap(16, 16))
228    del painter
229    return QIcon(pixmap)
230
231class _Entry(object):
232    """Each file or directory"""
233    def __init__(self, name='', parent=None):
234        self._name = name
235        self._parent = parent
236        self._status = None
237        self._child = {}
238        self._nameindex = []
239
240    @property
241    def parent(self):
242        return self._parent
243
244    @property
245    def path(self):
246        if self.parent is None or not self.parent.name:
247            return self.name
248        else:
249            return self.parent.path + '/' + self.name
250
251    @property
252    def name(self):
253        return self._name
254
255    @property
256    def status(self):
257        """Return file change status"""
258        return self._status
259
260    def setstatus(self, status):
261        assert status in 'MARC'
262        self._status = status
263
264    def __len__(self):
265        return len(self._child)
266
267    def __getitem__(self, name):
268        return self._child[name]
269
270    def addchild(self, name):
271        if name not in self._child:
272            self._nameindex.append(name)
273        self._child[name] = self.__class__(name, parent=self)
274
275    def __contains__(self, item):
276        return item in self._child
277
278    def at(self, index):
279        return self._child[self._nameindex[index]]
280
281    def index(self, name):
282        return self._nameindex.index(name)
283
284    def sort(self, reverse=False):
285        """Sort the entries recursively; directories first"""
286        for e in self._child.itervalues():
287            e.sort(reverse=reverse)
288        self._nameindex.sort(
289            key=lambda s: '%s%s' % (self[s] and 'D' or 'F', s),
290            reverse=reverse)