mirror of https://github.com/borgbase/vorta
Better diff selection without extra dialog. By @real-yfprojects (#1235)
This commit is contained in:
parent
add1ac7041
commit
b57414cfc2
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue