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: