mirror of
https://github.com/borgbase/vorta
synced 2025-01-02 21:25:48 +00:00
Improve extract dialog GUI.
Adds a contextmenu and a simplified tree view mode, a button to collapse the tree and the option to keep folders on top of the list. * src/vorta/assets/UI/extractdialog.ui * src/vorta/views/extract_dialog.py * src/vorta/views/extract_dialog.py (ExtractFileItem): Add this variable holding the type `FileSystemItem[FileData]`.
This commit is contained in:
parent
e7772f517b
commit
372b7107dd
2 changed files with 157 additions and 18 deletions
|
@ -52,6 +52,58 @@
|
||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="bFoldersOnTop">
|
||||||
|
<property name="focusPolicy">
|
||||||
|
<enum>Qt::NoFocus</enum>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Keep folders on top when sorting</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Folders On Top</string>
|
||||||
|
</property>
|
||||||
|
<property name="checkable">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="autoRaise">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="comboBoxDisplayMode">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Set display mode of diff view</string>
|
||||||
|
</property>
|
||||||
|
<property name="sizeAdjustPolicy">
|
||||||
|
<enum>QComboBox::AdjustToContents</enum>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Tree</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Tree, simplified</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="bCollapseAll">
|
||||||
|
<property name="focusPolicy">
|
||||||
|
<enum>Qt::NoFocus</enum>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Collapse All</string>
|
||||||
|
</property>
|
||||||
|
<property name="autoRaise">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
@ -61,13 +113,6 @@
|
||||||
</attribute>
|
</attribute>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="label">
|
|
||||||
<property name="text">
|
|
||||||
<string>Note: If you select a top-level folder and deselect its children, they will still be restored.</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
<item>
|
||||||
<widget class="QDialogButtonBox" name="buttonBox">
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
<property name="standardButtons">
|
<property name="standardButtons">
|
||||||
|
|
|
@ -7,11 +7,14 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from PyQt5 import uic
|
from PyQt5 import uic
|
||||||
from PyQt5.QtCore import QDateTime, QLocale, QModelIndex, Qt, QThread
|
from PyQt5.QtCore import (QDateTime, QLocale, QMimeData, QModelIndex, QPoint,
|
||||||
from PyQt5.QtGui import QColor
|
Qt, QThread, QUrl)
|
||||||
from PyQt5.QtWidgets import QDialogButtonBox, QHeaderView, QPushButton
|
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.utils import get_asset, pretty_bytes, uses_dark_mode
|
||||||
|
from vorta.views.utils import get_colored_icon
|
||||||
|
|
||||||
from .partials.treemodel import (FileSystemItem, FileTreeModel,
|
from .partials.treemodel import (FileSystemItem, FileTreeModel,
|
||||||
FileTreeSortProxyModel, path_to_str,
|
FileTreeSortProxyModel, path_to_str,
|
||||||
|
@ -60,6 +63,12 @@ def __init__(self, archive, model):
|
||||||
view.setAlternatingRowColors(True)
|
view.setAlternatingRowColors(True)
|
||||||
view.setUniformRowHeights(True) # Allows for scrolling optimizations.
|
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
|
# add sort proxy model
|
||||||
self.sortproxy = ExtractSortProxyModel(self)
|
self.sortproxy = ExtractSortProxyModel(self)
|
||||||
self.sortproxy.setSourceModel(self.model)
|
self.sortproxy.setSourceModel(self.model)
|
||||||
|
@ -76,6 +85,10 @@ def __init__(self, archive, model):
|
||||||
header.setSectionResizeMode(3, QHeaderView.ResizeToContents)
|
header.setSectionResizeMode(3, QHeaderView.ResizeToContents)
|
||||||
header.setSectionResizeMode(0, QHeaderView.Stretch)
|
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
|
# add extract button to button box
|
||||||
self.extractButton = QPushButton(self)
|
self.extractButton = QPushButton(self)
|
||||||
self.extractButton.setObjectName("extractButton")
|
self.extractButton.setObjectName("extractButton")
|
||||||
|
@ -88,9 +101,20 @@ def __init__(self, archive, model):
|
||||||
self.archiveNameLabel.setText(f"{archive.name}, {archive.time}")
|
self.archiveNameLabel.setText(f"{archive.name}, {archive.time}")
|
||||||
|
|
||||||
# connect signals
|
# 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.rejected.connect(self.close)
|
||||||
self.buttonBox.accepted.connect(self.accept)
|
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):
|
def retranslateUi(self, dialog):
|
||||||
"""Retranslate strings in ui."""
|
"""Retranslate strings in ui."""
|
||||||
super().retranslateUi(dialog)
|
super().retranslateUi(dialog)
|
||||||
|
@ -99,13 +123,80 @@ def retranslateUi(self, dialog):
|
||||||
if hasattr(self, "extractButton"):
|
if hasattr(self, "extractButton"):
|
||||||
self.extractButton.setText(self.tr("Extract"))
|
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):
|
def slot_sorted(self, column, order):
|
||||||
"""React the tree view being sorted."""
|
"""React to the tree view being sorted."""
|
||||||
# reveal selection
|
# reveal selection
|
||||||
selectedRows = self.treeView.selectionModel().selectedRows()
|
selectedRows = self.treeView.selectionModel().selectedRows()
|
||||||
if selectedRows:
|
if selectedRows:
|
||||||
self.treeView.scrollTo(selectedRows[0])
|
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"):
|
def parse_json_lines(lines, model: "ExtractTree"):
|
||||||
"""Parse json output of `borg list`."""
|
"""Parse json output of `borg list`."""
|
||||||
|
@ -150,7 +241,7 @@ class ExtractSortProxyModel(FileTreeSortProxyModel):
|
||||||
|
|
||||||
def choose_data(self, index: QModelIndex):
|
def choose_data(self, index: QModelIndex):
|
||||||
"""Choose the data of index used for comparison."""
|
"""Choose the data of index used for comparison."""
|
||||||
item: FileSystemItem[FileData] = index.internalPointer()
|
item: ExtractFileItem = index.internalPointer()
|
||||||
column = index.column()
|
column = index.column()
|
||||||
|
|
||||||
if column == 0:
|
if column == 0:
|
||||||
|
@ -198,6 +289,9 @@ class FileData:
|
||||||
checked_children: int = 0 # number of children checked
|
checked_children: int = 0 # number of children checked
|
||||||
|
|
||||||
|
|
||||||
|
ExtractFileItem = FileSystemItem[FileData]
|
||||||
|
|
||||||
|
|
||||||
class ExtractTree(FileTreeModel[FileData]):
|
class ExtractTree(FileTreeModel[FileData]):
|
||||||
"""The file tree model for diff results."""
|
"""The file tree model for diff results."""
|
||||||
|
|
||||||
|
@ -217,7 +311,7 @@ def _flat_filter(self, item):
|
||||||
"""
|
"""
|
||||||
return item.data and not item.children
|
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.
|
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():
|
if not index.isValid():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
item: FileSystemItem[FileData] = index.internalPointer()
|
item: ExtractFileItem = index.internalPointer()
|
||||||
column = index.column()
|
column = index.column()
|
||||||
|
|
||||||
if role == Qt.ItemDataRole.DisplayRole:
|
if role == Qt.ItemDataRole.DisplayRole:
|
||||||
|
@ -466,7 +560,7 @@ def setData(
|
||||||
if role != Qt.ItemDataRole.CheckStateRole:
|
if role != Qt.ItemDataRole.CheckStateRole:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
item: FileSystemItem[FileData] = index.internalPointer()
|
item: ExtractFileItem = index.internalPointer()
|
||||||
|
|
||||||
if value == item.data.checkstate:
|
if value == item.data.checkstate:
|
||||||
return True
|
return True
|
||||||
|
@ -504,10 +598,10 @@ def setData(
|
||||||
super_item.data.checked_children += 1
|
super_item.data.checked_children += 1
|
||||||
|
|
||||||
# update parent's state and possibly the parent's parent's state
|
# update parent's state and possibly the parent's parent's state
|
||||||
if parent.data.checked_children:
|
if super_item.data.checked_children:
|
||||||
self.setData(index.parent(), Qt.CheckState.PartiallyChecked, role)
|
self.setData(super_index, Qt.CheckState.PartiallyChecked, role)
|
||||||
else:
|
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
|
# update state of the children without changing their parents' states
|
||||||
if value != Qt.CheckState.PartiallyChecked:
|
if value != Qt.CheckState.PartiallyChecked:
|
||||||
|
|
Loading…
Reference in a new issue