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 = { |
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 """ |
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) |