operations.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. ########################################################################
  2. # Wiizard - A Wii games manager
  3. # Copyright (C) 2023 CYBERDEViL
  4. #
  5. # This file is part of Wiizard.
  6. #
  7. # Wiizard is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # Wiizard is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  19. #
  20. ########################################################################
  21. from PyQt5.QtWidgets import (
  22. QDialog,
  23. QVBoxLayout,
  24. QLabel,
  25. QListWidget,
  26. QPushButton,
  27. QTextEdit,
  28. QProgressBar
  29. )
  30. from PyQt5.QtGui import QBrush, QColor
  31. from PyQt5.QtCore import Qt, QVariant
  32. from wiizard.operations import (
  33. GameOperationsThread,
  34. GameOperation,
  35. GAME_OP_DELETE,
  36. GAME_OP_MOVE,
  37. GAME_OP_ADD
  38. )
  39. from wiizard.translations import _
  40. class ApplyChangedDialog(QDialog):
  41. def __init__(self, sourceModels, parent=None):
  42. QDialog.__init__(self, parent=parent)
  43. """
  44. There are {} operations pending.
  45. - {} are new games
  46. - {} are games marked for deletion
  47. Order of execution:
  48. - delete x from y
  49. - delete x from y
  50. """
  51. self.__sourceModels = sourceModels
  52. self.operations = []
  53. self.thread = None
  54. self.setWindowTitle(_("Apply changes"))
  55. self.setMinimumWidth(400)
  56. """
  57. PARTITION A | PARTITION B
  58. GAME_1 4GB | GAME_3 4GB
  59. GAME_2 4GB |
  60. |
  61. free: 0GB used: 8GB | free: 0GB used: 4GB
  62. If we where to swap PARTITION A with PARTITION B the operations list should
  63. look as follows:
  64. - ADD GAME_1 to PARTITION B
  65. - DEL GAME_1 from PARTITION A
  66. - ADD GAME_3 to PARTITION A
  67. - DEL GAME_3 from PARTITION B
  68. - ADD GAME_2 to PARTITION B
  69. - DEL GAME_2 from PARTITION A
  70. """
  71. # Create operations list
  72. # Stage 1: Delete games that are marked for deletion and don't have a
  73. # add operation pending.
  74. # Stage 2: Add games that need to be added but are marked for deletion.
  75. # - After adding a game, delete it from the other partition.
  76. # 1. We need a list of games that we can delete without consequences.
  77. # 2. We need a list of games that have conflicts and sort those.
  78. # Statistics gathering
  79. toDelete = []
  80. toAdd = []
  81. toMove = []
  82. for sourceModel in sourceModels:
  83. toDelete += sourceModel.pendingDelGames
  84. toAdd += sourceModel.pendingAddGames
  85. for game in list(toDelete):
  86. if game in toAdd:
  87. toDelete.remove(game)
  88. toAdd.remove(game)
  89. toMove.append(game)
  90. # Sort sourceModels so we start with the one with the lesser free space.
  91. sourceModels = sorted(sourceModels, key=lambda m: (m.getFree()))
  92. # Stage 1: Add games to the operation list that we can delete right a way
  93. # (these games do not also have a pending copy operation).
  94. for sourceModel in sourceModels:
  95. for game in sourceModel.pendingDelGames:
  96. if game in toDelete:
  97. self.operations.append(GameOperation(game, sourceModel, GAME_OP_DELETE))
  98. # Stage 2: Move games (First copy and then delete)
  99. for sourceModel in sourceModels:
  100. for game in sourceModel.pendingAddGames:
  101. if game in toMove:
  102. self.operations.append(GameOperation(game, sourceModel, GAME_OP_MOVE))
  103. # Stage 3: Add remaining games (Copy)
  104. for sourceModel in sourceModels:
  105. for game in sourceModel.pendingAddGames:
  106. if game in toAdd:
  107. self.operations.append(GameOperation(game, sourceModel, GAME_OP_ADD))
  108. totalOperations = len(self.operations)
  109. self.operationsLeft = totalOperations
  110. layout = QVBoxLayout(self)
  111. self.pendingLabel = QLabel(self)
  112. self.pendingLabel.setWordWrap(True)
  113. self.__reGenPendingLabel()
  114. self.operationsList = QListWidget(self)
  115. # TODO add icons
  116. for operation in self.operations:
  117. if operation.operation == GAME_OP_DELETE:
  118. self.operationsList.addItem(_("Delete \"{}\" from \"{}\"")
  119. .format(operation.game.title,
  120. operation.target.location))
  121. elif operation.operation == GAME_OP_ADD:
  122. self.operationsList.addItem(_("Add \"{}\" to \"{}\" from \"{}\"")
  123. .format(operation.game.title,
  124. operation.target.location,
  125. operation.game.sourceModel.location))
  126. elif operation.operation == GAME_OP_MOVE:
  127. self.operationsList.addItem(_("Move \"{}\" to \"{}\" from \"{}\"")
  128. .format(operation.game.title,
  129. operation.target.location,
  130. operation.game.sourceModel.location))
  131. self.logWidget = QTextEdit(self)
  132. self.logWidget.hide()
  133. self.applyButton = QPushButton(_("Apply"), self)
  134. self.cancelButton = QPushButton(_("Cancel"), self)
  135. self.cancelButton.hide()
  136. self.doneButton = QPushButton(_("Done"), self)
  137. self.doneButton.hide()
  138. self.progressBar = QProgressBar(self)
  139. self.progressBar.setFormat(("Busy please wait.. %v / %m %p%"))
  140. self.progressBar.setMaximum(totalOperations)
  141. self.progressBar.setValue(0)
  142. self.progressBar.hide()
  143. layout.addWidget(self.pendingLabel)
  144. layout.addWidget(self.operationsList)
  145. layout.addWidget(self.logWidget)
  146. layout.addWidget(self.progressBar)
  147. layout.addWidget(self.applyButton)
  148. layout.addWidget(self.cancelButton)
  149. layout.addWidget(self.doneButton)
  150. self.applyButton.clicked.connect(self.onApply)
  151. self.cancelButton.clicked.connect(self.onCancel)
  152. self.doneButton.clicked.connect(self.onDoneClicked)
  153. self.operationsList.currentRowChanged.connect(self.__setLogForOperation)
  154. def closeEvent(self, event):
  155. if self.thread is not None:
  156. self.onCancel()
  157. event.ignore()
  158. return
  159. QDialog.closeEvent(self, event)
  160. def onApply(self):
  161. self.logWidget.show()
  162. self.applyButton.hide()
  163. self.cancelButton.show()
  164. self.progressBar.show()
  165. self.thread = GameOperationsThread(self.operations)
  166. self.thread.progress.connect(self.__onProgressUpdate)
  167. self.thread.completed.connect(self.__onComplete)
  168. self.thread.start()
  169. def onCancel(self):
  170. self.progressBar.setFormat(_("Cancelling please wait on current "
  171. "operation.. %v / %m %p%"))
  172. self.thread.cancel()
  173. def onDoneClicked(self):
  174. self.accept()
  175. def __reGenPendingLabel(self):
  176. if self.operationsLeft == 1:
  177. self.pendingLabel.setText(_("There is 1 operation pending"))
  178. else:
  179. self.pendingLabel.setText(_("There are {} operations pending")
  180. .format(self.operationsLeft))
  181. def __onProgressUpdate(self, progress, errorMessage):
  182. self.progressBar.setValue(progress)
  183. index = progress - 1
  184. # https://doc.qt.io/qt-5/qlistwidgetitem.html
  185. item = self.operationsList.item(index)
  186. if errorMessage:
  187. item.setBackground(QBrush(QColor("#9C0000"), Qt.SolidPattern))
  188. item.setData(Qt.UserRole, QVariant(errorMessage))
  189. #txt = self.logWidget.toPlainText() + "\n" + errorMessage
  190. #self.logWidget.setPlainText(txt)
  191. else:
  192. item.setBackground(QBrush(QColor("#009C00"), Qt.SolidPattern))
  193. item.setData(Qt.UserRole, QVariant("OK"))
  194. self.operationsList.setCurrentRow(index)
  195. self.__setLogForOperation(index)
  196. self.operationsLeft -= 1
  197. self.__reGenPendingLabel()
  198. def __onComplete(self):
  199. self.cancelButton.hide()
  200. self.thread.wait()
  201. self.thread.quit()
  202. self.thread = None
  203. self.doneButton.show()
  204. self.progressBar.setFormat(_("Done %v / %m %p%"))
  205. for sourceModel in self.__sourceModels:
  206. sourceModel.startScan()
  207. def __setLogForOperation(self, index):
  208. item = self.operationsList.item(index)
  209. message = item.data(Qt.UserRole)
  210. self.logWidget.setPlainText(str(message))