/tortoisehg/hgqt/manifestmodel.py
Python | 290 lines | 263 code | 12 blank | 15 comment | 6 complexity | c51582bc498840c04e4426a523e64b59 MD5 | raw file
Possible License(s): GPL-2.0
- # manifestmodel.py - Model for TortoiseHg manifest view
- #
- # Copyright (C) 2009-2010 LOGILAB S.A. <http://www.logilab.fr/>
- # Copyright (C) 2010 Yuya Nishihara <yuya@tcha.org>
- #
- # This program is free software; you can redistribute it and/or modify it under
- # the terms of the GNU General Public License as published by the Free Software
- # Foundation; either version 2 of the License, or (at your option) any later
- # version.
- import os, itertools
- from PyQt4.QtCore import *
- from PyQt4.QtGui import *
- from mercurial import util
- from tortoisehg.util import hglib
- from tortoisehg.hgqt import qtlib, status, visdiff
- class ManifestModel(QAbstractItemModel):
- """
- Qt model to display a hg manifest, ie. the tree of files at a
- given revision. To be used with a QTreeView.
- """
- StatusRole = Qt.UserRole + 1
- """Role for file change status"""
- def __init__(self, repo, rev, statusfilter='MAC', parent=None):
- QAbstractItemModel.__init__(self, parent)
- self._repo = repo
- self._rev = rev
- assert util.all(c in 'MARC' for c in statusfilter)
- self._statusfilter = statusfilter
- def data(self, index, role=Qt.DisplayRole):
- if not index.isValid():
- return
- if role == Qt.DecorationRole:
- return self.fileIcon(index)
- if role == self.StatusRole:
- return self.fileStatus(index)
- e = index.internalPointer()
- if role == Qt.DisplayRole:
- return e.name
- def filePath(self, index):
- """Return path at the given index [unicode]"""
- if not index.isValid():
- return ''
- return index.internalPointer().path
- def fileIcon(self, index):
- ic = QApplication.style().standardIcon(
- self.isDir(index) and QStyle.SP_DirIcon or QStyle.SP_FileIcon)
- if not index.isValid():
- return ic
- e = index.internalPointer()
- if not e.status:
- return ic
- st = status.statusTypes[e.status]
- if st.icon:
- ic = _overlaidicon(ic, qtlib.geticon(st.icon.rstrip('.ico'))) # XXX
- return ic
- def fileStatus(self, index):
- """Return the change status of the specified file"""
- if not index.isValid():
- return
- e = index.internalPointer()
- return e.status
- def isDir(self, index):
- if not index.isValid():
- return True # root entry must be a directory
- e = index.internalPointer()
- return len(e) != 0
- def mimeData(self, indexes):
- def preparefiles():
- files = [self.filePath(i) for i in indexes if i.isValid()]
- if self._rev is not None:
- base, _fns = visdiff.snapshot(self._repo, files,
- self._repo[self._rev])
- else: # working copy
- base = self._repo.root
- return iter(os.path.join(base, e) for e in files)
- m = QMimeData()
- m.setUrls([QUrl.fromLocalFile(e) for e in preparefiles()])
- return m
- def mimeTypes(self):
- return ['text/uri-list']
- def flags(self, index):
- if not index.isValid():
- return Qt.ItemIsEnabled
- f = Qt.ItemIsEnabled | Qt.ItemIsSelectable
- if not (self.isDir(index) or self.fileStatus(index) == 'R'):
- f |= Qt.ItemIsDragEnabled
- return f
- def index(self, row, column, parent=QModelIndex()):
- try:
- return self.createIndex(row, column,
- self._parententry(parent).at(row))
- except IndexError:
- return QModelIndex()
- def indexFromPath(self, path, column=0):
- """Return index for the specified path if found [unicode]
- If not found, returns invalid index.
- """
- if not path:
- return QModelIndex()
- e = self._rootentry
- paths = path and unicode(path).split('/') or []
- try:
- for p in paths:
- e = e[p]
- except KeyError:
- return QModelIndex()
- return self.createIndex(e.parent.index(e.name), column, e)
- def parent(self, index):
- if not index.isValid():
- return QModelIndex()
- e = index.internalPointer()
- if e.path:
- return self.indexFromPath(e.parent.path, index.column())
- else:
- return QModelIndex()
- def _parententry(self, parent):
- if parent.isValid():
- return parent.internalPointer()
- else:
- return self._rootentry
- def rowCount(self, parent=QModelIndex()):
- return len(self._parententry(parent))
- def columnCount(self, parent=QModelIndex()):
- return 1
- @pyqtSlot(str)
- def setStatusFilter(self, status):
- """Filter file tree by change status 'MARC'"""
- status = str(status)
- assert util.all(c in 'MARC' for c in status)
- if self._statusfilter == status:
- return # for performance reason
- self._statusfilter = status
- self._buildrootentry()
- @property
- def statusFilter(self):
- """Return the current status filter"""
- return self._statusfilter
- @property
- def _rootentry(self):
- try:
- return self.__rootentry
- except AttributeError:
- self._buildrootentry()
- return self.__rootentry
- def _buildrootentry(self):
- """Rebuild the tree of files and directories"""
- roote = _Entry()
- ctx = self._repo[self._rev]
- status = dict(zip(('M', 'A', 'R'),
- (set(a) for a in self._repo.status(ctx.parents()[0],
- ctx)[:3])))
- uncleanpaths = status['M'] | status['A'] | status['R']
- def pathinstatus(path):
- """Test path is included by the status filter"""
- if util.any(c in self._statusfilter and path in e
- for c, e in status.iteritems()):
- return True
- if 'C' in self._statusfilter and path not in uncleanpaths:
- return True
- return False
- for path in itertools.chain(ctx.manifest(), status['R']):
- if not pathinstatus(path):
- continue
- e = roote
- for p in hglib.tounicode(path).split('/'):
- if not p in e:
- e.addchild(p)
- e = e[p]
- for st, files in status.iteritems():
- if path in files:
- # TODO: what if added & removed at once?
- e.setstatus(st)
- break
- else:
- e.setstatus('C')
- roote.sort()
- self.beginResetModel()
- self.__rootentry = roote
- self.endResetModel()
- def _overlaidicon(base, overlay):
- """Generate overlaid icon"""
- # TODO: generalize this function as a utility
- pixmap = base.pixmap(16, 16)
- painter = QPainter(pixmap)
- painter.setCompositionMode(QPainter.CompositionMode_SourceOver)
- painter.drawPixmap(0, 0, overlay.pixmap(16, 16))
- del painter
- return QIcon(pixmap)
- class _Entry(object):
- """Each file or directory"""
- def __init__(self, name='', parent=None):
- self._name = name
- self._parent = parent
- self._status = None
- self._child = {}
- self._nameindex = []
- @property
- def parent(self):
- return self._parent
- @property
- def path(self):
- if self.parent is None or not self.parent.name:
- return self.name
- else:
- return self.parent.path + '/' + self.name
- @property
- def name(self):
- return self._name
- @property
- def status(self):
- """Return file change status"""
- return self._status
- def setstatus(self, status):
- assert status in 'MARC'
- self._status = status
- def __len__(self):
- return len(self._child)
- def __getitem__(self, name):
- return self._child[name]
- def addchild(self, name):
- if name not in self._child:
- self._nameindex.append(name)
- self._child[name] = self.__class__(name, parent=self)
- def __contains__(self, item):
- return item in self._child
- def at(self, index):
- return self._child[self._nameindex[index]]
- def index(self, name):
- return self._nameindex.index(name)
- def sort(self, reverse=False):
- """Sort the entries recursively; directories first"""
- for e in self._child.itervalues():
- e.sort(reverse=reverse)
- self._nameindex.sort(
- key=lambda s: '%s%s' % (self[s] and 'D' or 'F', s),
- reverse=reverse)