|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2021 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a VCS Status widget for the sidebar/toolbar. |
|
8 """ |
|
9 |
|
10 import contextlib |
|
11 import os |
|
12 |
|
13 from PyQt6.QtCore import pyqtSlot, Qt, QEvent |
|
14 from PyQt6.QtWidgets import ( |
|
15 QWidget, QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy, QListView, |
|
16 QListWidget, QListWidgetItem, QToolButton, QAbstractItemView, QMenu, |
|
17 QGroupBox, QDialog |
|
18 ) |
|
19 |
|
20 from EricWidgets.EricApplication import ericApp |
|
21 from EricWidgets import EricMessageBox |
|
22 from EricWidgets.EricSpellCheckedTextEdit import EricSpellCheckedTextEdit |
|
23 from EricWidgets.EricListSelectionDialog import EricListSelectionDialog |
|
24 |
|
25 import Preferences |
|
26 import UI.PixmapCache |
|
27 import Utilities |
|
28 |
|
29 |
|
30 class StatusWidget(QWidget): |
|
31 """ |
|
32 Class implementing a VCS Status widget for the sidebar/toolbox. |
|
33 """ |
|
34 StatusDataRole = Qt.ItemDataRole.UserRole + 1 |
|
35 |
|
36 def __init__(self, project, viewmanager, parent=None): |
|
37 """ |
|
38 Constructor |
|
39 |
|
40 @param project reference to the project object |
|
41 @type Project |
|
42 @param viewmanager reference to the viewmanager object |
|
43 @type ViewManager |
|
44 @param parent reference to the parent widget (defaults to None) |
|
45 @type QWidget (optional) |
|
46 """ |
|
47 super().__init__(parent) |
|
48 self.setObjectName("VcsStatusWidget") |
|
49 |
|
50 self.__project = project |
|
51 self.__vm = viewmanager |
|
52 |
|
53 self.__layout = QVBoxLayout() |
|
54 self.__layout.setObjectName("MainLayout") |
|
55 self.__layout.setContentsMargins(0, 3, 0, 0) |
|
56 self.__topLayout = QHBoxLayout() |
|
57 self.__topLayout.setObjectName("topLayout") |
|
58 |
|
59 # Create the top area |
|
60 self.__infoLabel = QLabel(self) |
|
61 self.__infoLabel.setSizePolicy( |
|
62 QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) |
|
63 self.__topLayout.addWidget(self.__infoLabel) |
|
64 |
|
65 self.__commitToggleButton = QToolButton(self) |
|
66 self.__commitToggleButton.setIcon(UI.PixmapCache.getIcon("check")) |
|
67 self.__commitToggleButton.setToolTip( |
|
68 self.tr("Press to toggle the commit markers")) |
|
69 self.__commitToggleButton.clicked.connect(self.__toggleCheckMark) |
|
70 self.__topLayout.addWidget(self.__commitToggleButton) |
|
71 |
|
72 self.__commitButton = QToolButton(self) |
|
73 self.__commitButton.setIcon(UI.PixmapCache.getIcon("vcsCommit")) |
|
74 self.__commitButton.setToolTip( |
|
75 self.tr("Press to commit the marked entries with options")) |
|
76 self.__commitButton.clicked.connect(self.__commit) |
|
77 self.__topLayout.addWidget(self.__commitButton) |
|
78 |
|
79 self.__addButton = QToolButton(self) |
|
80 self.__addButton.setIcon(UI.PixmapCache.getIcon("vcsAdd")) |
|
81 self.__addButton.setToolTip( |
|
82 self.tr("Press to add the selected, untracked entries")) |
|
83 self.__addButton.clicked.connect(self.__addUntracked) |
|
84 self.__topLayout.addWidget(self.__addButton) |
|
85 |
|
86 self.__reloadButton = QToolButton(self) |
|
87 self.__reloadButton.setIcon(UI.PixmapCache.getIcon("reload")) |
|
88 self.__reloadButton.setToolTip( |
|
89 self.tr("Press to reload the status list")) |
|
90 self.__reloadButton.clicked.connect(self.__reload) |
|
91 self.__topLayout.addWidget(self.__reloadButton) |
|
92 |
|
93 self.__actionsButton = QToolButton(self) |
|
94 self.__actionsButton.setIcon( |
|
95 UI.PixmapCache.getIcon("actionsToolButton")) |
|
96 self.__actionsButton.setToolTip( |
|
97 self.tr("Select action from menu")) |
|
98 self.__actionsButton.setPopupMode( |
|
99 QToolButton.ToolButtonPopupMode.InstantPopup) |
|
100 self.__topLayout.addWidget(self.__actionsButton) |
|
101 |
|
102 self.__layout.addLayout(self.__topLayout) |
|
103 ################################################################### |
|
104 |
|
105 # Create the middle part |
|
106 self.__statusList = QListWidget(self) |
|
107 self.__statusList.setAlternatingRowColors(True) |
|
108 self.__statusList.setSortingEnabled(True) |
|
109 self.__statusList.setViewMode(QListView.ViewMode.ListMode) |
|
110 self.__statusList.setTextElideMode(Qt.TextElideMode.ElideLeft) |
|
111 self.__statusList.setSelectionMode( |
|
112 QAbstractItemView.SelectionMode.ExtendedSelection) |
|
113 self.__statusList.itemSelectionChanged.connect( |
|
114 self.__updateEnabledStates) |
|
115 self.__statusList.itemDoubleClicked.connect(self.__itemDoubleClicked) |
|
116 self.__statusList.itemChanged.connect(self.__updateEnabledStates) |
|
117 self.__layout.addWidget(self.__statusList) |
|
118 ################################################################### |
|
119 |
|
120 # create the Quick Commit area |
|
121 self.__quickCommitGroup = QGroupBox(self.tr("Quick Commit"), self) |
|
122 self.__quickCommitLayout = QVBoxLayout() |
|
123 self.__quickCommitEdit = EricSpellCheckedTextEdit(self) |
|
124 self.__quickCommitEdit.setSizePolicy( |
|
125 QSizePolicy.Policy.Expanding, |
|
126 QSizePolicy.Policy.Preferred) |
|
127 self.__quickCommitEdit.setMaximumHeight(100) |
|
128 self.__quickCommitEdit.setTabChangesFocus(True) |
|
129 self.__quickCommitEdit.installEventFilter(self) |
|
130 self.__quickCommitEdit.textChanged.connect( |
|
131 self.__quickCommitEditTextChanged) |
|
132 self.__quickCommitLayout.addWidget(self.__quickCommitEdit) |
|
133 |
|
134 self.__quickCommitLayout2 = QHBoxLayout() |
|
135 self.__quickCommitLayout2.addStretch() |
|
136 |
|
137 self.__quickCommitHistoryButton = QToolButton(self) |
|
138 self.__quickCommitHistoryButton.setIcon( |
|
139 UI.PixmapCache.getIcon("history")) |
|
140 self.__quickCommitHistoryButton.setToolTip( |
|
141 self.tr("Select commit message from previous commits")) |
|
142 self.__quickCommitHistoryButton.clicked.connect( |
|
143 self.__selectQuickCommitMessage) |
|
144 self.__quickCommitLayout2.addWidget(self.__quickCommitHistoryButton) |
|
145 |
|
146 self.__quickCommitHistoryClearButton = QToolButton(self) |
|
147 self.__quickCommitHistoryClearButton.setIcon( |
|
148 UI.PixmapCache.getIcon("historyClear")) |
|
149 self.__quickCommitHistoryClearButton.setToolTip( |
|
150 self.tr("Clear the list of saved commit messages")) |
|
151 self.__quickCommitHistoryClearButton.clicked.connect( |
|
152 self.__clearCommitMessages) |
|
153 self.__quickCommitLayout2.addWidget( |
|
154 self.__quickCommitHistoryClearButton) |
|
155 |
|
156 self.__quickCommitButton = QToolButton(self) |
|
157 self.__quickCommitButton.setIcon( |
|
158 UI.PixmapCache.getIcon("vcsCommit")) |
|
159 self.__quickCommitButton.setToolTip( |
|
160 self.tr("Press to commit the marked entries")) |
|
161 self.__quickCommitButton.clicked.connect(self.__quickCommit) |
|
162 self.__quickCommitLayout2.addWidget(self.__quickCommitButton) |
|
163 |
|
164 self.__quickCommitLayout.addLayout(self.__quickCommitLayout2) |
|
165 self.__quickCommitGroup.setLayout(self.__quickCommitLayout) |
|
166 self.__layout.addWidget(self.__quickCommitGroup) |
|
167 ################################################################### |
|
168 |
|
169 self.setLayout(self.__layout) |
|
170 |
|
171 self.__statusIcons = { |
|
172 "A": "vcs-added", # added |
|
173 "M": "vcs-modified", # modified |
|
174 "O": "vcs-removed", # removed |
|
175 "R": "vcs-renamed", # renamed |
|
176 "U": "vcs-update-required", # update needed |
|
177 "Z": "vcs-conflicting", # conflict |
|
178 "?": "vcs-untracked", # not tracked |
|
179 "!": "vcs-missing", # missing |
|
180 } |
|
181 self.__statusTexts = { |
|
182 "A": self.tr("added"), |
|
183 "M": self.tr("modified"), |
|
184 "O": self.tr("removed"), |
|
185 "R": self.tr("renamed"), |
|
186 "U": self.tr("needs update"), |
|
187 "Z": self.tr("conflict"), |
|
188 "?": self.tr("not tracked"), |
|
189 "!": self.tr("missing"), |
|
190 } |
|
191 |
|
192 self.__initActionsMenu() |
|
193 |
|
194 self.__reset() |
|
195 |
|
196 if self.__project.isOpen(): |
|
197 self.__projectOpened() |
|
198 else: |
|
199 self.__projectClosed() |
|
200 |
|
201 self.__addedItemsText = [] |
|
202 |
|
203 self.__project.projectOpened.connect(self.__projectOpened) |
|
204 self.__project.projectClosed.connect(self.__projectClosed) |
|
205 self.__project.projectPropertiesChanged.connect( |
|
206 self.__setProjectSpellCheckData) |
|
207 self.__project.vcsCommitted.connect(self.__committed) |
|
208 self.__project.vcsStatusMonitorInfo.connect(self.__setInfoText) |
|
209 self.__project.vcsStatusMonitorAllData.connect( |
|
210 self.__processStatusData) |
|
211 |
|
212 def __initActionsMenu(self): |
|
213 """ |
|
214 Private method to initialize the actions menu. |
|
215 """ |
|
216 self.__actionsMenu = QMenu() |
|
217 self.__actionsMenu.setToolTipsVisible(True) |
|
218 self.__actionsMenu.aboutToShow.connect(self.__showActionsMenu) |
|
219 |
|
220 self.__commitAct = self.__actionsMenu.addAction( |
|
221 UI.PixmapCache.getIcon("vcsCommit"), |
|
222 self.tr("Commit"), self.__commit) |
|
223 self.__commitAct.setToolTip(self.tr( |
|
224 "Commit the marked entries with options")) |
|
225 self.__commitSelectAct = self.__actionsMenu.addAction( |
|
226 self.tr("Select all for commit"), self.__commitSelectAll) |
|
227 self.__commitDeselectAct = self.__actionsMenu.addAction( |
|
228 self.tr("Unselect all from commit"), self.__commitDeselectAll) |
|
229 |
|
230 self.__actionsMenu.addSeparator() |
|
231 |
|
232 self.__addAct = self.__actionsMenu.addAction( |
|
233 UI.PixmapCache.getIcon("vcsAdd"), |
|
234 self.tr("Add"), self.__addUntracked) |
|
235 self.__addAct.setToolTip(self.tr( |
|
236 "Add the selected, untracked entries")) |
|
237 self.__addAllAct = self.__actionsMenu.addAction( |
|
238 self.tr("Add All"), self.__addAllUntracked) |
|
239 self.__addAllAct.setToolTip(self.tr( |
|
240 "Add all untracked entries")) |
|
241 |
|
242 self.__actionsMenu.addSeparator() |
|
243 |
|
244 self.__diffAct = self.__actionsMenu.addAction( |
|
245 UI.PixmapCache.getIcon("vcsDiff"), |
|
246 self.tr("Differences"), self.__diff) |
|
247 self.__diffAct.setToolTip(self.tr( |
|
248 "Shows the differences of the selected entry in a" |
|
249 " separate dialog")) |
|
250 self.__sbsDiffAct = self.__actionsMenu.addAction( |
|
251 UI.PixmapCache.getIcon("vcsSbsDiff"), |
|
252 self.tr("Differences Side-By-Side"), self.__sbsDiff) |
|
253 self.__sbsDiffAct.setToolTip(self.tr( |
|
254 "Shows the differences of the selected entry side-by-side in" |
|
255 " a separate dialog")) |
|
256 self.__diffAllAct = self.__actionsMenu.addAction( |
|
257 self.tr("All Differences"), self.__diffAll) |
|
258 self.__diffAllAct.setToolTip(self.tr( |
|
259 "Shows the differences of all entries in a separate dialog")) |
|
260 |
|
261 self.__actionsMenu.addSeparator() |
|
262 |
|
263 self.__revertAct = self.__actionsMenu.addAction( |
|
264 UI.PixmapCache.getIcon("vcsRevert"), |
|
265 self.tr("Revert"), self.__revert) |
|
266 self.__revertAct.setToolTip(self.tr( |
|
267 "Reverts the changes of the selected files")) |
|
268 |
|
269 self.__actionsMenu.addSeparator() |
|
270 |
|
271 self.__forgetAct = self.__actionsMenu.addAction( |
|
272 self.tr("Forget Missing"), self.__forgetMissing) |
|
273 self.__forgetAct.setToolTip(self.tr( |
|
274 "Forgets about the selected missing files")) |
|
275 self.__restoreAct = self.__actionsMenu.addAction( |
|
276 self.tr("Restore Missing"), self.__restoreMissing) |
|
277 self.__restoreAct.setToolTip(self.tr( |
|
278 "Restores the selected missing files")) |
|
279 self.__actionsMenu.addSeparator() |
|
280 |
|
281 self.__editAct = self.__actionsMenu.addAction( |
|
282 UI.PixmapCache.getIcon("open"), |
|
283 self.tr("Edit Conflict"), self.__editConflict) |
|
284 self.__editAct.setToolTip(self.tr( |
|
285 "Edit the selected conflicting file")) |
|
286 self.__resolvedAct = self.__actionsMenu.addAction( |
|
287 UI.PixmapCache.getIcon("vcsResolved"), |
|
288 self.tr("Conflict Resolved"), self.__conflictResolved) |
|
289 self.__resolvedAct.setToolTip(self.tr( |
|
290 "Mark the selected conflicting file as resolved")) |
|
291 |
|
292 self.__actionsButton.setMenu(self.__actionsMenu) |
|
293 |
|
294 @pyqtSlot() |
|
295 def __projectOpened(self): |
|
296 """ |
|
297 Private slot to handle the opening of a project. |
|
298 """ |
|
299 self.__reloadButton.setEnabled(True) |
|
300 self.__setProjectSpellCheckData() |
|
301 |
|
302 @pyqtSlot() |
|
303 def __setProjectSpellCheckData(self): |
|
304 """ |
|
305 Private slot to set the spell check properties of the |
|
306 quick commit area. |
|
307 """ |
|
308 pwl, pel = self.__project.getProjectDictionaries() |
|
309 language = self.__project.getProjectSpellLanguage() |
|
310 self.__quickCommitEdit.setLanguageWithPWL( |
|
311 language, pwl or None, pel or None) |
|
312 |
|
313 @pyqtSlot() |
|
314 def __projectClosed(self): |
|
315 """ |
|
316 Private slot to handle the closing of a project. |
|
317 """ |
|
318 self.__infoLabel.setText(self.tr("No project open.")) |
|
319 |
|
320 self.__reloadButton.setEnabled(False) |
|
321 |
|
322 self.__reset() |
|
323 |
|
324 @pyqtSlot(str) |
|
325 def __setInfoText(self, info): |
|
326 """ |
|
327 Private slot to set the info label text. |
|
328 |
|
329 @param info text to be shown |
|
330 @type str |
|
331 """ |
|
332 self.__infoLabel.setText(info) |
|
333 |
|
334 @pyqtSlot() |
|
335 def __reload(self): |
|
336 """ |
|
337 Private slot to reload the status list. |
|
338 """ |
|
339 self.__project.checkVCSStatus() |
|
340 |
|
341 def __reset(self): |
|
342 """ |
|
343 Private method to reset the widget to default. |
|
344 """ |
|
345 self.__statusList.clear() |
|
346 |
|
347 self.__commitToggleButton.setEnabled(False) |
|
348 self.__commitButton.setEnabled(False) |
|
349 self.__addButton.setEnabled(False) |
|
350 |
|
351 self.__quickCommitEdit.clear() |
|
352 self.__quickCommitGroup.setEnabled(False) |
|
353 |
|
354 def __updateEnabledStates(self): |
|
355 """ |
|
356 Private method to set the enabled states depending on the list state. |
|
357 """ |
|
358 modified = len(self.__getModifiedItems()) |
|
359 unversioned = len(self.__getSelectedUnversionedItems()) |
|
360 commitable = len(self.__getCommitableItems()) |
|
361 |
|
362 self.__commitToggleButton.setEnabled(modified) |
|
363 self.__commitButton.setEnabled(commitable) |
|
364 self.__addButton.setEnabled(unversioned) |
|
365 |
|
366 self.__quickCommitGroup.setEnabled(commitable) |
|
367 |
|
368 @pyqtSlot(dict) |
|
369 def __processStatusData(self, data): |
|
370 """ |
|
371 Private slot to process the status data emitted by the project. |
|
372 |
|
373 Each entry of the status data consists of a status flag and and the |
|
374 path relative to the project directory starting with the third column. |
|
375 The known status flags are: |
|
376 <ul> |
|
377 <li>"A" path was added but not yet committed</li> |
|
378 <li>"M" path has local changes</li> |
|
379 <li>"O" path was removed</li> |
|
380 <li>"R" path was deleted and then re-added</li> |
|
381 <li>"U" path needs an update</li> |
|
382 <li>"Z" path contains a conflict</li> |
|
383 <li>"?" path is not tracked</li> |
|
384 <li>"!" path is missing</li> |
|
385 <li>" " path is back at normal</li> |
|
386 </ul> |
|
387 |
|
388 @param data dictionary containing the status data |
|
389 @type dict |
|
390 """ |
|
391 # step 1: remember all currently checked entries |
|
392 checkedEntries = [itm.text() for itm in self.__getCommitableItems()] |
|
393 selectedEntries = [itm.text() |
|
394 for itm in self.__statusList.selectedItems()] |
|
395 knownEntries = [self.__statusList.item(row).text() |
|
396 for row in range(self.__statusList.count())] |
|
397 |
|
398 # step 2: clear the list and re-populate it with new data |
|
399 self.__statusList.clear() |
|
400 |
|
401 block = self.__statusList.blockSignals(True) |
|
402 for name, status in data.items(): |
|
403 if status: |
|
404 itm = QListWidgetItem(name, self.__statusList) |
|
405 with contextlib.suppress(KeyError): |
|
406 itm.setToolTip(self.__statusTexts[status]) |
|
407 itm.setIcon(UI.PixmapCache.getIcon( |
|
408 self.__statusIcons[status])) |
|
409 itm.setData(self.StatusDataRole, status) |
|
410 if status in "AMOR": |
|
411 itm.setFlags( |
|
412 itm.flags() | Qt.ItemFlag.ItemIsUserCheckable) |
|
413 if ( |
|
414 name in checkedEntries or |
|
415 name not in knownEntries or |
|
416 name in self.__addedItemsText |
|
417 ): |
|
418 itm.setCheckState(Qt.CheckState.Checked) |
|
419 else: |
|
420 itm.setCheckState(Qt.CheckState.Unchecked) |
|
421 else: |
|
422 itm.setFlags( |
|
423 itm.flags() & ~Qt.ItemFlag.ItemIsUserCheckable) |
|
424 itm.setSelected(name in selectedEntries) |
|
425 |
|
426 self.__statusList.sortItems(Qt.SortOrder.AscendingOrder) |
|
427 self.__statusList.blockSignals(block) |
|
428 |
|
429 self.__updateEnabledStates() |
|
430 |
|
431 @pyqtSlot() |
|
432 def __toggleCheckMark(self): |
|
433 """ |
|
434 Private slot to toggle the check marks. |
|
435 """ |
|
436 itemList = ( |
|
437 self.__statusList.selectedItems() |
|
438 if len(self.__statusList.selectedItems()) else |
|
439 [self.__statusList.item(row) |
|
440 for row in range(self.__statusList.count())] |
|
441 ) |
|
442 for itm in itemList: |
|
443 if ( |
|
444 itm.flags() & Qt.ItemFlag.ItemIsUserCheckable == |
|
445 Qt.ItemFlag.ItemIsUserCheckable |
|
446 ): |
|
447 if itm.checkState() == Qt.CheckState.Unchecked: |
|
448 itm.setCheckState(Qt.CheckState.Checked) |
|
449 else: |
|
450 itm.setCheckState(Qt.CheckState.Unchecked) |
|
451 |
|
452 def __setCheckMark(self, checked): |
|
453 """ |
|
454 Private method to set or unset all check marks. |
|
455 |
|
456 @param checked check mark state to be set |
|
457 @type bool |
|
458 """ |
|
459 for row in range(self.__statusList.count()): |
|
460 itm = self.__statusList.item(row) |
|
461 if ( |
|
462 itm.flags() & Qt.ItemFlag.ItemIsUserCheckable == |
|
463 Qt.ItemFlag.ItemIsUserCheckable |
|
464 ): |
|
465 if checked: |
|
466 itm.setCheckState(Qt.CheckState.Checked) |
|
467 else: |
|
468 itm.setCheckState(Qt.CheckState.Unchecked) |
|
469 |
|
470 @pyqtSlot() |
|
471 def __commit(self): |
|
472 """ |
|
473 Private slot to handle the commit button. |
|
474 """ |
|
475 projectPath = self.__project.getProjectPath() |
|
476 names = [] |
|
477 |
|
478 for row in range(self.__statusList.count()): |
|
479 itm = self.__statusList.item(row) |
|
480 if itm.checkState() == Qt.CheckState.Checked: |
|
481 names.append(os.path.join(projectPath, itm.text())) |
|
482 |
|
483 if not names: |
|
484 EricMessageBox.information( |
|
485 self, |
|
486 self.tr("Commit"), |
|
487 self.tr("""There are no entries selected to be""" |
|
488 """ committed.""")) |
|
489 return |
|
490 |
|
491 if Preferences.getVCS("AutoSaveFiles"): |
|
492 vm = ericApp().getObject("ViewManager") |
|
493 for name in names: |
|
494 vm.saveEditor(name) |
|
495 vcs = self.__project.getVcs() |
|
496 vcs and vcs.vcsCommit(names, '') |
|
497 |
|
498 @pyqtSlot() |
|
499 def __committed(self): |
|
500 """ |
|
501 Private slot called after the commit has been completed. |
|
502 """ |
|
503 self.__reload() |
|
504 |
|
505 @pyqtSlot() |
|
506 def __commitSelectAll(self): |
|
507 """ |
|
508 Private slot to select all entries for commit. |
|
509 """ |
|
510 self.__setCheckMark(True) |
|
511 |
|
512 @pyqtSlot() |
|
513 def __commitDeselectAll(self): |
|
514 """ |
|
515 Private slot to deselect all entries from commit. |
|
516 """ |
|
517 self.__setCheckMark(False) |
|
518 |
|
519 @pyqtSlot() |
|
520 def __addUntracked(self, allItems=False): |
|
521 """ |
|
522 Private slot to add the selected untracked entries. |
|
523 |
|
524 @param allItems flag indicating to show the differences of all files |
|
525 (defaults to False) |
|
526 @type bool (optional) |
|
527 """ |
|
528 projectPath = self.__project.getProjectPath() |
|
529 |
|
530 names = [ |
|
531 os.path.join(projectPath, itm.text()) |
|
532 for itm in self.__getUnversionedItems() |
|
533 ] if allItems else [ |
|
534 os.path.join(projectPath, itm.text()) |
|
535 for itm in self.__getSelectedUnversionedItems() |
|
536 ] |
|
537 |
|
538 if not names: |
|
539 EricMessageBox.information( |
|
540 self, |
|
541 self.tr("Add"), |
|
542 self.tr("""There are no unversioned entries""" |
|
543 """ available/selected.""")) |
|
544 return |
|
545 |
|
546 self.__addedItemsText = [ |
|
547 itm.text() for itm in self.__getUnversionedItems() |
|
548 ] if allItems else [ |
|
549 itm.text() for itm in self.__getSelectedUnversionedItems() |
|
550 ] |
|
551 |
|
552 vcs = self.__project.getVcs() |
|
553 vcs and vcs.vcsAdd(names) |
|
554 self.__reload() |
|
555 |
|
556 @pyqtSlot(QListWidgetItem) |
|
557 def __itemDoubleClicked(self, itm): |
|
558 """ |
|
559 Private slot to handle double clicking an item. |
|
560 |
|
561 @param itm reference to the double clicked item |
|
562 @type QListWidgetItem |
|
563 """ |
|
564 projectPath = self.__project.getProjectPath() |
|
565 |
|
566 if itm.data(self.StatusDataRole) in "MZ": |
|
567 # modified and conflicting items |
|
568 name = os.path.join(projectPath, itm.text()) |
|
569 vcs = self.__project.getVcs() |
|
570 vcs and vcs.vcsDiff(name) |
|
571 |
|
572 ########################################################################### |
|
573 ## Menu handling methods |
|
574 ########################################################################### |
|
575 |
|
576 def __showActionsMenu(self): |
|
577 """ |
|
578 Private slot to prepare the actions button menu before it is shown. |
|
579 """ |
|
580 modified = len(self.__getSelectedModifiedItems()) |
|
581 allModified = len(self.__getModifiedItems()) |
|
582 unversioned = len(self.__getSelectedUnversionedItems()) |
|
583 allUnversioned = len(self.__getUnversionedItems()) |
|
584 missing = len(self.__getMissingItems()) |
|
585 commitable = len(self.__getCommitableItems()) |
|
586 commitableUnselected = len(self.__getCommitableUnselectedItems()) |
|
587 conflicting = len(self.__getSelectedConflictingItems()) |
|
588 |
|
589 self.__addAct.setEnabled(unversioned) |
|
590 self.__addAllAct.setEnabled(allUnversioned) |
|
591 self.__diffAct.setEnabled(modified) |
|
592 self.__sbsDiffAct.setEnabled(modified == 1) |
|
593 self.__diffAllAct.setEnabled(allModified) |
|
594 self.__revertAct.setEnabled(modified) |
|
595 self.__forgetAct.setEnabled(missing) |
|
596 self.__restoreAct.setEnabled(missing) |
|
597 self.__commitAct.setEnabled(commitable) |
|
598 self.__commitSelectAct.setEnabled(commitableUnselected) |
|
599 self.__commitDeselectAct.setEnabled(commitable) |
|
600 self.__editAct.setEnabled(conflicting == 1) |
|
601 self.__resolvedAct.setEnabled(conflicting) |
|
602 |
|
603 def __getCommitableItems(self): |
|
604 """ |
|
605 Private method to retrieve all entries the user wants to commit. |
|
606 |
|
607 @return list of all items, the user has checked |
|
608 @rtype list of QListWidgetItem |
|
609 """ |
|
610 commitableItems = [] |
|
611 for row in range(self.__statusList.count()): |
|
612 itm = self.__statusList.item(row) |
|
613 if ( |
|
614 itm.checkState() == Qt.CheckState.Checked |
|
615 ): |
|
616 commitableItems.append(itm) |
|
617 return commitableItems |
|
618 |
|
619 def __getCommitableUnselectedItems(self): |
|
620 """ |
|
621 Private method to retrieve all entries the user may commit but hasn't |
|
622 selected. |
|
623 |
|
624 @return list of all items, the user has checked |
|
625 @rtype list of QListWidgetItem |
|
626 """ |
|
627 items = [] |
|
628 for row in range(self.__statusList.count()): |
|
629 itm = self.__statusList.item(row) |
|
630 if ( |
|
631 (itm.flags() & Qt.ItemFlag.ItemIsUserCheckable == |
|
632 Qt.ItemFlag.ItemIsUserCheckable) and |
|
633 itm.checkState() == Qt.CheckState.Unchecked |
|
634 ): |
|
635 items.append(itm) |
|
636 return items |
|
637 |
|
638 def __getModifiedItems(self): |
|
639 """ |
|
640 Private method to retrieve all entries, that have a modified status. |
|
641 |
|
642 @return list of all items with a modified status |
|
643 @rtype list of QListWidgetItem |
|
644 """ |
|
645 items = [] |
|
646 for row in range(self.__statusList.count()): |
|
647 itm = self.__statusList.item(row) |
|
648 if itm.data(self.StatusDataRole) in "AMOR": |
|
649 items.append(itm) |
|
650 return items |
|
651 |
|
652 def __getSelectedModifiedItems(self): |
|
653 """ |
|
654 Private method to retrieve all selected entries, that have a modified |
|
655 status. |
|
656 |
|
657 @return list of all selected entries with a modified status |
|
658 @rtype list of QListWidgetItem |
|
659 """ |
|
660 return [itm for itm in self.__statusList.selectedItems() |
|
661 if itm.data(self.StatusDataRole) in "AMOR"] |
|
662 |
|
663 def __getUnversionedItems(self): |
|
664 """ |
|
665 Private method to retrieve all entries, that have an unversioned |
|
666 status. |
|
667 |
|
668 @return list of all items with an unversioned status |
|
669 @rtype list of QListWidgetItem |
|
670 """ |
|
671 items = [] |
|
672 for row in range(self.__statusList.count()): |
|
673 itm = self.__statusList.item(row) |
|
674 if itm.data(self.StatusDataRole) == "?": |
|
675 items.append(itm) |
|
676 return items |
|
677 |
|
678 def __getSelectedUnversionedItems(self): |
|
679 """ |
|
680 Private method to retrieve all selected entries, that have an |
|
681 unversioned status. |
|
682 |
|
683 @return list of all items with an unversioned status |
|
684 @rtype list of QListWidgetItem |
|
685 """ |
|
686 return [itm for itm in self.__statusList.selectedItems() |
|
687 if itm.data(self.StatusDataRole) == "?"] |
|
688 |
|
689 def __getMissingItems(self): |
|
690 """ |
|
691 Private method to retrieve all entries, that have a missing status. |
|
692 |
|
693 @return list of all items with a missing status |
|
694 @rtype list of QListWidgetItem |
|
695 """ |
|
696 return [itm for itm in self.__statusList.selectedItems() |
|
697 if itm.data(self.StatusDataRole) == "!"] |
|
698 |
|
699 def __getSelectedConflictingItems(self): |
|
700 """ |
|
701 Private method to retrieve all selected entries, that have a conflict |
|
702 status. |
|
703 |
|
704 @return list of all selected entries with a conflict status |
|
705 @rtype list of QListWidgetItem |
|
706 """ |
|
707 return [itm for itm in self.__statusList.selectedItems() |
|
708 if itm.data(self.StatusDataRole) == "Z"] |
|
709 |
|
710 @pyqtSlot() |
|
711 def __addAllUntracked(self): |
|
712 """ |
|
713 Private slot to handle the Add All action menu entry. |
|
714 """ |
|
715 self.__addUntracked(allItems=True) |
|
716 |
|
717 @pyqtSlot() |
|
718 def __diff(self, allItems=False): |
|
719 """ |
|
720 Private slot to handle the Differences action menu entry. |
|
721 |
|
722 @param allItems flag indicating to show the differences of all files |
|
723 (defaults to False) |
|
724 @type bool (optional) |
|
725 """ |
|
726 projectPath = self.__project.getProjectPath() |
|
727 |
|
728 names = [ |
|
729 os.path.join(projectPath, itm.text()) |
|
730 for itm in self.__getModifiedItems() |
|
731 ] if allItems else [ |
|
732 os.path.join(projectPath, itm.text()) |
|
733 for itm in self.__getSelectedModifiedItems() |
|
734 ] |
|
735 if not names: |
|
736 EricMessageBox.information( |
|
737 self, |
|
738 self.tr("Differences"), |
|
739 self.tr("""There are no uncommitted changes""" |
|
740 """ available/selected.""")) |
|
741 return |
|
742 |
|
743 vcs = self.__project.getVcs() |
|
744 vcs and vcs.vcsDiff(names) |
|
745 |
|
746 @pyqtSlot() |
|
747 def __diffAll(self): |
|
748 """ |
|
749 Private slot to handle the All Differences action menu entry. |
|
750 """ |
|
751 self.__diff(allItems=True) |
|
752 |
|
753 @pyqtSlot() |
|
754 def __sbsDiff(self): |
|
755 """ |
|
756 Private slot to handle the Side-By-Side Differences action menu entry. |
|
757 """ |
|
758 projectPath = self.__project.getProjectPath() |
|
759 |
|
760 names = [os.path.join(projectPath, itm.text()) |
|
761 for itm in self.__getSelectedModifiedItems()] |
|
762 if not names: |
|
763 EricMessageBox.information( |
|
764 self, |
|
765 self.tr("Differences Side-By-Side"), |
|
766 self.tr("""There are no uncommitted changes""" |
|
767 """ available/selected.""")) |
|
768 return |
|
769 elif len(names) > 1: |
|
770 EricMessageBox.information( |
|
771 self, |
|
772 self.tr("Differences Side-By-Side"), |
|
773 self.tr("""Only one file with uncommitted changes""" |
|
774 """ must be selected.""")) |
|
775 return |
|
776 |
|
777 vcs = self.__project.getVcs() |
|
778 vcs and vcs.vcsSbsDiff(names[0]) |
|
779 |
|
780 @pyqtSlot() |
|
781 def __revert(self): |
|
782 """ |
|
783 Private slot to handle the Revert action menu entry. |
|
784 """ |
|
785 projectPath = self.__project.getProjectPath() |
|
786 |
|
787 names = [os.path.join(projectPath, itm.text()) |
|
788 for itm in self.__getSelectedModifiedItems()] |
|
789 if not names: |
|
790 EricMessageBox.information( |
|
791 self, |
|
792 self.tr("Revert"), |
|
793 self.tr("""There are no uncommitted changes""" |
|
794 """ available/selected.""")) |
|
795 return |
|
796 |
|
797 vcs = self.__project.getVcs() |
|
798 vcs and vcs.vcsRevert(names) |
|
799 self.__reload() |
|
800 |
|
801 @pyqtSlot() |
|
802 def __forgetMissing(self): |
|
803 """ |
|
804 Private slot to handle the Forget action menu entry. |
|
805 """ |
|
806 projectPath = self.__project.getProjectPath() |
|
807 |
|
808 names = [os.path.join(projectPath, itm.text()) |
|
809 for itm in self.__getMissingItems()] |
|
810 if not names: |
|
811 EricMessageBox.information( |
|
812 self, |
|
813 self.tr("Forget Missing"), |
|
814 self.tr("""There are no missing entries""" |
|
815 """ available/selected.""")) |
|
816 return |
|
817 |
|
818 vcs = self.__project.getVcs() |
|
819 vcs and vcs.vcsForget(names) |
|
820 self.__reload() |
|
821 |
|
822 @pyqtSlot() |
|
823 def __restoreMissing(self): |
|
824 """ |
|
825 Private slot to handle the Restore Missing context menu entry. |
|
826 """ |
|
827 projectPath = self.__project.getProjectPath() |
|
828 |
|
829 names = [os.path.join(projectPath, itm.text()) |
|
830 for itm in self.__getMissingItems()] |
|
831 if not names: |
|
832 EricMessageBox.information( |
|
833 self, |
|
834 self.tr("Restore Missing"), |
|
835 self.tr("""There are no missing entries""" |
|
836 """ available/selected.""")) |
|
837 return |
|
838 |
|
839 vcs = self.__project.getVcs() |
|
840 vcs and vcs.vcsRevert(names) |
|
841 self.__reload() |
|
842 |
|
843 @pyqtSlot() |
|
844 def __editConflict(self): |
|
845 """ |
|
846 Private slot to handle the Edit Conflict action menu entry. |
|
847 """ |
|
848 projectPath = self.__project.getProjectPath() |
|
849 |
|
850 itm = self.__getSelectedConflictingItems()[0] |
|
851 filename = os.path.join(projectPath, itm.text()) |
|
852 if Utilities.MimeTypes.isTextFile(filename): |
|
853 self.__vm.getEditor(filename) |
|
854 |
|
855 @pyqtSlot() |
|
856 def __conflictResolved(self): |
|
857 """ |
|
858 Private slot to handle the Conflict Resolved action menu entry. |
|
859 """ |
|
860 projectPath = self.__project.getProjectPath() |
|
861 |
|
862 names = [os.path.join(projectPath, itm.text()) |
|
863 for itm in self.__getSelectedConflictingItems()] |
|
864 if not names: |
|
865 EricMessageBox.information( |
|
866 self, |
|
867 self.tr("Conflict Resolved"), |
|
868 self.tr("""There are no conflicting entries""" |
|
869 """ available/selected.""")) |
|
870 return |
|
871 |
|
872 vcs = self.__project.getVcs() |
|
873 vcs and vcs.vcsResolved(names) |
|
874 self.__reload() |
|
875 |
|
876 ####################################################################### |
|
877 ## Quick Commit handling methods |
|
878 ####################################################################### |
|
879 |
|
880 @pyqtSlot() |
|
881 def __selectQuickCommitMessage(self): |
|
882 """ |
|
883 Private slot to select a commit message from the list of |
|
884 saved messages. |
|
885 """ |
|
886 vcs = self.__project.getVcs() |
|
887 if vcs: |
|
888 commitMessages = vcs.vcsCommitMessages() |
|
889 dlg = EricListSelectionDialog( |
|
890 commitMessages, |
|
891 selectionMode=QAbstractItemView.SelectionMode.SingleSelection, |
|
892 title=self.tr("Quick Commit"), |
|
893 message=self.tr("Select your commit message:"), |
|
894 doubleClickOk=True, |
|
895 parent=self |
|
896 ) |
|
897 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
898 selection = dlg.getSelection() |
|
899 if selection: |
|
900 self.__quickCommitEdit.setPlainText(selection[0]) |
|
901 |
|
902 @pyqtSlot() |
|
903 def __clearCommitMessages(self): |
|
904 """ |
|
905 Private slot to clear the list of saved commit messages. |
|
906 """ |
|
907 vcs = self.__project.getVcs() |
|
908 vcs and vcs.vcsClearCommitMessages() |
|
909 |
|
910 @pyqtSlot() |
|
911 def __quickCommit(self): |
|
912 """ |
|
913 Private slot to commit all marked entries with the entered |
|
914 commit message. |
|
915 """ |
|
916 projectPath = self.__project.getProjectPath() |
|
917 names = [] |
|
918 |
|
919 for row in range(self.__statusList.count()): |
|
920 itm = self.__statusList.item(row) |
|
921 if itm.checkState() == Qt.CheckState.Checked: |
|
922 names.append(os.path.join(projectPath, itm.text())) |
|
923 |
|
924 if not names: |
|
925 EricMessageBox.information( |
|
926 self, |
|
927 self.tr("Commit"), |
|
928 self.tr("""There are no entries selected to be""" |
|
929 """ committed.""")) |
|
930 return |
|
931 |
|
932 if Preferences.getVCS("AutoSaveFiles"): |
|
933 vm = ericApp().getObject("ViewManager") |
|
934 for name in names: |
|
935 vm.saveEditor(name) |
|
936 |
|
937 commitMessage = self.__quickCommitEdit.toPlainText() |
|
938 vcs = self.__project.getVcs() |
|
939 if vcs: |
|
940 vcs.vcsCommit(names, commitMessage, noDialog=True) |
|
941 vcs.vcsAddCommitMessage(commitMessage) |
|
942 self.__quickCommitEdit.clear() |
|
943 |
|
944 @pyqtSlot() |
|
945 def __quickCommitEditTextChanged(self): |
|
946 """ |
|
947 Private slot to react upon changes of the quick commit text. |
|
948 """ |
|
949 self.__quickCommitButton.setEnabled(bool( |
|
950 self.__quickCommitEdit.toPlainText())) |
|
951 |
|
952 def eventFilter(self, obj, evt): |
|
953 """ |
|
954 Public method to process some events for the Commit edit. |
|
955 |
|
956 @param obj reference to the object the event was meant for |
|
957 @type QObject |
|
958 @param evt reference to the event object |
|
959 @type QEvent |
|
960 @return flag to indicate that the event was handled |
|
961 @rtype bool |
|
962 """ |
|
963 if ( |
|
964 obj is self.__quickCommitEdit and |
|
965 evt.type() == QEvent.Type.KeyPress and |
|
966 evt.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter) and |
|
967 evt.modifiers() == Qt.KeyboardModifier.ControlModifier |
|
968 ): |
|
969 # Ctrl-Enter or Ctrl-Return => commit |
|
970 self.__quickCommitButton.animateClick() |
|
971 return True |
|
972 else: |
|
973 # standard event processing |
|
974 return super().eventFilter(obj, evt) |