feat: reimplement drag drop files (Port #153) (#528)

* feat: Drag and drop files in and out of TagStudio (#153)

* Ability to drop local files in to TagStudio to add to library

* Added renaming option to drop import

* Improved readability and switched to pathLib

* format

* Apply suggestions from code review

Co-authored-by: yed podtrzitko <yedpodtrzitko@users.noreply.github.com>

* Revert Change

* Update tagstudio/src/qt/modals/drop_import.py

Co-authored-by: yed podtrzitko <yedpodtrzitko@users.noreply.github.com>

* Added support for folders

* formatting

* Progress bars added

* Added Ability to Drag out of window

* f

* format

* Ability to drop local files in to TagStudio to add to library

* Added renaming option to drop import

* Improved readability and switched to pathLib

* format

* Apply suggestions from code review

Co-authored-by: yed podtrzitko <yedpodtrzitko@users.noreply.github.com>

* Revert Change

* Update tagstudio/src/qt/modals/drop_import.py

Co-authored-by: yed podtrzitko <yedpodtrzitko@users.noreply.github.com>

* Added support for folders

* formatting

* Progress bars added

* Added Ability to Drag out of window

* f

* format

* format

* formatting and refactor

* format again

* formatting for mypy

* convert lambda to func for clarity

* mypy fixes

* fixed dragout only worked on selected

* Refactor typo, Add license

* Reformat QMessageBox

* Disable drops when no library is open

Co-authored-by: Sean Krueger <skrueger2270@gmail.com>

* Rebased onto SQL migration

* Updated logic to based on selected grid_idx instead of selected ids

* Add newly dragged-in files to SQL database

* Fix buttons being inconsistant across platforms

* Fix ruff formatting

* Rename "override" button to "overwrite"

---------

Co-authored-by: yed podtrzitko <yedpodtrzitko@users.noreply.github.com>
Co-authored-by: Travis Abendshien <lvnvtravis@gmail.com>
Co-authored-by: Sean Krueger <skrueger2270@gmail.com>

* refactor: Update dialog and simplify drop import logic

* Handle Qt events for main window in ts_qt.py

* Replace magic values with enums

* Match import duplicate file dialog to delete missing entry dialog

* Remove excessive progess widgets

* Add docstrings and logging

* refactor: add function for common ProgressWidget use

Extracts the create_progress_bar function from drop_import to the
ProgressWidget class. Instead of creating a ProgressWidget,
FunctionIterator, and CustomRunnable every time a thread-safe progress
widget is needed, the from_iterable function in ProgressWidget now
handles all of that.

---------

Co-authored-by: Creepler13 <denis.schlichting03@gmail.com>
Co-authored-by: yed podtrzitko <yedpodtrzitko@users.noreply.github.com>
Co-authored-by: Travis Abendshien <lvnvtravis@gmail.com>
This commit is contained in:
Sean Krueger
2024-12-11 16:00:27 -08:00
committed by GitHub
parent a1daf5ab6a
commit 385aaf42d8
9 changed files with 361 additions and 137 deletions

View File

@@ -4,7 +4,7 @@
import typing
from PySide6.QtCore import Qt, QThreadPool, Signal
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QStandardItem, QStandardItemModel
from PySide6.QtWidgets import (
QHBoxLayout,
@@ -15,8 +15,6 @@ from PySide6.QtWidgets import (
QWidget,
)
from src.core.utils.missing_files import MissingRegistry
from src.qt.helpers.custom_runnable import CustomRunnable
from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.widgets.progress import ProgressWidget
# Only import for type checking/autocompletion, will not be imported at runtime.
@@ -77,9 +75,14 @@ class DeleteUnlinkedEntriesModal(QWidget):
self.model.clear()
for i in self.tracker.missing_files:
self.model.appendRow(QStandardItem(str(i.path)))
item = QStandardItem(str(i.path))
item.setEditable(False)
self.model.appendRow(item)
def delete_entries(self):
def displayed_text(x):
return f"Deleting {x}/{self.tracker.missing_files_count} Unlinked Entries"
pw = ProgressWidget(
window_title="Deleting Entries",
label_text="",
@@ -87,23 +90,5 @@ class DeleteUnlinkedEntriesModal(QWidget):
minimum=0,
maximum=self.tracker.missing_files_count,
)
pw.show()
iterator = FunctionIterator(self.tracker.execute_deletion)
files_count = self.tracker.missing_files_count
iterator.value.connect(
lambda idx: (
pw.update_progress(idx),
pw.update_label(f"Deleting {idx}/{files_count} Unlinked Entries"),
)
)
r = CustomRunnable(iterator.run)
QThreadPool.globalInstance().start(r)
r.done.connect(
lambda: (
pw.hide(),
pw.deleteLater(),
self.done.emit(),
)
)
pw.from_iterable_function(self.tracker.execute_deletion, displayed_text, self.done.emit)

View File

@@ -0,0 +1,229 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import enum
import shutil
from pathlib import Path
from typing import TYPE_CHECKING
import structlog
from PySide6.QtCore import Qt, QUrl
from PySide6.QtGui import QStandardItem, QStandardItemModel
from PySide6.QtWidgets import (
QHBoxLayout,
QLabel,
QListView,
QPushButton,
QVBoxLayout,
QWidget,
)
from src.qt.widgets.progress import ProgressWidget
if TYPE_CHECKING:
from src.qt.ts_qt import QtDriver
logger = structlog.get_logger(__name__)
class DuplicateChoice(enum.StrEnum):
SKIP = "Skipped"
OVERWRITE = "Overwritten"
RENAME = "Renamed"
CANCEL = "Cancelled"
class DropImportModal(QWidget):
DUPE_NAME_LIMT: int = 5
def __init__(self, driver: "QtDriver"):
super().__init__()
self.driver: QtDriver = driver
# Widget ======================
self.setWindowTitle("Conflicting File(s)")
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(500, 400)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 6, 6, 6)
self.desc_widget = QLabel()
self.desc_widget.setObjectName("descriptionLabel")
self.desc_widget.setWordWrap(True)
self.desc_widget.setText("The following files have filenames already exist in the library")
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
# Duplicate File List ========
self.list_view = QListView()
self.model = QStandardItemModel()
self.list_view.setModel(self.model)
# Buttons ====================
self.button_container = QWidget()
self.button_layout = QHBoxLayout(self.button_container)
self.button_layout.setContentsMargins(6, 6, 6, 6)
self.button_layout.addStretch(1)
self.skip_button = QPushButton()
self.skip_button.setText("&Skip")
self.skip_button.setDefault(True)
self.skip_button.clicked.connect(lambda: self.begin_transfer(DuplicateChoice.SKIP))
self.button_layout.addWidget(self.skip_button)
self.overwrite_button = QPushButton()
self.overwrite_button.setText("&Overwrite")
self.overwrite_button.clicked.connect(
lambda: self.begin_transfer(DuplicateChoice.OVERWRITE)
)
self.button_layout.addWidget(self.overwrite_button)
self.rename_button = QPushButton()
self.rename_button.setText("&Rename")
self.rename_button.clicked.connect(lambda: self.begin_transfer(DuplicateChoice.RENAME))
self.button_layout.addWidget(self.rename_button)
self.cancel_button = QPushButton()
self.cancel_button.setText("&Cancel")
self.cancel_button.clicked.connect(lambda: self.begin_transfer(DuplicateChoice.CANCEL))
self.button_layout.addWidget(self.cancel_button)
# Layout =====================
self.root_layout.addWidget(self.desc_widget)
self.root_layout.addWidget(self.list_view)
self.root_layout.addWidget(self.button_container)
def import_urls(self, urls: list[QUrl]):
"""Add a colleciton of urls to the library."""
self.files: list[Path] = []
self.dirs_in_root: list[Path] = []
self.duplicate_files: list[Path] = []
self.collect_files_to_import(urls)
if len(self.duplicate_files) > 0:
self.ask_duplicates_choice()
else:
self.begin_transfer()
def collect_files_to_import(self, urls: list[QUrl]):
"""Collect one or more files from drop event urls."""
for url in urls:
if not url.isLocalFile():
continue
file = Path(url.toLocalFile())
if file.is_dir():
for f in file.glob("**/*"):
if f.is_dir():
continue
self.files.append(f)
if (self.driver.lib.library_dir / self._get_relative_path(file)).exists():
self.duplicate_files.append(f)
self.dirs_in_root.append(file.parent)
else:
self.files.append(file)
if file.parent not in self.dirs_in_root:
self.dirs_in_root.append(
file.parent
) # to create relative path of files not in folder
if (Path(self.driver.lib.library_dir) / file.name).exists():
self.duplicate_files.append(file)
def ask_duplicates_choice(self):
"""Display the message widgeth with a list of the duplicated files."""
self.desc_widget.setText(
f"The following {len(self.duplicate_files)} file(s) have filenames already exist in the library." # noqa: E501
)
self.model.clear()
for dupe in self.duplicate_files:
item = QStandardItem(str(self._get_relative_path(dupe)))
item.setEditable(False)
self.model.appendRow(item)
self.driver.main_window.raise_()
self.show()
def begin_transfer(self, choice: DuplicateChoice | None = None):
"""Display a progress bar and begin copying files into library."""
self.hide()
self.choice: DuplicateChoice | None = choice
logger.info("duplicated choice selected", choice=self.choice)
if self.choice == DuplicateChoice.CANCEL:
return
def displayed_text(x):
text = (
f"Importing New Files...\n{x[0] + 1} File{'s' if x[0] + 1 != 1 else ''} Imported."
)
if self.choice:
text += f" {x[1]} {self.choice.value}"
return text
pw = ProgressWidget(
window_title="Import Files",
label_text="Importing New Files...",
cancel_button_text=None,
minimum=0,
maximum=len(self.files),
)
pw.from_iterable_function(
self.copy_files,
displayed_text,
self.driver.add_new_files_callback,
self.deleteLater,
)
def copy_files(self):
"""Copy files from original location to the library directory."""
file_count = 0
duplicated_files_progress = 0
for file in self.files:
if file.is_dir():
continue
dest_file = self._get_relative_path(file)
if file in self.duplicate_files:
duplicated_files_progress += 1
if self.choice == DuplicateChoice.SKIP:
file_count += 1
continue
elif self.choice == DuplicateChoice.RENAME:
new_name = self._get_renamed_duplicate_filename(dest_file)
dest_file = dest_file.with_name(new_name)
(self.driver.lib.library_dir / dest_file).parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(file, self.driver.lib.library_dir / dest_file)
file_count += 1
yield [file_count, duplicated_files_progress]
def _get_relative_path(self, path: Path) -> Path:
for dir in self.dirs_in_root:
if path.is_relative_to(dir):
return path.relative_to(dir)
return Path(path.name)
def _get_renamed_duplicate_filename(self, filepath: Path) -> str:
index = 2
o_filename = filepath.name
try:
dot_idx = o_filename.index(".")
except ValueError:
dot_idx = len(o_filename)
while (self.driver.lib.library_dir / filepath).exists():
filepath = filepath.with_name(
o_filename[:dot_idx] + f" ({index})" + o_filename[dot_idx:]
)
index += 1
return filepath.name

View File

@@ -5,12 +5,10 @@
import typing
from PySide6.QtCore import Qt, QThreadPool
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
from src.core.library import Library
from src.core.utils.missing_files import MissingRegistry
from src.qt.helpers.custom_runnable import CustomRunnable
from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.modals.delete_unlinked import DeleteUnlinkedEntriesModal
from src.qt.modals.merge_dupe_entries import MergeDuplicateEntries
from src.qt.modals.relink_unlinked import RelinkUnlinkedEntries
@@ -85,7 +83,7 @@ class FixUnlinkedEntriesModal(QWidget):
self.delete_modal = DeleteUnlinkedEntriesModal(self.driver, self.tracker)
self.delete_modal.done.connect(
lambda: (
self.set_missing_count(self.tracker.missing_files_count),
self.set_missing_count(),
# refresh the grid
self.driver.filter_items(),
)
@@ -125,23 +123,19 @@ class FixUnlinkedEntriesModal(QWidget):
maximum=self.lib.entries_count,
)
pw.show()
iterator = FunctionIterator(self.tracker.refresh_missing_files)
iterator.value.connect(lambda v: pw.update_progress(v + 1))
r = CustomRunnable(iterator.run)
QThreadPool.globalInstance().start(r)
r.done.connect(
lambda: (
pw.hide(),
pw.deleteLater(),
self.set_missing_count(self.tracker.missing_files_count),
self.delete_modal.refresh_list(),
)
pw.from_iterable_function(
self.tracker.refresh_missing_files,
None,
self.set_missing_count,
self.delete_modal.refresh_list,
)
def set_missing_count(self, count: int):
self.missing_count = count
def set_missing_count(self, count: int | None = None):
if count is not None:
self.missing_count = count
else:
self.missing_count = self.tracker.missing_files_count
if self.missing_count < 0:
self.search_button.setDisabled(True)
self.delete_button.setDisabled(True)

View File

@@ -4,11 +4,9 @@
import typing
from PySide6.QtCore import QObject, QThreadPool, Signal
from PySide6.QtCore import QObject, Signal
from src.core.library import Library
from src.core.utils.dupe_files import DupeRegistry
from src.qt.helpers.custom_runnable import CustomRunnable
from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.widgets.progress import ProgressWidget
# Only import for type checking/autocompletion, will not be imported at runtime.
@@ -26,20 +24,12 @@ class MergeDuplicateEntries(QObject):
self.tracker = DupeRegistry(library=self.lib)
def merge_entries(self):
iterator = FunctionIterator(self.tracker.merge_dupe_entries)
pw = ProgressWidget(
window_title="Merging Duplicate Entries",
label_text="",
label_text="Merging Duplicate Entries...",
cancel_button_text=None,
minimum=0,
maximum=self.tracker.groups_count,
)
pw.show()
iterator.value.connect(lambda x: pw.update_progress(x))
iterator.value.connect(lambda: (pw.update_label("Merging Duplicate Entries...")))
r = CustomRunnable(iterator.run)
r.done.connect(lambda: (pw.hide(), pw.deleteLater(), self.done.emit()))
QThreadPool.globalInstance().start(r)
pw.from_iterable_function(self.tracker.merge_dupe_entries, None, self.done.emit)

View File

@@ -6,7 +6,7 @@
import typing
from time import sleep
from PySide6.QtCore import Qt, QThreadPool, Signal
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QStandardItem, QStandardItemModel
from PySide6.QtWidgets import (
QHBoxLayout,
@@ -17,8 +17,6 @@ from PySide6.QtWidgets import (
QWidget,
)
from src.core.utils.dupe_files import DupeRegistry
from src.qt.helpers.custom_runnable import CustomRunnable
from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.widgets.progress import ProgressWidget
# Only import for type checking/autocompletion, will not be imported at runtime.
@@ -83,28 +81,22 @@ class MirrorEntriesModal(QWidget):
self.model.appendRow(QStandardItem(str(i)))
def mirror_entries(self):
iterator = FunctionIterator(self.mirror_entries_runnable)
def displayed_text(x):
return f"Mirroring {x + 1}/{self.tracker.groups_count} Entries..."
pw = ProgressWidget(
window_title="Mirroring Entries",
label_text=f"Mirroring 1/{self.tracker.groups_count} Entries...",
label_text="",
cancel_button_text=None,
minimum=0,
maximum=self.tracker.groups_count,
)
pw.show()
iterator.value.connect(lambda x: pw.update_progress(x + 1))
iterator.value.connect(
lambda x: pw.update_label(f"Mirroring {x + 1}/{self.tracker.groups_count} Entries...")
)
r = CustomRunnable(iterator.run)
QThreadPool.globalInstance().start(r)
r.done.connect(
lambda: (
pw.hide(),
pw.deleteLater(),
self.driver.preview_panel.update_widgets(),
self.done.emit(),
)
pw.from_iterable_function(
self.mirror_entries_runnable,
displayed_text,
self.driver.preview_panel.update_widgets,
self.done.emit,
)
def mirror_entries_runnable(self):

View File

@@ -3,10 +3,8 @@
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from PySide6.QtCore import QObject, QThreadPool, Signal
from PySide6.QtCore import QObject, Signal
from src.core.utils.missing_files import MissingRegistry
from src.qt.helpers.custom_runnable import CustomRunnable
from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.widgets.progress import ProgressWidget
@@ -18,7 +16,10 @@ class RelinkUnlinkedEntries(QObject):
self.tracker = tracker
def repair_entries(self):
iterator = FunctionIterator(self.tracker.fix_missing_files)
def displayed_text(x):
text = f"Attempting to Relink {x}/{self.tracker.missing_files_count} Entries. \n"
text += f"{self.tracker.files_fixed_count} Successfully Relinked."
return text
pw = ProgressWidget(
window_title="Relinking Entries",
@@ -28,24 +29,4 @@ class RelinkUnlinkedEntries(QObject):
maximum=self.tracker.missing_files_count,
)
pw.show()
iterator.value.connect(
lambda idx: (
pw.update_progress(idx),
pw.update_label(
f"Attempting to Relink {idx}/{self.tracker.missing_files_count} Entries. "
f"{self.tracker.files_fixed_count} Successfully Relinked."
),
)
)
r = CustomRunnable(iterator.run)
r.done.connect(
lambda: (
pw.hide(),
pw.deleteLater(),
self.done.emit(),
)
)
QThreadPool.globalInstance().start(r)
pw.from_iterable_function(self.tracker.fix_missing_files, displayed_text, self.done.emit)

View File

@@ -37,6 +37,9 @@ from PySide6.QtCore import (
from PySide6.QtGui import (
QAction,
QColor,
QDragEnterEvent,
QDragMoveEvent,
QDropEvent,
QFontDatabase,
QGuiApplication,
QIcon,
@@ -82,6 +85,7 @@ from src.qt.helpers.custom_runnable import CustomRunnable
from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.main_window import Ui_MainWindow
from src.qt.modals.build_tag import BuildTagPanel
from src.qt.modals.drop_import import DropImportModal
from src.qt.modals.file_extension import FileExtensionModal
from src.qt.modals.fix_dupes import FixDupeFilesModal
from src.qt.modals.fix_unlinked import FixUnlinkedEntriesModal
@@ -234,19 +238,10 @@ class QtDriver(DriverMixin, QObject):
# self.main_window = loader.load(home_path)
self.main_window = Ui_MainWindow(self)
self.main_window.setWindowTitle(self.base_title)
self.main_window.mousePressEvent = self.mouse_navigation # type: ignore
# self.main_window.setStyleSheet(
# f'QScrollBar::{{background:red;}}'
# )
# # self.main_window.windowFlags() &
# # self.main_window.setWindowFlag(Qt.WindowType.FramelessWindowHint, True)
# self.main_window.setWindowFlag(Qt.WindowType.NoDropShadowWindowHint, True)
# self.main_window.setWindowFlag(Qt.WindowType.WindowTransparentForInput, False)
# self.main_window.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
# self.windowFX = WindowEffect()
# self.windowFX.setAcrylicEffect(self.main_window.winId())
self.main_window.mousePressEvent = self.mouse_navigation # type: ignore[method-assign]
self.main_window.dragEnterEvent = self.drag_enter_event # type: ignore[method-assign]
self.main_window.dragMoveEvent = self.drag_move_event # type: ignore[method-assign]
self.main_window.dropEvent = self.drop_event # type: ignore[method-assign]
splash_pixmap = QPixmap(":/images/splash.png")
splash_pixmap.setDevicePixelRatio(self.main_window.devicePixelRatio())
@@ -716,26 +711,6 @@ class QtDriver(DriverMixin, QObject):
Threaded method.
"""
# pb = QProgressDialog(
# f"Running Configured Macros on 1/{len(new_ids)} New Entries", None, 0, len(new_ids)
# )
# pb.setFixedSize(432, 112)
# pb.setWindowFlags(pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
# pb.setWindowTitle('Running Macros')
# pb.setWindowModality(Qt.WindowModality.ApplicationModal)
# pb.show()
# r = CustomRunnable(lambda: self.new_file_macros_runnable(pb, new_ids))
# r.done.connect(lambda: (pb.hide(), pb.deleteLater(), self.filter_items('')))
# r.run()
# # QThreadPool.globalInstance().start(r)
# # self.main_window.statusbar.showMessage(
# # f"Running configured Macros on {len(new_ids)} new Entries...", 3
# # )
# # pb.hide()
files_count = tracker.files_count
iterator = FunctionIterator(tracker.save_new_files)
@@ -747,6 +722,7 @@ class QtDriver(DriverMixin, QObject):
maximum=files_count,
)
pw.show()
iterator.value.connect(
lambda x: (
pw.update_progress(x + 1),
@@ -905,6 +881,7 @@ class QtDriver(DriverMixin, QObject):
item_thumb = ItemThumb(
None, self.lib, self, (self.thumb_size, self.thumb_size), grid_idx
)
layout.addWidget(item_thumb)
self.item_thumbs.append(item_thumb)
@@ -1234,6 +1211,7 @@ class QtDriver(DriverMixin, QObject):
self.update_libs_list(path)
title_text = f"{self.base_title} - Library '{self.lib.library_dir}'"
self.main_window.setWindowTitle(title_text)
self.main_window.setAcceptDrops(True)
self.selected.clear()
self.preview_panel.update_widgets()
@@ -1243,3 +1221,27 @@ class QtDriver(DriverMixin, QObject):
self.main_window.toggle_landing_page(enabled=False)
return open_status
def drop_event(self, event: QDropEvent):
if event.source() is self:
return
if not event.mimeData().hasUrls():
return
urls = event.mimeData().urls()
logger.info("New items dragged in", urls=urls)
drop_import = DropImportModal(self)
drop_import.import_urls(urls)
def drag_enter_event(self, event: QDragEnterEvent):
if event.mimeData().hasUrls():
event.accept()
else:
event.ignore()
def drag_move_event(self, event: QDragMoveEvent):
if event.mimeData().hasUrls():
event.accept()
else:
event.ignore()

View File

@@ -10,8 +10,8 @@ from typing import TYPE_CHECKING
import structlog
from PIL import Image, ImageQt
from PySide6.QtCore import QEvent, QSize, Qt
from PySide6.QtGui import QAction, QEnterEvent, QPixmap
from PySide6.QtCore import QEvent, QMimeData, QSize, Qt, QUrl
from PySide6.QtGui import QAction, QDrag, QEnterEvent, QPixmap
from PySide6.QtWidgets import (
QBoxLayout,
QCheckBox,
@@ -127,6 +127,7 @@ class ItemThumb(FlowWidget):
self.thumb_size: tuple[int, int] = thumb_size
self.setMinimumSize(*thumb_size)
self.setMaximumSize(*thumb_size)
self.setMouseTracking(True)
check_size = 24
# +----------+
@@ -480,3 +481,29 @@ class ItemThumb(FlowWidget):
if self.driver.preview_panel.is_open:
self.driver.preview_panel.update_widgets()
def mouseMoveEvent(self, event): # noqa: N802
if event.buttons() is not Qt.MouseButton.LeftButton:
return
drag = QDrag(self.driver)
paths = []
mimedata = QMimeData()
selected_idxs = self.driver.selected
if self.grid_idx not in selected_idxs:
selected_idxs = [self.grid_idx]
for grid_idx in selected_idxs:
id = self.driver.item_thumbs[grid_idx].item_id
entry = self.lib.get_entry(id)
if not entry:
continue
url = QUrl.fromLocalFile(Path(self.lib.library_dir) / entry.path)
paths.append(url)
mimedata.setUrls(paths)
drag.setMimeData(mimedata)
drag.exec(Qt.DropAction.CopyAction)
logger.info("dragged files to external program", thumbnail_indexs=selected_idxs)

View File

@@ -2,11 +2,12 @@
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from typing import Callable, Optional
from typing import Optional
from PySide6.QtCore import Qt
from PySide6.QtCore import Qt, QThreadPool
from PySide6.QtWidgets import QProgressDialog, QVBoxLayout, QWidget
from src.qt.helpers.custom_runnable import CustomRunnable
from src.qt.helpers.function_iterator import FunctionIterator
class ProgressWidget(QWidget):
@@ -39,3 +40,26 @@ class ProgressWidget(QWidget):
def update_progress(self, value: int):
self.pb.setValue(value)
def _update_progress_unknown_iterable(self, value):
if hasattr(value, "__getitem__"):
self.update_progress(value[0] + 1)
else:
self.update_progress(value + 1)
def from_iterable_function(
self, function: Callable, update_label_callback: Callable | None, *done_callbacks
):
"""Display the progress widget from a threaded iterable function."""
iterator = FunctionIterator(function)
iterator.value.connect(lambda x: self._update_progress_unknown_iterable(x))
if update_label_callback:
iterator.value.connect(lambda x: self.update_label(update_label_callback(x)))
self.show()
r = CustomRunnable(lambda: iterator.run())
r.done.connect(
lambda: (self.hide(), self.deleteLater(), [callback() for callback in done_callbacks])
)
QThreadPool.globalInstance().start(r)