diff --git a/src/vorta/assets/UI/extractdialog.ui b/src/vorta/assets/UI/extractdialog.ui index 6fba2640..77d745fd 100644 --- a/src/vorta/assets/UI/extractdialog.ui +++ b/src/vorta/assets/UI/extractdialog.ui @@ -52,6 +52,58 @@ + + + + Qt::NoFocus + + + Keep folders on top when sorting + + + Folders On Top + + + true + + + true + + + + + + + Set display mode of diff view + + + QComboBox::AdjustToContents + + + + Tree + + + + + Tree, simplified + + + + + + + + Qt::NoFocus + + + Collapse All + + + true + + + @@ -61,13 +113,6 @@ - - - - Note: If you select a top-level folder and deselect its children, they will still be restored. - - - diff --git a/src/vorta/views/extract_dialog.py b/src/vorta/views/extract_dialog.py index 250fb32b..9b6489fc 100644 --- a/src/vorta/views/extract_dialog.py +++ b/src/vorta/views/extract_dialog.py @@ -7,11 +7,14 @@ from typing import Optional from PyQt5 import uic -from PyQt5.QtCore import QDateTime, QLocale, QModelIndex, Qt, QThread -from PyQt5.QtGui import QColor -from PyQt5.QtWidgets import QDialogButtonBox, QHeaderView, QPushButton +from PyQt5.QtCore import (QDateTime, QLocale, QMimeData, QModelIndex, QPoint, + Qt, QThread, QUrl) +from PyQt5.QtGui import QColor, QKeySequence +from PyQt5.QtWidgets import (QApplication, QDialogButtonBox, QHeaderView, + QMenu, QPushButton, QShortcut) from vorta.utils import get_asset, pretty_bytes, uses_dark_mode +from vorta.views.utils import get_colored_icon from .partials.treemodel import (FileSystemItem, FileTreeModel, FileTreeSortProxyModel, path_to_str, @@ -60,6 +63,12 @@ def __init__(self, archive, model): view.setAlternatingRowColors(True) view.setUniformRowHeights(True) # Allows for scrolling optimizations. + # custom context menu + self.treeView.setContextMenuPolicy( + Qt.ContextMenuPolicy.CustomContextMenu) + self.treeView.customContextMenuRequested.connect( + self.treeview_context_menu) + # add sort proxy model self.sortproxy = ExtractSortProxyModel(self) self.sortproxy.setSourceModel(self.model) @@ -76,6 +85,10 @@ def __init__(self, archive, model): header.setSectionResizeMode(3, QHeaderView.ResizeToContents) header.setSectionResizeMode(0, QHeaderView.Stretch) + # shortcuts + shortcut_copy = QShortcut(QKeySequence.StandardKey.Copy, self.treeView) + shortcut_copy.activated.connect(self.copy_item) + # add extract button to button box self.extractButton = QPushButton(self) self.extractButton.setObjectName("extractButton") @@ -88,9 +101,20 @@ def __init__(self, archive, model): self.archiveNameLabel.setText(f"{archive.name}, {archive.time}") # connect signals + self.comboBoxDisplayMode.currentIndexChanged.connect( + self.change_display_mode) + self.bFoldersOnTop.toggled.connect(self.sortproxy.keepFoldersOnTop) + self.bCollapseAll.clicked.connect(self.treeView.collapseAll) + self.buttonBox.rejected.connect(self.close) self.buttonBox.accepted.connect(self.accept) + self.set_icons() + + # Connect to palette change + QApplication.instance().paletteChanged.connect( + lambda p: self.set_icons()) + def retranslateUi(self, dialog): """Retranslate strings in ui.""" super().retranslateUi(dialog) @@ -99,13 +123,80 @@ def retranslateUi(self, dialog): if hasattr(self, "extractButton"): self.extractButton.setText(self.tr("Extract")) + def set_icons(self): + """Set or update the icons in the right color scheme.""" + self.bCollapseAll.setIcon(get_colored_icon('angle-up-solid')) + def slot_sorted(self, column, order): - """React the tree view being sorted.""" + """React to the tree view being sorted.""" # reveal selection selectedRows = self.treeView.selectionModel().selectedRows() if selectedRows: self.treeView.scrollTo(selectedRows[0]) + def copy_item(self, index: QModelIndex = None): + """ + Copy an item path to the clipboard. + + Copies the first selected item if no index is specified. + """ + if index is None or (not index.isValid()): + indexes = self.treeView.selectionModel().selectedRows() + + if not indexes: + return + + index = indexes[0] + + index = self.sortproxy.mapToSource(index) + item: ExtractFileItem = index.internalPointer() + path = PurePath('/', *item.path) + + data = QMimeData() + data.setUrls([QUrl(path.as_uri())]) + data.setText(str(path)) + + QApplication.clipboard().setMimeData(data) + + def change_display_mode(self, selection: int): + """ + Change the display mode of the tree view + + The `selection` parameter specifies the index of the selected mode in + `comboBoxDisplayMode`. + + """ + if selection == 0: + mode = FileTreeModel.DisplayMode.TREE + elif selection == 1: + mode = FileTreeModel.DisplayMode.SIMPLIFIED_TREE + else: + raise Exception( + "Unknown item in comboBoxDisplayMode with index {}".format( + selection)) + + self.model.setMode(mode) + + def treeview_context_menu(self, pos: QPoint): + """Display a context menu for `treeView`.""" + index = self.treeView.indexAt(pos) + if not index.isValid(): + # popup only for items + return + + menu = QMenu(self.treeView) + + menu.addAction(get_colored_icon('copy'), self.tr("Copy"), + lambda: self.copy_item(index)) + + if self.model.getMode() != self.model.DisplayMode.FLAT: + menu.addSeparator() + menu.addAction(get_colored_icon('angle-down-solid'), + self.tr("Expand recursively"), + lambda: self.treeView.expandRecursively(index)) + + menu.popup(self.treeView.viewport().mapToGlobal(pos)) + def parse_json_lines(lines, model: "ExtractTree"): """Parse json output of `borg list`.""" @@ -150,7 +241,7 @@ class ExtractSortProxyModel(FileTreeSortProxyModel): def choose_data(self, index: QModelIndex): """Choose the data of index used for comparison.""" - item: FileSystemItem[FileData] = index.internalPointer() + item: ExtractFileItem = index.internalPointer() column = index.column() if column == 0: @@ -198,6 +289,9 @@ class FileData: checked_children: int = 0 # number of children checked +ExtractFileItem = FileSystemItem[FileData] + + class ExtractTree(FileTreeModel[FileData]): """The file tree model for diff results.""" @@ -217,7 +311,7 @@ def _flat_filter(self, item): """ return item.data and not item.children - def _simplify_filter(self, item: FileSystemItem[FileData]) -> bool: + def _simplify_filter(self, item: ExtractFileItem) -> bool: """ Return whether an item may be merged in simplified mode. @@ -353,7 +447,7 @@ def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole): if not index.isValid(): return None - item: FileSystemItem[FileData] = index.internalPointer() + item: ExtractFileItem = index.internalPointer() column = index.column() if role == Qt.ItemDataRole.DisplayRole: @@ -466,7 +560,7 @@ def setData( if role != Qt.ItemDataRole.CheckStateRole: return False - item: FileSystemItem[FileData] = index.internalPointer() + item: ExtractFileItem = index.internalPointer() if value == item.data.checkstate: return True @@ -504,10 +598,10 @@ def setData( super_item.data.checked_children += 1 # update parent's state and possibly the parent's parent's state - if parent.data.checked_children: - self.setData(index.parent(), Qt.CheckState.PartiallyChecked, role) + if super_item.data.checked_children: + self.setData(super_index, Qt.CheckState.PartiallyChecked, role) else: - self.setData(index.parent(), Qt.CheckState.Unchecked, role) + self.setData(super_index, Qt.CheckState.Unchecked, role) # update state of the children without changing their parents' states if value != Qt.CheckState.PartiallyChecked: