eric7/VCS/StatusWidget.py

branch
eric7
changeset 8621
8c9f41115c04
parent 8620
84f7f7867b5f
child 8622
149d51870ce8
equal deleted inserted replaced
8620:84f7f7867b5f 8621:8c9f41115c04
11 import os 11 import os
12 12
13 from PyQt6.QtCore import pyqtSlot, Qt 13 from PyQt6.QtCore import pyqtSlot, Qt
14 from PyQt6.QtWidgets import ( 14 from PyQt6.QtWidgets import (
15 QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QListView, 15 QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QListView,
16 QListWidget, QListWidgetItem, QToolButton, QAbstractItemView 16 QListWidget, QListWidgetItem, QToolButton, QAbstractItemView, QMenu
17 ) 17 )
18 18
19 from EricWidgets.EricApplication import ericApp 19 from EricWidgets.EricApplication import ericApp
20 from EricWidgets import EricMessageBox 20 from EricWidgets import EricMessageBox
21 21
22 import Preferences 22 import Preferences
23 import UI.PixmapCache 23 import UI.PixmapCache
24 import Utilities
24 25
25 26
26 class StatusWidget(QWidget): 27 class StatusWidget(QWidget):
27 """ 28 """
28 Class implementing a VCS Status widget for the sidebar/toolbox. 29 Class implementing a VCS Status widget for the sidebar/toolbox.
29 """ 30 """
30 StatusDataRole = Qt.ItemDataRole.UserRole + 1 31 StatusDataRole = Qt.ItemDataRole.UserRole + 1
31 32
32 def __init__(self, project, parent=None): 33 def __init__(self, project, viewmanager, parent=None):
33 """ 34 """
34 Constructor 35 Constructor
35 36
36 @param project reference to the project object 37 @param project reference to the project object
37 @type Project 38 @type Project
39 @param viewmanager reference to the viewmanager object
40 @type ViewManager
38 @param parent reference to the parent widget (defaults to None) 41 @param parent reference to the parent widget (defaults to None)
39 @type QWidget (optional) 42 @type QWidget (optional)
40 """ 43 """
41 super().__init__(parent) 44 super().__init__(parent)
42 self.setObjectName("VcsStatusWidget") 45 self.setObjectName("VcsStatusWidget")
43 46
44 self.__project = project 47 self.__project = project
48 self.__vm = viewmanager
45 49
46 self.__layout = QVBoxLayout() 50 self.__layout = QVBoxLayout()
47 self.__layout.setObjectName("MainLayout") 51 self.__layout.setObjectName("MainLayout")
48 self.__layout.setContentsMargins(0, 3, 0, 0) 52 self.__layout.setContentsMargins(0, 3, 0, 0)
49 self.__topLayout = QHBoxLayout() 53 self.__topLayout = QHBoxLayout()
80 self.__reloadButton.setIcon(UI.PixmapCache.getIcon("reload")) 84 self.__reloadButton.setIcon(UI.PixmapCache.getIcon("reload"))
81 self.__reloadButton.setToolTip( 85 self.__reloadButton.setToolTip(
82 self.tr("Press to reload the status list")) 86 self.tr("Press to reload the status list"))
83 self.__reloadButton.clicked.connect(self.__reload) 87 self.__reloadButton.clicked.connect(self.__reload)
84 self.__topLayout.addWidget(self.__reloadButton) 88 self.__topLayout.addWidget(self.__reloadButton)
89
90 self.__actionsButton = QToolButton(self)
91 self.__actionsButton.setIcon(
92 UI.PixmapCache.getIcon("actionsToolButton"))
93 self.__actionsButton.setToolTip(
94 self.tr("Select action from menu"))
95 self.__actionsButton.setPopupMode(
96 QToolButton.ToolButtonPopupMode.InstantPopup)
97 self.__topLayout.addWidget(self.__actionsButton)
85 98
86 self.__layout.addLayout(self.__topLayout) 99 self.__layout.addLayout(self.__topLayout)
87 100
88 self.__statusList = QListWidget(self) 101 self.__statusList = QListWidget(self)
89 self.__statusList.setAlternatingRowColors(True) 102 self.__statusList.setAlternatingRowColors(True)
90 self.__statusList.setSortingEnabled(True) 103 self.__statusList.setSortingEnabled(True)
91 self.__statusList.setViewMode(QListView.ViewMode.ListMode) 104 self.__statusList.setViewMode(QListView.ViewMode.ListMode)
92 self.__statusList.setTextElideMode(Qt.TextElideMode.ElideLeft) 105 self.__statusList.setTextElideMode(Qt.TextElideMode.ElideLeft)
93 self.__statusList.setSelectionMode( 106 self.__statusList.setSelectionMode(
94 QAbstractItemView.SelectionMode.ExtendedSelection) 107 QAbstractItemView.SelectionMode.ExtendedSelection)
108 self.__statusList.itemSelectionChanged.connect(
109 self.__updateButtonStates)
95 self.__layout.addWidget(self.__statusList) 110 self.__layout.addWidget(self.__statusList)
96 111
97 self.setLayout(self.__layout) 112 self.setLayout(self.__layout)
98 113
99 self.__statusIcons = { 114 self.__statusIcons = {
115 "Z": self.tr("conflict"), 130 "Z": self.tr("conflict"),
116 "?": self.tr("not tracked"), 131 "?": self.tr("not tracked"),
117 "!": self.tr("missing"), 132 "!": self.tr("missing"),
118 } 133 }
119 134
135 self.__initActionsMenu()
136
137 self.__reset()
138
120 if self.__project.isOpen(): 139 if self.__project.isOpen():
121 self.__projectOpened() 140 self.__projectOpened()
122 else: 141 else:
123 self.__projectClosed() 142 self.__projectClosed()
124 143
127 self.__project.vcsCommitted.connect(self.__committed) 146 self.__project.vcsCommitted.connect(self.__committed)
128 self.__project.vcsStatusMonitorInfo.connect(self.__setInfoText) 147 self.__project.vcsStatusMonitorInfo.connect(self.__setInfoText)
129 self.__project.vcsStatusMonitorAllData.connect( 148 self.__project.vcsStatusMonitorAllData.connect(
130 self.__processStatusData) 149 self.__processStatusData)
131 150
151 def __initActionsMenu(self):
152 """
153 Private method to initialize the actions menu.
154 """
155 self.__actionsMenu = QMenu()
156 self.__actionsMenu.setToolTipsVisible(True)
157 self.__actionsMenu.aboutToShow.connect(self.__showActionsMenu)
158
159 self.__commitAct = self.__actionsMenu.addAction(
160 UI.PixmapCache.getIcon("vcsCommit"),
161 self.tr("Commit"), self.__commit)
162 self.__commitAct.setToolTip(self.tr("Commit the selected changes"))
163 self.__commitSelectAct = self.__actionsMenu.addAction(
164 self.tr("Select all for commit"), self.__commitSelectAll)
165 self.__commitDeselectAct = self.__actionsMenu.addAction(
166 self.tr("Unselect all from commit"), self.__commitDeselectAll)
167
168 self.__actionsMenu.addSeparator()
169
170 self.__addAct = self.__actionsMenu.addAction(
171 UI.PixmapCache.getIcon("vcsAdd"),
172 self.tr("Add"), self.__addUntracked)
173
174 self.__actionsMenu.addSeparator()
175
176 self.__diffAct = self.__actionsMenu.addAction(
177 UI.PixmapCache.getIcon("vcsDiff"),
178 self.tr("Differences"), self.__diff)
179 self.__diffAct.setToolTip(self.tr(
180 "Shows the differences of the selected entry in a"
181 " separate dialog"))
182 self.__sbsDiffAct = self.__actionsMenu.addAction(
183 UI.PixmapCache.getIcon("vcsSbsDiff"),
184 self.tr("Differences Side-By-Side"), self.__sbsDiff)
185 self.__sbsDiffAct.setToolTip(self.tr(
186 "Shows the differences of the selected entry side-by-side in"
187 " a separate dialog"))
188
189 self.__actionsMenu.addSeparator()
190
191 self.__revertAct = self.__actionsMenu.addAction(
192 UI.PixmapCache.getIcon("vcsRevert"),
193 self.tr("Revert"), self.__revert)
194 self.__revertAct.setToolTip(self.tr(
195 "Reverts the changes of the selected files"))
196
197 self.__actionsMenu.addSeparator()
198
199 self.__forgetAct = self.__actionsMenu.addAction(
200 self.tr("Forget Missing"), self.__forgetMissing)
201 self.__forgetAct.setToolTip(self.tr(
202 "Forgets about the selected missing files"))
203 self.__restoreAct = self.__actionsMenu.addAction(
204 self.tr("Restore Missing"), self.__restoreMissing)
205 self.__restoreAct.setToolTip(self.tr(
206 "Restores the selected missing files"))
207 self.__actionsMenu.addSeparator()
208
209 self.__editAct = self.__actionsMenu.addAction(
210 UI.PixmapCache.getIcon("open"),
211 self.tr("Edit Conflict"), self.__editConflict)
212 self.__editAct.setToolTip(self.tr(
213 "Edit the selected conflicting file"))
214 # TODO: add menu entry for 'Conflict Resolved'
215
216 self.__actionsButton.setMenu(self.__actionsMenu)
217
132 @pyqtSlot() 218 @pyqtSlot()
133 def __projectOpened(self): 219 def __projectOpened(self):
134 """ 220 """
135 Private slot to handle the opening of a project. 221 Private slot to handle the opening of a project.
136 """ 222 """
143 """ 229 """
144 self.__infoLabel.setText(self.tr("No project open.")) 230 self.__infoLabel.setText(self.tr("No project open."))
145 231
146 self.__reloadButton.setEnabled(False) 232 self.__reloadButton.setEnabled(False)
147 233
148 self.__statusList.clear() 234 self.__reset()
149 235
150 @pyqtSlot(str) 236 @pyqtSlot(str)
151 def __setInfoText(self, info): 237 def __setInfoText(self, info):
152 """ 238 """
153 Private slot to set the info label text. 239 Private slot to set the info label text.
161 def __reload(self): 247 def __reload(self):
162 """ 248 """
163 Private slot to reload the status list. 249 Private slot to reload the status list.
164 """ 250 """
165 self.__project.checkVCSStatus() 251 self.__project.checkVCSStatus()
252
253 def __reset(self):
254 """
255 Private method to reset the widget to default.
256 """
257 self.__statusList.clear()
258
259 self.__commitToggleButton.setEnabled(False)
260 self.__commitButton.setEnabled(False)
261 self.__addButton.setEnabled(False)
262
263 def __updateButtonStates(self):
264 """
265 Private method to set the button states depending on the list state.
266 """
267 modified = len(self.__getModifiedItems())
268 unversioned = len(self.__getUnversionedItems())
269 commitable = len(self.__getCommitableItems())
270
271 self.__commitToggleButton.setEnabled(modified)
272 self.__commitButton.setEnabled(commitable)
273 self.__addButton.setEnabled(unversioned)
166 274
167 @pyqtSlot(dict) 275 @pyqtSlot(dict)
168 def __processStatusData(self, data): 276 def __processStatusData(self, data):
169 """ 277 """
170 Private slot to process the status data emitted by the project. 278 Private slot to process the status data emitted by the project.
185 </ul> 293 </ul>
186 294
187 @param data dictionary containing the status data 295 @param data dictionary containing the status data
188 @type dict 296 @type dict
189 """ 297 """
190 self.__statusList.clear() 298 self.__reset()
191 299
192 for name, status in data.items(): 300 for name, status in data.items():
193 if status: 301 if status:
194 itm = QListWidgetItem(name, self.__statusList) 302 itm = QListWidgetItem(name, self.__statusList)
195 with contextlib.suppress(KeyError): 303 with contextlib.suppress(KeyError):
204 else: 312 else:
205 itm.setFlags( 313 itm.setFlags(
206 itm.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) 314 itm.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
207 315
208 self.__statusList.sortItems(Qt.SortOrder.AscendingOrder) 316 self.__statusList.sortItems(Qt.SortOrder.AscendingOrder)
317
318 self.__updateButtonStates()
209 319
210 @pyqtSlot() 320 @pyqtSlot()
211 def __toggleCheckMark(self): 321 def __toggleCheckMark(self):
212 """ 322 """
213 Private slot to toggle the check marks. 323 Private slot to toggle the check marks.
221 if itm.checkState() == Qt.CheckState.Unchecked: 331 if itm.checkState() == Qt.CheckState.Unchecked:
222 itm.setCheckState(Qt.CheckState.Checked) 332 itm.setCheckState(Qt.CheckState.Checked)
223 else: 333 else:
224 itm.setCheckState(Qt.CheckState.Unchecked) 334 itm.setCheckState(Qt.CheckState.Unchecked)
225 335
336 def __setCheckMark(self, checked):
337 """
338 Private method to set or unset all check marks.
339
340 @param checked check mark state to be set
341 @type bool
342 """
343 for row in range(self.__statusList.count()):
344 itm = self.__statusList.item(row)
345 if (
346 itm.flags() & Qt.ItemFlag.ItemIsUserCheckable ==
347 Qt.ItemFlag.ItemIsUserCheckable
348 ):
349 if checked:
350 itm.setCheckState(Qt.CheckState.Checked)
351 else:
352 itm.setCheckState(Qt.CheckState.Unchecked)
353
226 @pyqtSlot() 354 @pyqtSlot()
227 def __commit(self): 355 def __commit(self):
228 """ 356 """
229 Private slot to handle the commit button. 357 Private slot to handle the commit button.
230 """ 358 """
257 Private slot called after the commit has been completed. 385 Private slot called after the commit has been completed.
258 """ 386 """
259 self.__reload() 387 self.__reload()
260 388
261 @pyqtSlot() 389 @pyqtSlot()
390 def __commitSelectAll(self):
391 """
392 Private slot to select all entries for commit.
393 """
394 self.__setCheckMark(True)
395
396 @pyqtSlot()
397 def __commitDeselectAll(self):
398 """
399 Private slot to deselect all entries from commit.
400 """
401 self.__setCheckMark(False)
402
403 @pyqtSlot()
262 def __addUntracked(self): 404 def __addUntracked(self):
263 """ 405 """
264 Private slot to add the selected untracked entries. 406 Private slot to add the selected untracked entries.
265 """ 407 """
266 projectPath = self.__project.getProjectPath() 408 projectPath = self.__project.getProjectPath()
267 409
268 names = [ 410 names = [os.path.join(projectPath, itm.text())
269 os.path.join(projectPath, itm.text()) 411 for itm in self.__getUnversionedItems()]
270 for itm in self.__statusList.selectedItems()
271 if itm.data(self.StatusDataRole) == "?"
272 ]
273 412
274 if not names: 413 if not names:
275 EricMessageBox.information( 414 EricMessageBox.information(
276 self, 415 self,
277 self.tr("Add"), 416 self.tr("Add"),
280 return 419 return
281 420
282 vcs = self.__project.getVcs() 421 vcs = self.__project.getVcs()
283 vcs and vcs.vcsAdd(names) 422 vcs and vcs.vcsAdd(names)
284 self.__reload() 423 self.__reload()
424
425 ###########################################################################
426 ## Menu handling methods
427 ###########################################################################
428
429 def __showActionsMenu(self):
430 """
431 Private slot to prepare the actions button menu before it is shown.
432 """
433 modified = len(self.__getSelectedModifiedItems())
434 unversioned = len(self.__getUnversionedItems())
435 missing = len(self.__getMissingItems())
436 commitable = len(self.__getCommitableItems())
437 commitableUnselected = len(self.__getCommitableUnselectedItems())
438 conflicting = len(self.__getSelectedConflictingItems())
439
440 self.__addAct.setEnabled(unversioned)
441 self.__diffAct.setEnabled(modified)
442 self.__sbsDiffAct.setEnabled(modified == 1)
443 self.__revertAct.setEnabled(modified)
444 self.__forgetAct.setEnabled(missing)
445 self.__restoreAct.setEnabled(missing)
446 self.__commitAct.setEnabled(commitable)
447 self.__commitSelectAct.setEnabled(commitableUnselected)
448 self.__commitDeselectAct.setEnabled(commitable)
449 self.__editAct.setEnabled(conflicting == 1)
450
451 def __getCommitableItems(self):
452 """
453 Private method to retrieve all entries the user wants to commit.
454
455 @return list of all items, the user has checked
456 @rtype list of QListWidgetItem
457 """
458 commitableItems = []
459 for row in range(self.__statusList.count()):
460 itm = self.__statusList.item(row)
461 if (
462 itm.checkState() == Qt.CheckState.Checked
463 ):
464 commitableItems.append(itm)
465 return commitableItems
466
467 def __getCommitableUnselectedItems(self):
468 """
469 Private method to retrieve all entries the user may commit but hasn't
470 selected.
471
472 @return list of all items, the user has checked
473 @rtype list of QListWidgetItem
474 """
475 items = []
476 for row in range(self.__statusList.count()):
477 itm = self.__statusList.item(row)
478 if (
479 (itm.flags() & Qt.ItemFlag.ItemIsUserCheckable ==
480 Qt.ItemFlag.ItemIsUserCheckable) and
481 itm.checkState() == Qt.CheckState.Unchecked
482 ):
483 items.append(itm)
484 return items
485
486 def __getModifiedItems(self):
487 """
488 Private method to retrieve all entries, that have a modified status.
489
490 @return list of all items with a modified status
491 @rtype list of QListWidgetItem
492 """
493 items = []
494 for row in range(self.__statusList.count()):
495 itm = self.__statusList.item(row)
496 if itm.data(self.StatusDataRole) in "AMOR":
497 items.append(itm)
498 return items
499
500 def __getSelectedModifiedItems(self):
501 """
502 Private method to retrieve all selected entries, that have a modified
503 status.
504
505 @return list of all selected entries with a modified status
506 @rtype list of QListWidgetItem
507 """
508 return [itm for itm in self.__statusList.selectedItems()
509 if itm.data(self.StatusDataRole) in "AMOR"]
510
511 def __getUnversionedItems(self):
512 """
513 Private method to retrieve all entries, that have an unversioned
514 status.
515
516 @return list of all items with an unversioned status
517 @rtype list of QListWidgetItem
518 """
519 return [itm for itm in self.__statusList.selectedItems()
520 if itm.data(self.StatusDataRole) == "?"]
521
522 def __getMissingItems(self):
523 """
524 Private method to retrieve all entries, that have a missing status.
525
526 @return list of all items with a missing status
527 @rtype list of QListWidgetItem
528 """
529 return [itm for itm in self.__statusList.selectedItems()
530 if itm.data(self.StatusDataRole) == "!"]
531
532 def __getSelectedConflictingItems(self):
533 """
534 Private method to retrieve all selected entries, that have a conflict
535 status.
536
537 @return list of all selected entries with a conflict status
538 @rtype list of QListWidgetItem
539 """
540 return [itm for itm in self.__statusList.selectedItems()
541 if itm.data(self.StatusDataRole) == "Z"]
542
543 @pyqtSlot()
544 def __diff(self):
545 """
546 Private slot to handle the Diff action menu entry.
547 """
548 projectPath = self.__project.getProjectPath()
549
550 names = [os.path.join(projectPath, itm.text())
551 for itm in self.__getSelectedModifiedItems()]
552 if not names:
553 EricMessageBox.information(
554 self,
555 self.tr("Differences"),
556 self.tr("""There are no uncommitted changes"""
557 """ available/selected."""))
558 return
559
560 vcs = self.__project.getVcs()
561 vcs and vcs.vcsDiff(names)
562
563 @pyqtSlot()
564 def __sbsDiff(self):
565 """
566 Private slot to handle the Side-By-Side Diff action menu entry.
567 """
568 projectPath = self.__project.getProjectPath()
569
570 names = [os.path.join(projectPath, itm.text())
571 for itm in self.__getSelectedModifiedItems()]
572 if not names:
573 EricMessageBox.information(
574 self,
575 self.tr("Differences Side-By-Side"),
576 self.tr("""There are no uncommitted changes"""
577 """ available/selected."""))
578 return
579 elif len(names) > 1:
580 EricMessageBox.information(
581 self,
582 self.tr("Differences Side-By-Side"),
583 self.tr("""Only one file with uncommitted changes"""
584 """ must be selected."""))
585 return
586
587 vcs = self.__project.getVcs()
588 vcs and vcs.vcsSbsDiff(names[0])
589
590 def __revert(self):
591 """
592 Private slot to handle the Revert action menu entry.
593 """
594 projectPath = self.__project.getProjectPath()
595
596 names = [os.path.join(projectPath, itm.text())
597 for itm in self.__getSelectedModifiedItems()]
598 if not names:
599 EricMessageBox.information(
600 self,
601 self.tr("Revert"),
602 self.tr("""There are no uncommitted changes"""
603 """ available/selected."""))
604 return
605
606 vcs = self.__project.getVcs()
607 vcs and vcs.vcsRevert(names)
608 self.__reload()
609
610 def __forgetMissing(self):
611 """
612 Private slot to handle the Forget action menu entry.
613 """
614 projectPath = self.__project.getProjectPath()
615
616 names = [os.path.join(projectPath, itm.text())
617 for itm in self.__getMissingItems()]
618 if not names:
619 EricMessageBox.information(
620 self,
621 self.tr("Forget Missing"),
622 self.tr("""There are no missing entries"""
623 """ available/selected."""))
624 return
625
626 vcs = self.__project.getVcs()
627 vcs and vcs.vcsForget(names)
628 self.__reload()
629
630 def __restoreMissing(self):
631 """
632 Private slot to handle the Restore Missing context menu entry.
633 """
634 projectPath = self.__project.getProjectPath()
635
636 names = [os.path.join(projectPath, itm.text())
637 for itm in self.__getMissingItems()]
638 if not names:
639 EricMessageBox.information(
640 self,
641 self.tr("Revert Missing"),
642 self.tr("""There are no missing entries"""
643 """ available/selected."""))
644 return
645
646 vcs = self.__project.getVcs()
647 vcs and vcs.vcsRevert(names)
648 self.__reload()
649
650 def __editConflict(self):
651 """
652 Private slot to handle the Edit Conflict action menu entry.
653 """
654 projectPath = self.__project.getProjectPath()
655
656 itm = self.__getSelectedConflictingItems()[0]
657 filename = os.path.join(projectPath, itm.text())
658 if Utilities.MimeTypes.isTextFile(filename):
659 self.__vm.getEditor(filename)

eric ide

mercurial