/tortoisehg/hgqt/shelve.py

https://bitbucket.org/tortoisehg/hgtk/ · Python · 420 lines · 361 code · 51 blank · 8 comment · 51 complexity · 8d3cf8a7a6652c7db38f492bd66bc2e8 MD5 · raw file

  1. # shelve.py - TortoiseHg shelve and patch tool
  2. #
  3. # Copyright 2011 Steve Borho <steve@borho.org>
  4. #
  5. # This software may be used and distributed according to the terms
  6. # of the GNU General Public License, incorporated herein by reference.
  7. import os
  8. import time
  9. from tortoisehg.util import hglib
  10. from tortoisehg.util.patchctx import patchctx
  11. from tortoisehg.hgqt.i18n import _
  12. from tortoisehg.hgqt import qtlib, cmdui, chunks
  13. from PyQt4.QtCore import *
  14. from PyQt4.QtGui import *
  15. class ShelveDialog(QDialog):
  16. finished = pyqtSignal(int)
  17. wdir = _('Working Directory')
  18. def __init__(self, repo, parent):
  19. QDialog.__init__(self, parent)
  20. self.repo = repo
  21. self.shelves = []
  22. self.patches = []
  23. layout = QVBoxLayout()
  24. layout.setContentsMargins(2, 2, 2, 2)
  25. layout.setSpacing(0)
  26. self.setLayout(layout)
  27. self.tbarhbox = hbox = QHBoxLayout()
  28. hbox.setContentsMargins(0, 0, 0, 0)
  29. hbox.setSpacing(0)
  30. self.layout().addLayout(self.tbarhbox)
  31. self.splitter = QSplitter(self)
  32. self.splitter.setOrientation(Qt.Horizontal)
  33. self.splitter.setChildrenCollapsible(False)
  34. self.splitter.setObjectName('splitter')
  35. self.layout().addWidget(self.splitter, 1)
  36. aframe = QFrame(self.splitter)
  37. avbox = QVBoxLayout()
  38. avbox.setSpacing(2)
  39. avbox.setMargin(2)
  40. avbox.setContentsMargins(2, 2, 2, 2)
  41. aframe.setLayout(avbox)
  42. ahbox = QHBoxLayout()
  43. ahbox.setSpacing(2)
  44. ahbox.setMargin(2)
  45. ahbox.setContentsMargins(2, 2, 2, 2)
  46. avbox.addLayout(ahbox)
  47. self.comboa = QComboBox(self)
  48. self.comboa.currentIndexChanged.connect(self.comboAChanged)
  49. self.delShelfButtonA = QPushButton(_('Delete'))
  50. self.delShelfButtonA.setToolTip(_('Delete the current shelf file'))
  51. self.delShelfButtonA.clicked.connect(self.deleteShelfA)
  52. ahbox.addWidget(self.comboa, 1)
  53. ahbox.addWidget(self.delShelfButtonA)
  54. self.browsea = chunks.ChunksWidget(repo, self)
  55. self.browsea.splitter.splitterMoved.connect(self.linkSplitters)
  56. self.browsea.linkActivated.connect(self.linkActivated)
  57. self.browsea.showMessage.connect(self.showMessage)
  58. avbox.addWidget(self.browsea)
  59. bframe = QFrame(self.splitter)
  60. bvbox = QVBoxLayout()
  61. bvbox.setSpacing(2)
  62. bvbox.setMargin(2)
  63. bvbox.setContentsMargins(2, 2, 2, 2)
  64. bframe.setLayout(bvbox)
  65. bhbox = QHBoxLayout()
  66. bhbox.setSpacing(2)
  67. bhbox.setMargin(2)
  68. bhbox.setContentsMargins(2, 2, 2, 2)
  69. bvbox.addLayout(bhbox)
  70. self.combob = QComboBox(self)
  71. self.combob.currentIndexChanged.connect(self.comboBChanged)
  72. self.delShelfButtonB = QPushButton(_('Delete'))
  73. self.delShelfButtonB.setToolTip(_('Delete the current shelf file'))
  74. self.delShelfButtonB.clicked.connect(self.deleteShelfB)
  75. bhbox.addWidget(self.combob, 1)
  76. bhbox.addWidget(self.delShelfButtonB)
  77. self.browseb = chunks.ChunksWidget(repo, self)
  78. self.browseb.splitter.splitterMoved.connect(self.linkSplitters)
  79. self.browseb.linkActivated.connect(self.linkActivated)
  80. self.browseb.showMessage.connect(self.showMessage)
  81. bvbox.addWidget(self.browseb)
  82. self.lefttbar = QToolBar(_('Left Toolbar'), objectName='lefttbar')
  83. self.tbarhbox.addWidget(self.lefttbar)
  84. self.deletea = a = QAction(_('Deleted selected chunks'), self)
  85. self.deletea.triggered.connect(self.browsea.deleteSelectedChunks)
  86. a.setIcon(qtlib.geticon('delfilesleft'))
  87. self.lefttbar.addAction(self.deletea)
  88. self.allright = a = QAction(_('Move all files right'), self)
  89. self.allright.triggered.connect(self.moveFilesRight)
  90. a.setIcon(qtlib.geticon('all2right'))
  91. self.lefttbar.addAction(self.allright)
  92. self.fileright = a = QAction(_('Move selected file right'), self)
  93. self.fileright.triggered.connect(self.moveFileRight)
  94. a.setIcon(qtlib.geticon('file2right'))
  95. self.lefttbar.addAction(self.fileright)
  96. self.editfilea = a = QAction(_('Edit file'), self)
  97. a.setIcon(qtlib.geticon('edit-find'))
  98. self.lefttbar.addAction(self.editfilea)
  99. self.chunksright = a = QAction(_('Move selected chunks right'), self)
  100. self.chunksright.triggered.connect(self.moveChunksRight)
  101. a.setIcon(qtlib.geticon('chunk2right'))
  102. self.lefttbar.addAction(self.chunksright)
  103. self.rbar = QToolBar(_('Refresh Toolbar'), objectName='rbar')
  104. self.tbarhbox.addStretch(1)
  105. self.tbarhbox.addWidget(self.rbar)
  106. self.refreshAction = a = QAction(_('Refresh'), self)
  107. a.setIcon(qtlib.geticon('reload'))
  108. a.setShortcut(QKeySequence.Refresh)
  109. a.triggered.connect(self.refreshCombos)
  110. self.rbar.addAction(self.refreshAction)
  111. self.actionNew = a = QAction(_('New Shelf'), self)
  112. a.setIcon(qtlib.geticon('document-new'))
  113. a.triggered.connect(self.newShelfPressed)
  114. self.rbar.addAction(self.actionNew)
  115. self.righttbar = QToolBar(_('Right Toolbar'), objectName='righttbar')
  116. self.tbarhbox.addStretch(1)
  117. self.tbarhbox.addWidget(self.righttbar)
  118. self.chunksleft = a = QAction(_('Move selected chunks left'), self)
  119. self.chunksleft.triggered.connect(self.moveChunksLeft)
  120. a.setIcon(qtlib.geticon('chunk2left'))
  121. self.righttbar.addAction(self.chunksleft)
  122. self.editfileb = a = QAction(_('Edit file'), self)
  123. a.setIcon(qtlib.geticon('edit-find'))
  124. self.righttbar.addAction(self.editfileb)
  125. self.fileleft = a = QAction(_('Move selected file left'), self)
  126. self.fileleft.triggered.connect(self.moveFileLeft)
  127. a.setIcon(qtlib.geticon('file2left'))
  128. self.righttbar.addAction(self.fileleft)
  129. self.allleft = a = QAction(_('Move all files left'), self)
  130. self.allleft.triggered.connect(self.moveFilesLeft)
  131. a.setIcon(qtlib.geticon('all2left'))
  132. self.righttbar.addAction(self.allleft)
  133. self.deleteb = a = QAction(_('Deleted selected chunks'), self)
  134. self.deleteb.triggered.connect(self.browseb.deleteSelectedChunks)
  135. a.setIcon(qtlib.geticon('delfilesright'))
  136. self.righttbar.addAction(self.deleteb)
  137. self.editfilea.triggered.connect(self.browsea.editCurrentFile)
  138. self.editfileb.triggered.connect(self.browseb.editCurrentFile)
  139. self.browsea.chunksSelected.connect(self.chunksright.setEnabled)
  140. self.browsea.chunksSelected.connect(self.deletea.setEnabled)
  141. self.browsea.fileSelected.connect(self.fileright.setEnabled)
  142. self.browsea.fileSelected.connect(self.editfilea.setEnabled)
  143. self.browsea.fileModified.connect(self.refreshCombos)
  144. self.browsea.fileModelEmpty.connect(self.allright.setDisabled)
  145. self.browseb.chunksSelected.connect(self.chunksleft.setEnabled)
  146. self.browseb.chunksSelected.connect(self.deleteb.setEnabled)
  147. self.browseb.fileSelected.connect(self.fileleft.setEnabled)
  148. self.browseb.fileSelected.connect(self.editfileb.setEnabled)
  149. self.browseb.fileModified.connect(self.refreshCombos)
  150. self.browseb.fileModelEmpty.connect(self.allleft.setDisabled)
  151. self.statusbar = cmdui.ThgStatusBar(self)
  152. self.layout().addWidget(self.statusbar)
  153. self.refreshCombos()
  154. repo.repositoryChanged.connect(self.refreshCombos)
  155. self.setWindowTitle(_('TortoiseHg Shelve - %s') % repo.displayname)
  156. self.restoreSettings()
  157. @pyqtSlot()
  158. def moveFileRight(self):
  159. if self.combob.currentIndex() == -1:
  160. self.newShelf(False)
  161. file, _ = self.browsea.getSelectedFileAndChunks()
  162. chunks = self.browsea.getChunksForFile(file)
  163. if self.browseb.mergeChunks(file, chunks):
  164. self.browsea.removeFile(file)
  165. @pyqtSlot()
  166. def moveFileLeft(self):
  167. file, _ = self.browseb.getSelectedFileAndChunks()
  168. chunks = self.browseb.getChunksForFile(file)
  169. if self.browsea.mergeChunks(file, chunks):
  170. self.browseb.removeFile(file)
  171. @pyqtSlot()
  172. def moveFilesRight(self):
  173. if self.combob.currentIndex() == -1:
  174. self.newShelf(False)
  175. for file in self.browsea.getFileList():
  176. chunks = self.browsea.getChunksForFile(file)
  177. if self.browseb.mergeChunks(file, chunks):
  178. self.browsea.removeFile(file)
  179. @pyqtSlot()
  180. def moveFilesLeft(self):
  181. for file in self.browseb.getFileList():
  182. chunks = self.browseb.getChunksForFile(file)
  183. if self.browsea.mergeChunks(file, chunks):
  184. self.browseb.removeFile(file)
  185. @pyqtSlot()
  186. def moveChunksRight(self):
  187. if self.combob.currentIndex() == -1:
  188. self.newShelf(False)
  189. file, chunks = self.browsea.getSelectedFileAndChunks()
  190. if self.browseb.mergeChunks(file, chunks):
  191. self.browsea.deleteSelectedChunks()
  192. @pyqtSlot()
  193. def moveChunksLeft(self):
  194. file, chunks = self.browseb.getSelectedFileAndChunks()
  195. if self.browsea.mergeChunks(file, chunks):
  196. self.browseb.deleteSelectedChunks()
  197. @pyqtSlot()
  198. def newShelfPressed(self):
  199. self.newShelf(True)
  200. def newShelf(self, interactive):
  201. shelve = time.strftime('%Y-%m-%d_%H-%M-%S')
  202. if interactive:
  203. dlg = QInputDialog(self, Qt.Sheet)
  204. dlg.setWindowModality(Qt.WindowModal)
  205. dlg.setWindowTitle(_('TortoiseHg New Shelf Name'))
  206. dlg.setLabelText(_('Specify name of new shelf'))
  207. dlg.setTextValue(shelve)
  208. if not dlg.exec_():
  209. return
  210. shelve = hglib.fromunicode(dlg.textValue())
  211. try:
  212. fn = os.path.join('shelves', shelve)
  213. shelfpath = self.repo.join(fn)
  214. if os.path.exists(shelfpath):
  215. qtlib.ErrorMsgBox(_('File already exists'),
  216. _('A shelf file of that name already exists'))
  217. return
  218. self.repo.opener(fn, 'wb').write('')
  219. self.showMessage(_('New shelf created'))
  220. self.refreshCombos()
  221. if shelfpath in self.shelves:
  222. self.combob.setCurrentIndex(self.shelves.index(shelfpath))
  223. except EnvironmentError, e:
  224. self.showMessage(hglib.tounicode(str(e)))
  225. @pyqtSlot()
  226. def deleteShelfA(self):
  227. shelf = self.currentPatchA()
  228. ushelf = hglib.tounicode(os.path.basename(shelf))
  229. if not qtlib.QuestionMsgBox(_('Are you sure?'),
  230. _('Delete shelf file %s?') % ushelf):
  231. return
  232. try:
  233. os.unlink(shelf)
  234. self.showMessage(_('Shelf deleted'))
  235. except EnvironmentError, e:
  236. self.showMessage(hglib.tounicode(str(e)))
  237. self.refreshCombos()
  238. @pyqtSlot()
  239. def deleteShelfB(self):
  240. shelf = self.currentPatchB()
  241. ushelf = hglib.tounicode(os.path.basename(shelf))
  242. if not qtlib.QuestionMsgBox(_('Are you sure?'),
  243. _('Delete shelf file %s?') % ushelf):
  244. return
  245. try:
  246. os.unlink(shelf)
  247. self.showMessage(_('Shelf deleted'))
  248. except EnvironmentError, e:
  249. self.showMessage(hglib.tounicode(str(e)))
  250. self.refreshCombos()
  251. def currentPatchA(self):
  252. idx = self.comboa.currentIndex()
  253. if idx == -1:
  254. return None
  255. if idx == 0:
  256. return self.wdir
  257. idx -= 1
  258. if idx < len(self.shelves):
  259. return self.shelves[idx]
  260. idx -= len(self.shelves)
  261. if idx < len(self.patches):
  262. return self.patches[idx]
  263. return None
  264. def currentPatchB(self):
  265. idx = self.combob.currentIndex()
  266. if idx == -1:
  267. return None
  268. if idx < len(self.shelves):
  269. return self.shelves[idx]
  270. idx -= len(self.shelves)
  271. if idx < len(self.patches):
  272. return self.patches[idx]
  273. return None
  274. @pyqtSlot()
  275. def refreshCombos(self):
  276. shelvea, shelveb = self.currentPatchA(), self.currentPatchB()
  277. shelves = self.repo.thgshelves()
  278. disp = [_('Shelf: %s') % hglib.tounicode(s) for s in shelves]
  279. patches = self.repo.thgmqunappliedpatches
  280. disp += [_('Patch: %s') % hglib.tounicode(p) for p in patches]
  281. # store fully qualified paths
  282. self.shelves = [os.path.join(self.repo.shelfdir, s) for s in shelves]
  283. self.patches = [self.repo.mq.join(p) for p in patches]
  284. self.comboRefreshInProgress = True
  285. self.comboa.clear()
  286. self.combob.clear()
  287. self.comboa.addItems([self.wdir] + disp)
  288. self.combob.addItems(disp)
  289. # attempt to restore selection
  290. if shelvea == self.wdir:
  291. idxa = 0
  292. elif shelvea in self.shelves:
  293. idxa = self.shelves.index(shelvea) + 1
  294. elif shelvea in self.patches:
  295. idxa = len(self.shelves) + self.patches.index(shelvea) + 1
  296. else:
  297. idxa = 0
  298. self.comboa.setCurrentIndex(idxa)
  299. if shelveb in self.shelves:
  300. idxb = self.shelves.index(shelveb)
  301. elif shelveb in self.patches:
  302. idxb = len(self.shelves) + self.patches.index(shelveb)
  303. else:
  304. idxb = 0
  305. self.combob.setCurrentIndex(idxb)
  306. self.comboRefreshInProgress = False
  307. self.comboAChanged(idxa)
  308. self.comboBChanged(idxb)
  309. if not patches and not shelves:
  310. self.delShelfButtonB.setEnabled(False)
  311. self.browseb.setContext(patchctx('', self.repo, None))
  312. @pyqtSlot(int)
  313. def comboAChanged(self, index):
  314. if self.comboRefreshInProgress:
  315. return
  316. if index == 0:
  317. rev = None
  318. self.delShelfButtonA.setEnabled(False)
  319. else:
  320. rev = self.currentPatchA()
  321. self.delShelfButtonA.setEnabled(index <= len(self.shelves))
  322. self.browsea.setContext(self.repo.changectx(rev))
  323. @pyqtSlot(int)
  324. def comboBChanged(self, index):
  325. if self.comboRefreshInProgress:
  326. return
  327. rev = self.currentPatchB()
  328. self.delShelfButtonB.setEnabled(index < len(self.shelves))
  329. self.browseb.setContext(self.repo.changectx(rev))
  330. @pyqtSlot(int, int)
  331. def linkSplitters(self, pos, index):
  332. if self.browsea.splitter.sizes()[0] != pos:
  333. self.browsea.splitter.moveSplitter(pos, index)
  334. if self.browseb.splitter.sizes()[0] != pos:
  335. self.browseb.splitter.moveSplitter(pos, index)
  336. @pyqtSlot(QString)
  337. def linkActivated(self, linktext):
  338. pass
  339. @pyqtSlot(QString)
  340. def showMessage(self, message):
  341. self.statusbar.showMessage(message)
  342. def storeSettings(self):
  343. s = QSettings()
  344. wb = "shelve/"
  345. s.setValue(wb + 'geometry', self.saveGeometry())
  346. s.setValue(wb + 'filesplitter', self.browsea.splitter.saveState())
  347. self.browsea.saveSettings(s, wb + 'fileviewa')
  348. self.browseb.saveSettings(s, wb + 'fileviewb')
  349. def restoreSettings(self):
  350. s = QSettings()
  351. wb = "shelve/"
  352. self.restoreGeometry(s.value(wb + 'geometry').toByteArray())
  353. self.browsea.splitter.restoreState(
  354. s.value(wb + 'filesplitter').toByteArray())
  355. self.browseb.splitter.restoreState(
  356. s.value(wb + 'filesplitter').toByteArray())
  357. self.browsea.loadSettings(s, wb + 'fileviewa')
  358. self.browseb.loadSettings(s, wb + 'fileviewb')
  359. def closeEvent(self, event):
  360. self.storeSettings()
  361. super(ShelveDialog, self).closeEvent(event)
  362. def run(ui, *pats, **opts):
  363. if 'repo' in opts:
  364. repo = opts['repo']
  365. else:
  366. from tortoisehg.util import paths
  367. from tortoisehg.hgqt import thgrepo
  368. repo = thgrepo.repository(ui, path=paths.find_root())
  369. return ShelveDialog(repo, None)