Better diff selection without extra dialog. By @real-yfprojects (#1235)

This commit is contained in:
yfprojects 2022-05-05 09:21:54 +00:00 committed by GitHub
parent add1ac7041
commit b57414cfc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 149 additions and 131 deletions

View File

@ -312,7 +312,7 @@
<string>Compare two archives</string>
</property>
<property name="text">
<string>Diff</string>
<string>Diff</string>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>

View File

@ -5,11 +5,11 @@ from datetime import timedelta
from typing import Dict, Optional
from PyQt5 import QtCore, uic
from PyQt5.QtCore import QMimeData, QPoint, Qt, pyqtSlot
from PyQt5.QtCore import QItemSelectionModel, QMimeData, QPoint, Qt, pyqtSlot
from PyQt5.QtGui import QDesktopServices, QKeySequence
from PyQt5.QtWidgets import (QApplication, QHeaderView, QInputDialog, QLayout,
QMenu, QMessageBox, QShortcut, QTableView,
QTableWidgetItem, QWidget)
from PyQt5.QtWidgets import (QAction, QApplication, QHeaderView, QInputDialog,
QLayout, QMenu, QMessageBox, QShortcut,
QTableView, QTableWidgetItem, QWidget)
from vorta.borg.check import BorgCheckJob
from vorta.borg.compact import BorgCompactJob
@ -27,7 +27,6 @@ from vorta.i18n import trans_late
from vorta.store.models import ArchiveModel, BackupProfileMixin
from vorta.utils import (choose_file_dialog, format_archive_name, get_asset,
get_mount_points, pretty_bytes)
from vorta.views.diff_dialog import DiffDialog
from vorta.views.diff_result import DiffResult
from vorta.views.extract_dialog import ExtractDialog
from vorta.views.source_tab import SizeItem
@ -47,7 +46,8 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
super().__init__(parent)
self.setupUi(parent)
self.mount_points = {} # mount points of archives
self.repo_mount_point: Optional[str] = None # mount point of whole repo
self.repo_mount_point: Optional[
str] = None # mount point of whole repo
self.menu = None
self.app = app
self.toolBox.setCurrentIndex(0)
@ -55,6 +55,7 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
#: Tooltip dict to save the tooltips set in the designer
self.tooltip_dict: Dict[QWidget, str] = {}
self.tooltip_dict[self.bDiff] = self.bDiff.toolTip()
header = self.archiveTable.horizontalHeader()
header.setVisible(True)
@ -69,7 +70,6 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
self._set_status('') # Set platform-specific hints.
self.archiveTable.setSelectionBehavior(QTableView.SelectRows)
self.archiveTable.setSelectionMode(QTableView.SingleSelection)
self.archiveTable.setEditTriggers(QTableView.NoEditTriggers)
self.archiveTable.setWordWrap(False)
self.archiveTable.setTextElideMode(QtCore.Qt.ElideLeft)
@ -86,6 +86,12 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
self.archiveTable)
shortcut_copy.activated.connect(self.archive_copy)
# single and double selection feature
self.archiveTable.setSelectionMode(
QTableView.SelectionMode.ExtendedSelection)
self.archiveTable.selectionModel().selectionChanged.connect(
self.on_selection_change)
# connect archive actions
self.bMountArchive.clicked.connect(self.bmountarchive_clicked)
self.bRefreshArchive.clicked.connect(self.refresh_archive_info)
@ -101,12 +107,12 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
self.bDiff.clicked.connect(self.diff_action)
self.bMountRepo.clicked.connect(self.bmountrepo_clicked)
self.archiveTable.itemSelectionChanged.connect(self.on_selection_change)
self.archiveNameTemplate.textChanged.connect(
lambda tpl, key='new_archive_name': self.save_archive_template(tpl, key))
lambda tpl, key='new_archive_name': self.save_archive_template(
tpl, key))
self.prunePrefixTemplate.textChanged.connect(
lambda tpl, key='prune_prefix': self.save_archive_template(tpl, key))
lambda tpl, key='prune_prefix': self.save_archive_template(
tpl, key))
self.populate_from_profile()
self.selected_archives = None
@ -136,24 +142,50 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
if not index.isValid():
return # popup only for items
menu = QMenu(self.archiveTable)
menu.setEnabled(self.repoactions_enabled)
selected_rows = self.archiveTable.selectionModel().selectedRows(
index.column())
if selected_rows and index not in selected_rows:
return # popup only for selected items
menu = QMenu(self.archiveTable)
menu.addAction(get_colored_icon('copy'), self.tr("Copy"),
lambda: self.archive_copy(index=index))
menu.addSeparator()
menu.addAction(self.bRefreshArchive.icon(),
self.bRefreshArchive.text(),
self.refresh_archive_info)
menu.addAction(self.bMountArchive.icon(), self.bMountArchive.text(),
self.bmountarchive_clicked)
menu.addAction(self.bExtract.icon(), self.bExtract.text(),
self.extract_action)
menu.addAction(self.bRename.icon(), self.bRename.text(),
self.rename_action)
menu.addAction(self.bDelete.icon(), self.bDelete.text(),
self.delete_action)
# archive actions
archive_actions = []
archive_actions.append(
menu.addAction(self.bRefreshArchive.icon(),
self.bRefreshArchive.text(),
self.refresh_archive_info))
archive_actions.append(
menu.addAction(self.bMount.icon(), self.bMount.text(),
self.bmount_clicked))
archive_actions.append(
menu.addAction(self.bExtract.icon(), self.bExtract.text(),
self.extract_action))
archive_actions.append(
menu.addAction(self.bRename.icon(), self.bRename.text(),
self.rename_action))
archive_actions.append(
menu.addAction(self.bDelete.icon(), self.bDelete.text(),
self.delete_action))
if not (self.repoactions_enabled and len(selected_rows) <= 1):
for action in archive_actions:
action.setEnabled(False)
# diff action
menu.addSeparator()
diff_action = QAction(self.bDiff.icon(), self.bDiff.text(), menu)
diff_action.triggered.connect(self.diff_action)
menu.addAction(diff_action)
selected_rows = self.archiveTable.selectionModel().selectedRows(
index.column())
diff_action.setEnabled(self.repoactions_enabled
and len(selected_rows) > 1)
menu.popup(self.archiveTable.viewport().mapToGlobal(pos))
@ -242,25 +274,52 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
self.prune_keep_within.setText(profile.prune_keep_within)
self.prune_keep_within.editingFinished.connect(self.save_prune_setting)
@pyqtSlot()
def on_selection_change(self):
def on_selection_change(self, selected=None, deselected=None):
"""
React to a change of the selection of the archiveTableView.
Enables or disables archive actions and the diff button.
Makes sure at maximum 2 rows are selected.
Parameters
----------
selected : QItemSelection
selected : QItemSelection, optional
The new selection.
deselected : QItemSelection
deselected : QItemSelection, optional
The previous selection.
"""
indexes = self.archiveTable.selectionModel().selectedRows()
# handle selection of more than 2 rows
selectionModel: QItemSelectionModel = self.archiveTable.selectionModel(
)
indexes = selectionModel.selectedRows()
# Toggle archive actions frame
layout: QLayout = self.fArchiveActions.layout()
# Make sure at maximum 2 rows are selected.
if len(indexes) > 2:
selectionModel.select(
indexes[0], QItemSelectionModel.SelectionFlag.Deselect
| QItemSelectionModel.SelectionFlag.Rows)
indexes = selectionModel.selectedRows()
# Toggle diff button
if len(indexes) >= 2:
# Enable diff button
self.bDiff.setEnabled(True)
self.bDiff.setToolTip(self.tooltip_dict.get(self.bDiff, ""))
else:
# disable diff button
self.bDiff.setEnabled(False)
tooltip = self.tooltip_dict[self.bDiff]
self.bDiff.setToolTip(tooltip + " " +
self.tr("(Select two archives)"))
if len(indexes) == 1:
# Enable archive actions
self.fArchiveActions.setEnabled(True)
layout: QLayout = self.fArchiveActions.layout()
for index in range(layout.count()):
widget = layout.itemAt(index).widget()
widget.setToolTip(self.tooltip_dict.get(widget, ""))
@ -271,14 +330,13 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
# too few or too many selected.
self.fArchiveActions.setEnabled(False)
layout: QLayout = self.fArchiveActions.layout()
for index in range(layout.count()):
widget = layout.itemAt(index).widget()
tooltip = widget.toolTip()
tooltip = self.tooltip_dict.setdefault(widget, tooltip)
widget.setToolTip(
tooltip + " " + self.tr("(Select exactly one archive)"))
widget.setToolTip(tooltip + " " +
self.tr("(Select exactly one archive)"))
def archive_copy(self, index=None):
"""
@ -736,41 +794,57 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin):
self._toggle_all_buttons(True)
def diff_action(self):
def process_result():
if window.selected_archives:
self.selected_archives = window.selected_archives
archive_cell_newer = self.archiveTable.item(self.selected_archives[0], 4)
archive_cell_older = self.archiveTable.item(self.selected_archives[1], 4)
if archive_cell_older and archive_cell_newer:
archive_name_newer = archive_cell_newer.text()
archive_name_older = archive_cell_older.text()
params = BorgDiffJob.prepare(profile, archive_name_older, archive_name_newer)
if params['ok']:
self._toggle_all_buttons(False)
job = BorgDiffJob(params['cmd'], params, self.profile().repo.id)
job.updated.connect(self.mountErrors.setText)
job.result.connect(self.list_diff_result)
self.app.jobs_manager.add_job(job)
else:
self._set_status(params['message'])
"""
Handle the diff button being clicked.
Exactly two archives must be selected in `archiveTable`. This is
usually enforced by `on_selection_change`.
"""
selected_archives = self.archiveTable.selectionModel().selectedRows()
profile = self.profile()
window = DiffDialog(self.archiveTable)
self._toggle_all_buttons(True)
window.setParent(self, QtCore.Qt.Sheet)
self._window = window # for testing
window.show()
window.accepted.connect(process_result)
name1 = self.archiveTable.item(selected_archives[0].row(), 4).text()
name2 = self.archiveTable.item(selected_archives[1].row(), 4).text()
archive1, archive2 = (profile.repo.archives.select().where(
(ArchiveModel.name == name1)
| (ArchiveModel.name == name2)).order_by(ArchiveModel.time.desc()))
archive_name_newer = archive1.name
archive_name_older = archive2.name
# Start diff job
params = BorgDiffJob.prepare(profile, archive_name_older,
archive_name_newer)
if params['ok']:
self._toggle_all_buttons(False)
job = BorgDiffJob(params['cmd'], params, self.profile().repo.id)
job.updated.connect(self.mountErrors.setText)
job.result.connect(self.list_diff_result)
self.app.jobs_manager.add_job(job)
else:
self._set_status(params['message'])
def list_diff_result(self, result):
"""
Process the result of the `BorgDiffJob`.
The `BorgDiffJob` was initiated by `diff_action`.
Parameters
----------
result : dict
The BorgJob result.
"""
self._set_status('')
if result['returncode'] == 0:
archive_newer = ArchiveModel.get(name=result['params']['archive_name_newer'])
archive_older = ArchiveModel.get(name=result['params']['archive_name_older'])
window = DiffResult(result['data'], archive_newer, archive_older, result['params']['json_lines'])
archive_newer = ArchiveModel.get(
name=result['params']['archive_name_newer'])
archive_older = ArchiveModel.get(
name=result['params']['archive_name_older'])
window = DiffResult(result['data'], archive_newer, archive_older,
result['params']['json_lines'])
self._toggle_all_buttons(True)
window.setParent(self, QtCore.Qt.Sheet)
self._resultwindow = window # for testing

View File

@ -1,63 +0,0 @@
from PyQt5 import QtCore, uic
from PyQt5.QtWidgets import QHeaderView, QTableView, QTableWidgetItem
from vorta.utils import get_asset
uifile = get_asset("UI/diffdialog.ui")
DiffDialogUI, DiffDialogBase = uic.loadUiType(uifile)
class DiffDialog(DiffDialogBase, DiffDialogUI):
def __init__(self, archiveTable):
super().__init__()
self.setupUi(self)
header = self.archiveTable.horizontalHeader()
header.setVisible(True)
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
header.setSectionResizeMode(3, QHeaderView.Interactive)
header.setSectionResizeMode(4, QHeaderView.Stretch)
header.setStretchLastSection(True)
self.archiveTable.setSelectionBehavior(QTableView.SelectRows)
self.archiveTable.setSelectionMode(QTableView.MultiSelection)
self.archiveTable.setEditTriggers(QTableView.NoEditTriggers)
self.archiveTable.setWordWrap(False)
self.archiveTable.setTextElideMode(QtCore.Qt.ElideLeft)
self.archiveTable.setAlternatingRowColors(True)
self.archiveTable.itemSelectionChanged.connect(self.itemSelectionChanged_action)
# Copy archiveTable of MainWindow
self.archiveTable.setRowCount(archiveTable.rowCount())
for row in range(archiveTable.rowCount()):
for column in range(archiveTable.columnCount()):
try:
text = archiveTable.item(row, column).text()
self.archiveTable.setItem(row, column, QTableWidgetItem(text))
except AttributeError:
self.archiveTable.setItem(row, column, QTableWidgetItem(""))
self.diffButton.setEnabled(False)
self.cancelButton.clicked.connect(self.close)
self.diffButton.clicked.connect(self.diff_action)
self.selected_archives = None
def diff_action(self):
rows_selected = self.archiveTable.selectionModel().selectedRows()
# Makes sure that first element in the tuple is the newer archive
if rows_selected[0].row() < rows_selected[1].row():
self.selected_archives = (rows_selected[0].row(), rows_selected[1].row())
else:
self.selected_archives = (rows_selected[1].row(), rows_selected[0].row())
self.accept()
def itemSelectionChanged_action(self):
if len(self.archiveTable.selectionModel().selectedRows()) == 2:
self.diffButton.setEnabled(True)
else:
self.diffButton.setEnabled(False)

View File

@ -1,7 +1,9 @@
import pytest
from PyQt5.QtCore import QItemSelectionModel
import vorta.borg
import vorta.views.archive_tab
import vorta.utils
import vorta.views.archive_tab
@pytest.mark.parametrize('json_mock_file,folder_root', [
@ -18,12 +20,17 @@ def test_archive_diff(qapp, qtbot, mocker, borg_json_output, json_mock_file, fol
popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0)
mocker.patch.object(vorta.borg.borg_job, 'Popen', return_value=popen_result)
tab.diff_action()
qtbot.waitUntil(lambda: hasattr(tab, '_window'), **pytest._wait_defaults)
selection_model: QItemSelectionModel = tab.archiveTable.selectionModel()
model = tab.archiveTable.model()
flags = QItemSelectionModel.SelectionFlag.Rows
flags |= QItemSelectionModel.SelectionFlag.Select
selection_model.select(model.index(0, 0), flags)
selection_model.select(model.index(1, 0), flags)
tab.diff_action()
tab._window.archiveTable.selectRow(0)
tab._window.archiveTable.selectRow(1)
tab._window.diff_action()
qtbot.waitUntil(lambda: hasattr(tab, '_resultwindow'), **pytest._wait_defaults)
assert tab._resultwindow.treeView.model().rootItem.childItems[0].data(0) == folder_root