PageRenderTime 114ms CodeModel.GetById 54ms RepoModel.GetById 2ms app.codeStats 0ms

/tortoisehg/hgqt/manifestmodel.py

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