Split out remaining modals from ts_qt.py

This commit is contained in:
Andrew Arneson 2024-04-28 11:09:06 -06:00
parent f7c4e1ccc0
commit ca9735ca86
11 changed files with 102 additions and 30852 deletions

View file

@ -1,2 +1,4 @@
from .open_file import open_file
from .file_opener import FileOpenerHelper, FileOpenerLabel
from .function_iterator import FunctionIterator
from .custom_runnable import CustomRunnable

View file

@ -0,0 +1,19 @@
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from PySide6.QtCore import Signal, QRunnable, QObject
class CustomRunnable(QRunnable, QObject):
done = Signal()
def __init__(self, function) -> None:
QRunnable.__init__(self)
QObject.__init__(self)
self.function = function
def run(self):
self.function()
self.done.emit()

View file

@ -0,0 +1,21 @@
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
from types import FunctionType
from PySide6.QtCore import Signal, QObject
class FunctionIterator(QObject):
"""Iterates over a yielding function and emits progress as the 'value' signal.\n\nThread-Safe Guarantee™"""
value = Signal(object)
def __init__(self, function: FunctionType):
super().__init__()
self.iterable = function
def run(self):
for i in self.iterable():
self.value.emit(i)

View file

@ -3,3 +3,9 @@ from .build_tag import BuildTagPanel
from .tag_database import TagDatabasePanel
from .add_field import AddFieldModal
from .file_extension import FileExtensionModal
from .delete_unlinked import DeleteUnlinkedEntriesModal
from .relink_unlinked import RelinkUnlinkedEntries
from .fix_unlinked import FixUnlinkedEntriesModal
from .mirror_entities import MirrorEntriesModal
from .fix_dupes import FixDupeFilesModal
from .folders_to_tags import FoldersToTagsModal

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -17,23 +17,21 @@ from types import FunctionType
from datetime import datetime as dt
from pathlib import Path
from queue import Empty, Queue
from time import sleep
from typing import Optional
import cv2
from PIL import Image, UnidentifiedImageError, ImageQt
from PySide6 import QtCore
from PySide6.QtCore import QObject, QThread, Signal, QRunnable, Qt, QThreadPool, QSize, QEvent, QTimer, QSettings
from PySide6.QtCore import QObject, QThread, Signal, Qt, QThreadPool, QSize, QEvent, QTimer, QSettings
from PySide6.QtGui import (QGuiApplication, QPixmap, QEnterEvent, QMouseEvent, QResizeEvent, QColor, QAction,
QStandardItemModel, QStandardItem, QFontDatabase, QIcon)
QFontDatabase, QIcon)
from PySide6.QtUiTools import QUiLoader
from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QLineEdit,
QScrollArea, QFrame, QFileDialog, QListView, QSplitter, QSizePolicy, QMessageBox,
QScrollArea, QFrame, QFileDialog, QSplitter, QSizePolicy, QMessageBox,
QBoxLayout, QCheckBox, QSplashScreen, QMenu)
from humanfriendly import format_timespan, format_size
from src.core.library import Collation, Entry, ItemType, Library, Tag
from src.core.palette import ColorType, get_tag_color
from src.core.library import Entry, ItemType, Library
from src.core.ts_core import (PLAINTEXT_TYPES, TagStudioCore, TAG_COLORS, DATE_FIELDS, TEXT_FIELDS, BOX_FIELDS, ALL_FILE_TYPES,
SHORTCUT_TYPES, PROGRAM_TYPES, ARCHIVE_TYPES, PRESENTATION_TYPES,
SPREADSHEET_TYPES, DOC_TYPES, AUDIO_TYPES, VIDEO_TYPES, IMAGE_TYPES,
@ -42,10 +40,11 @@ from src.core.ts_core import (PLAINTEXT_TYPES, TagStudioCore, TAG_COLORS, DATE_F
from src.core.utils.web import strip_web_protocol
from src.qt.flowlayout import FlowLayout, FlowWidget
from src.qt.main_window import Ui_MainWindow
from src.qt.helpers import open_file, FileOpenerHelper, FileOpenerLabel
from src.qt.widgets import (FieldContainer, FieldWidget, CollageIconRenderer, ThumbButton, ThumbRenderer, PanelModal,
EditTextBox, EditTextLine, ProgressWidget, TagBoxWidget, TextWidget)
from src.qt.modals import BuildTagPanel, TagDatabasePanel, AddFieldModal, FileExtensionModal
from src.qt.helpers import open_file, FileOpenerHelper, FileOpenerLabel, FunctionIterator, CustomRunnable
from src.qt.widgets import (FieldContainer, CollageIconRenderer, ThumbButton, ThumbRenderer, PanelModal, EditTextBox,
EditTextLine, ProgressWidget, TagBoxWidget, TextWidget)
from src.qt.modals import (BuildTagPanel, TagDatabasePanel, AddFieldModal, FileExtensionModal, FixUnlinkedEntriesModal,
FixDupeFilesModal, FoldersToTagsModal)
import src.qt.resources_rc
# SIGQUIT is not defined on Windows
@ -107,608 +106,6 @@ class Consumer(QThread):
pass
class FunctionIterator(QObject):
"""Iterates over a yielding function and emits progress as the 'value' signal.\n\nThread-Safe Guarantee™"""
value = Signal(object)
def __init__(self, function: FunctionType):
super().__init__()
self.iterable = function
def run(self):
for i in self.iterable():
self.value.emit(i)
class FixDupeFilesModal(QWidget):
# done = Signal(int)
def __init__(self, library:'Library', driver:'QtDriver'):
super().__init__()
self.lib = library
self.driver:QtDriver = driver
self.count = -1
self.filename = ''
self.setWindowTitle(f'Fix Duplicate Files')
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(400, 300)
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.setStyleSheet(
# 'background:blue;'
'text-align:left;'
# 'font-weight:bold;'
# 'font-size:14px;'
# 'padding-top: 6px'
'')
self.desc_widget.setText('''TagStudio supports importing DupeGuru results to manage duplicate files.''')
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.dupe_count = QLabel()
self.dupe_count.setObjectName('dupeCountLabel')
self.dupe_count.setStyleSheet(
# 'background:blue;'
# 'text-align:center;'
'font-weight:bold;'
'font-size:14px;'
# 'padding-top: 6px'
'')
self.dupe_count.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.file_label = QLabel()
self.file_label.setObjectName('fileLabel')
# self.file_label.setStyleSheet(
# # 'background:blue;'
# # 'text-align:center;'
# 'font-weight:bold;'
# 'font-size:14px;'
# # 'padding-top: 6px'
# '')
# self.file_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.file_label.setText('No DupeGuru File Selected')
self.open_button = QPushButton()
self.open_button.setText('&Load DupeGuru File')
self.open_button.clicked.connect(lambda: self.select_file())
self.mirror_button = QPushButton()
self.mirror_modal = MirrorEntriesModal(self.lib, self.driver)
self.mirror_modal.done.connect(lambda: self.refresh_dupes())
self.mirror_button.setText('&Mirror Entries')
self.mirror_button.clicked.connect(lambda: self.mirror_modal.show())
self.mirror_desc = QLabel()
self.mirror_desc.setWordWrap(True)
self.mirror_desc.setText("""Mirror the Entry data across each duplicate match set, combining all data while not removing or duplicating fields. This operation will not delete any files or data.""")
# self.mirror_delete_button = QPushButton()
# self.mirror_delete_button.setText('Mirror && Delete')
self.advice_label = QLabel()
self.advice_label.setWordWrap(True)
self.advice_label.setText("""After mirroring, you're free to use DupeGuru to delete the unwanted files. Afterwards, use TagStudio's "Fix Unlinked Entries" feature in the Tools menu in order to delete the unlinked Entries.""")
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.done_button = QPushButton()
self.done_button.setText('&Done')
# self.save_button.setAutoDefault(True)
self.done_button.setDefault(True)
self.done_button.clicked.connect(self.hide)
# self.done_button.clicked.connect(lambda: self.done.emit(self.combo_box.currentIndex()))
# self.save_button.clicked.connect(lambda: save_callback(widget.get_content()))
self.button_layout.addWidget(self.done_button)
# self.returnPressed.connect(lambda: self.done.emit(self.combo_box.currentIndex()))
# self.done.connect(lambda x: callback(x))
self.root_layout.addWidget(self.desc_widget)
self.root_layout.addWidget(self.dupe_count)
self.root_layout.addWidget(self.file_label)
self.root_layout.addWidget(self.open_button)
# self.mirror_delete_button.setHidden(True)
self.root_layout.addWidget(self.mirror_button)
self.root_layout.addWidget(self.mirror_desc)
# self.root_layout.addWidget(self.mirror_delete_button)
self.root_layout.addWidget(self.advice_label)
# self.root_layout.setStretch(1,2)
self.root_layout.addStretch(1)
self.root_layout.addWidget(self.button_container)
self.set_dupe_count(self.count)
def select_file(self):
qfd = QFileDialog(self,
'Open DupeGuru Results File',
os.path.normpath(self.lib.library_dir))
qfd.setFileMode(QFileDialog.FileMode.ExistingFile)
qfd.setNameFilter("DupeGuru Files (*.dupeguru)")
if qfd.exec_():
filename = qfd.selectedFiles()
if filename:
self.set_filename(filename[0])
def set_filename(self, filename:str):
if filename:
self.file_label.setText(filename)
else:
self.file_label.setText('No DupeGuru File Selected')
self.filename = filename
self.refresh_dupes()
self.mirror_modal.refresh_list()
def refresh_dupes(self):
self.lib.refresh_dupe_files(self.filename)
self.set_dupe_count(len(self.lib.dupe_files))
def set_dupe_count(self, count:int):
self.count = count
if self.count < 0:
self.mirror_button.setDisabled(True)
self.dupe_count.setText(f'Duplicate File Matches: N/A')
elif self.count == 0:
self.mirror_button.setDisabled(True)
self.dupe_count.setText(f'Duplicate File Matches: {count}')
else:
self.mirror_button.setDisabled(False)
self.dupe_count.setText(f'Duplicate File Matches: {count}')
class MirrorEntriesModal(QWidget):
done = Signal()
def __init__(self, library:'Library', driver:'QtDriver'):
super().__init__()
self.lib = library
self.driver:QtDriver = driver
self.setWindowTitle(f'Mirror Entries')
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(f'''
Are you sure you want to mirror the following {len(self.lib.dupe_files)} Entries?
''')
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.list_view = QListView()
self.model = QStandardItemModel()
self.list_view.setModel(self.model)
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.cancel_button = QPushButton()
self.cancel_button.setText('&Cancel')
self.cancel_button.setDefault(True)
self.cancel_button.clicked.connect(self.hide)
self.button_layout.addWidget(self.cancel_button)
self.mirror_button = QPushButton()
self.mirror_button.setText('&Mirror')
self.mirror_button.clicked.connect(self.hide)
self.mirror_button.clicked.connect(lambda: self.mirror_entries())
self.button_layout.addWidget(self.mirror_button)
self.root_layout.addWidget(self.desc_widget)
self.root_layout.addWidget(self.list_view)
self.root_layout.addWidget(self.button_container)
def refresh_list(self):
self.desc_widget.setText(f'''
Are you sure you want to mirror the following {len(self.lib.dupe_files)} Entries?
''')
self.model.clear()
for i in self.lib.dupe_files:
self.model.appendRow(QStandardItem(str(i)))
def mirror_entries(self):
# pb = QProgressDialog('', None, 0, len(self.lib.dupe_files))
# # pb.setMaximum(len(self.lib.missing_files))
# pb.setFixedSize(432, 112)
# pb.setWindowFlags(pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
# pb.setWindowTitle('Mirroring Entries')
# pb.setWindowModality(Qt.WindowModality.ApplicationModal)
# pb.show()
# r = CustomRunnable(lambda: self.mirror_entries_runnable(pb))
# r.done.connect(lambda: self.done.emit())
# r.done.connect(lambda: self.driver.preview_panel.refresh())
# # r.done.connect(lambda: self.model.clear())
# # QThreadPool.globalInstance().start(r)
# r.run()
iterator = FunctionIterator(self.mirror_entries_runnable)
pw = ProgressWidget(
window_title='Mirroring Entries',
label_text=f'Mirroring 1/{len(self.lib.dupe_files)} Entries...',
cancel_button_text=None,
minimum=0,
maximum=len(self.lib.dupe_files)
)
pw.show()
iterator.value.connect(lambda x: pw.update_progress(x+1))
iterator.value.connect(lambda x: pw.update_label(f'Mirroring {x+1}/{len(self.lib.dupe_files)} Entries...'))
r = CustomRunnable(lambda:iterator.run())
QThreadPool.globalInstance().start(r)
r.done.connect(lambda: (
pw.hide(),
pw.deleteLater(),
self.driver.preview_panel.update_widgets(),
self.done.emit()
))
def mirror_entries_runnable(self):
mirrored = []
for i, dupe in enumerate(self.lib.dupe_files):
# pb.setValue(i)
# pb.setLabelText(f'Mirroring {i}/{len(self.lib.dupe_files)} Entries')
entry_id_1 = self.lib.get_entry_id_from_filepath(
dupe[0])
entry_id_2 = self.lib.get_entry_id_from_filepath(
dupe[1])
self.lib.mirror_entry_fields([entry_id_1, entry_id_2])
sleep(0.005)
yield i
for d in mirrored:
self.lib.dupe_files.remove(d)
# self.driver.filter_items('')
# self.done.emit()
class FixUnlinkedEntriesModal(QWidget):
# done = Signal(int)
def __init__(self, library:'Library', driver:'QtDriver'):
super().__init__()
self.lib = library
self.driver:QtDriver = driver
self.count = -1
self.setWindowTitle(f'Fix Unlinked Entries')
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(400, 300)
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.setStyleSheet(
# 'background:blue;'
'text-align:left;'
# 'font-weight:bold;'
# 'font-size:14px;'
# 'padding-top: 6px'
'')
self.desc_widget.setText('''Each library entry is linked to a file in one of your directories. If a file linked to an entry is moved or deleted outside of TagStudio, it is then considered unlinked.
Unlinked entries may be automatically relinked via searching your directories, manually relinked by the user, or deleted if desired.''')
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.missing_count = QLabel()
self.missing_count.setObjectName('missingCountLabel')
self.missing_count.setStyleSheet(
# 'background:blue;'
# 'text-align:center;'
'font-weight:bold;'
'font-size:14px;'
# 'padding-top: 6px'
'')
self.missing_count.setAlignment(Qt.AlignmentFlag.AlignCenter)
# self.missing_count.setText('Missing Files: N/A')
self.refresh_button = QPushButton()
self.refresh_button.setText('&Refresh')
self.refresh_button.clicked.connect(lambda: self.refresh_missing_files())
self.search_button = QPushButton()
self.search_button.setText('&Search && Relink')
self.relink_class = RelinkUnlinkedEntries(self.lib, self.driver)
self.relink_class.done.connect(lambda: self.refresh_missing_files())
self.relink_class.done.connect(lambda: self.driver.update_thumbs())
self.search_button.clicked.connect(lambda: self.relink_class.repair_entries())
self.manual_button = QPushButton()
self.manual_button.setText('&Manual Relink')
self.delete_button = QPushButton()
self.delete_modal = DeleteUnlinkedEntriesModal(self.lib, self.driver)
self.delete_modal.done.connect(lambda: self.set_missing_count(len(self.lib.missing_files)))
self.delete_modal.done.connect(lambda: self.driver.update_thumbs())
self.delete_button.setText('De&lete Unlinked Entries')
self.delete_button.clicked.connect(lambda: self.delete_modal.show())
# self.combo_box = QComboBox()
# self.combo_box.setEditable(False)
# # self.combo_box.setMaxVisibleItems(5)
# self.combo_box.setStyleSheet('combobox-popup:0;')
# self.combo_box.view().setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
# for df in self.lib.default_fields:
# self.combo_box.addItem(f'{df["name"]} ({df["type"].replace("_", " ").title()})')
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.done_button = QPushButton()
self.done_button.setText('&Done')
# self.save_button.setAutoDefault(True)
self.done_button.setDefault(True)
self.done_button.clicked.connect(self.hide)
# self.done_button.clicked.connect(lambda: self.done.emit(self.combo_box.currentIndex()))
# self.save_button.clicked.connect(lambda: save_callback(widget.get_content()))
self.button_layout.addWidget(self.done_button)
# self.returnPressed.connect(lambda: self.done.emit(self.combo_box.currentIndex()))
# self.done.connect(lambda x: callback(x))
self.root_layout.addWidget(self.desc_widget)
self.root_layout.addWidget(self.missing_count)
self.root_layout.addWidget(self.refresh_button)
self.root_layout.addWidget(self.search_button)
self.manual_button.setHidden(True)
self.root_layout.addWidget(self.manual_button)
self.root_layout.addWidget(self.delete_button)
# self.root_layout.setStretch(1,2)
self.root_layout.addStretch(1)
self.root_layout.addWidget(self.button_container)
self.set_missing_count(self.count)
def refresh_missing_files(self):
logging.info(f'Start RMF: {QThread.currentThread()}')
# pb = QProgressDialog(f'Scanning Library for Unlinked Entries...', None, 0,len(self.lib.entries))
# pb.setFixedSize(432, 112)
# pb.setWindowFlags(pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
# pb.setWindowTitle('Scanning Library')
# pb.setWindowModality(Qt.WindowModality.ApplicationModal)
# pb.show()
iterator = FunctionIterator(self.lib.refresh_missing_files)
pw = ProgressWidget(
window_title='Scanning Library',
label_text=f'Scanning Library for Unlinked Entries...',
cancel_button_text=None,
minimum=0,
maximum=len(self.lib.entries)
)
pw.show()
iterator.value.connect(lambda v: pw.update_progress(v+1))
# rmf.value.connect(lambda v: pw.update_label(f'Progress: {v}'))
r = CustomRunnable(lambda:iterator.run())
QThreadPool.globalInstance().start(r)
r.done.connect(lambda: (pw.hide(), pw.deleteLater(),
self.set_missing_count(len(self.lib.missing_files)),
self.delete_modal.refresh_list()))
# r = CustomRunnable(lambda: self.lib.refresh_missing_files(lambda v: self.update_scan_value(pb, v)))
# r.done.connect(lambda: (pb.hide(), pb.deleteLater(), self.set_missing_count(len(self.lib.missing_files)), self.delete_modal.refresh_list()))
# QThreadPool.globalInstance().start(r)
# # r.run()
# pass
# def update_scan_value(self, pb:QProgressDialog, value=int):
# # pb.setLabelText(f'Scanning Library for Unlinked Entries ({value}/{len(self.lib.entries)})...')
# pb.setValue(value)
def set_missing_count(self, count:int):
self.count = count
if self.count < 0:
self.search_button.setDisabled(True)
self.delete_button.setDisabled(True)
self.missing_count.setText(f'Unlinked Entries: N/A')
elif self.count == 0:
self.search_button.setDisabled(True)
self.delete_button.setDisabled(True)
self.missing_count.setText(f'Unlinked Entries: {count}')
else:
self.search_button.setDisabled(False)
self.delete_button.setDisabled(False)
self.missing_count.setText(f'Unlinked Entries: {count}')
class DeleteUnlinkedEntriesModal(QWidget):
done = Signal()
def __init__(self, library:'Library', driver:'QtDriver'):
super().__init__()
self.lib = library
self.driver:QtDriver = driver
self.setWindowTitle(f'Delete Unlinked Entries')
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(f'''
Are you sure you want to delete the following {len(self.lib.missing_files)} entries?
''')
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.list_view = QListView()
self.model = QStandardItemModel()
self.list_view.setModel(self.model)
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.cancel_button = QPushButton()
self.cancel_button.setText('&Cancel')
self.cancel_button.setDefault(True)
self.cancel_button.clicked.connect(self.hide)
self.button_layout.addWidget(self.cancel_button)
self.delete_button = QPushButton()
self.delete_button.setText('&Delete')
self.delete_button.clicked.connect(self.hide)
self.delete_button.clicked.connect(lambda: self.delete_entries())
self.button_layout.addWidget(self.delete_button)
self.root_layout.addWidget(self.desc_widget)
self.root_layout.addWidget(self.list_view)
self.root_layout.addWidget(self.button_container)
def refresh_list(self):
self.desc_widget.setText(f'''
Are you sure you want to delete the following {len(self.lib.missing_files)} entries?
''')
self.model.clear()
for i in self.lib.missing_files:
self.model.appendRow(QStandardItem(i))
def delete_entries(self):
# pb = QProgressDialog('', None, 0, len(self.lib.missing_files))
# # pb.setMaximum(len(self.lib.missing_files))
# pb.setFixedSize(432, 112)
# pb.setWindowFlags(pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
# pb.setWindowTitle('Deleting Entries')
# pb.setWindowModality(Qt.WindowModality.ApplicationModal)
# pb.show()
# r = CustomRunnable(lambda: self.lib.ref(pb))
# r.done.connect(lambda: self.done.emit())
# # r.done.connect(lambda: self.model.clear())
# QThreadPool.globalInstance().start(r)
# # r.run()
iterator = FunctionIterator(self.lib.remove_missing_files)
pw = ProgressWidget(
window_title='Deleting Entries',
label_text='',
cancel_button_text=None,
minimum=0,
maximum=len(self.lib.missing_files)
)
pw.show()
iterator.value.connect(lambda x: pw.update_progress(x[0]+1))
iterator.value.connect(lambda x: pw.update_label(f'Deleting {x[0]+1}/{len(self.lib.missing_files)} Unlinked Entries'))
iterator.value.connect(lambda x: self.driver.purge_item_from_navigation(ItemType.ENTRY, x[1]))
r = CustomRunnable(lambda:iterator.run())
QThreadPool.globalInstance().start(r)
r.done.connect(lambda: (pw.hide(), pw.deleteLater(), self.done.emit()))
# def delete_entries_runnable(self):
# deleted = []
# for i, missing in enumerate(self.lib.missing_files):
# # pb.setValue(i)
# # pb.setLabelText(f'Deleting {i}/{len(self.lib.missing_files)} Unlinked Entries')
# try:
# id = self.lib.get_entry_id_from_filepath(missing)
# logging.info(f'Removing Entry ID {id}:\n\t{missing}')
# self.lib.remove_entry(id)
# self.driver.purge_item_from_navigation(ItemType.ENTRY, id)
# deleted.append(missing)
# except KeyError:
# logging.info(
# f'{ERROR} \"{id}\" was reported as missing, but is not in the file_to_entry_id map.')
# yield i
# for d in deleted:
# self.lib.missing_files.remove(d)
# # self.driver.filter_items('')
# # self.done.emit()
class RelinkUnlinkedEntries(QObject):
done = Signal()
def __init__(self, library:'Library', driver:'QtDriver'):
super().__init__()
self.lib = library
self.driver:QtDriver = driver
self.fixed = 0
def repair_entries(self):
# pb = QProgressDialog('', None, 0, len(self.lib.missing_files))
# # pb.setMaximum(len(self.lib.missing_files))
# pb.setFixedSize(432, 112)
# pb.setWindowFlags(pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
# pb.setWindowTitle('Relinking Entries')
# pb.setWindowModality(Qt.WindowModality.ApplicationModal)
# pb.show()
# r = CustomRunnable(lambda: self.repair_entries_runnable(pb))
# r.done.connect(lambda: self.done.emit())
# # r.done.connect(lambda: self.model.clear())
# QThreadPool.globalInstance().start(r)
# # r.run()
iterator = FunctionIterator(self.lib.fix_missing_files)
pw = ProgressWidget(
window_title='Relinking Entries',
label_text='',
cancel_button_text=None,
minimum=0,
maximum=len(self.lib.missing_files)
)
pw.show()
iterator.value.connect(lambda x: pw.update_progress(x[0]+1))
iterator.value.connect(lambda x: (self.increment_fixed() if x[1] else (), pw.update_label(f'Attempting to Relink {x[0]+1}/{len(self.lib.missing_files)} Entries, {self.fixed} Successfully Relinked')))
# iterator.value.connect(lambda x: self.driver.purge_item_from_navigation(ItemType.ENTRY, x[1]))
r = CustomRunnable(lambda:iterator.run())
r.done.connect(lambda: (pw.hide(), pw.deleteLater(), self.done.emit(), self.reset_fixed()))
QThreadPool.globalInstance().start(r)
def increment_fixed(self):
self.fixed += 1
def reset_fixed(self):
self.fixed = 0
# def repair_entries_runnable(self, pb: QProgressDialog):
# fixed = 0
# for i in self.lib.fix_missing_files():
# if i[1]:
# fixed += 1
# pb.setValue(i[0])
# pb.setLabelText(f'Attempting to Relink {i[0]+1}/{len(self.lib.missing_files)} Entries, {fixed} Successfully Relinked')
# for i, missing in enumerate(self.lib.missing_files):
# pb.setValue(i)
# pb.setLabelText(f'Relinking {i}/{len(self.lib.missing_files)} Unlinked Entries')
# self.lib.fix_missing_files()
# try:
# id = self.lib.get_entry_id_from_filepath(missing)
# logging.info(f'Removing Entry ID {id}:\n\t{missing}')
# self.lib.remove_entry(id)
# self.driver.purge_item_from_navigation(ItemType.ENTRY, id)
# deleted.append(missing)
# except KeyError:
# logging.info(
# f'{ERROR} \"{id}\" was reported as missing, but is not in the file_to_entry_id map.')
# for d in deleted:
# self.lib.missing_files.remove(d)
class PreviewPanel(QWidget):
"""The Preview Panel Widget."""
tags_updated = Signal()
@ -1921,17 +1318,6 @@ class ItemThumb(FlowWidget):
# e.remove_tag(1)
class CustomRunnable(QRunnable, QObject):
done = Signal()
def __init__(self, function) -> None:
QRunnable.__init__(self)
QObject.__init__(self)
self.function = function
def run(self):
self.function()
self.done.emit()
class QtDriver(QObject):
"""A Qt GUI frontend driver for TagStudio."""
@ -2974,258 +2360,3 @@ class QtDriver(QObject):
end_time = time.time()
self.main_window.statusbar.showMessage(f'Collage Saved at "{filename}" ({format_timespan(end_time - self.collage_start_time)})')
logging.info(f'Collage Saved at "{filename}" ({format_timespan(end_time - self.collage_start_time)})')
class FoldersToTagsModal(QWidget):
# done = Signal(int)
def __init__(self, library:'Library', driver:'QtDriver'):
super().__init__()
self.library = library
self.driver:QtDriver = driver
self.count = -1
self.filename = ''
self.setWindowTitle(f'Folders To Tags')
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(500, 800)
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.setStyleSheet(
# 'background:blue;'
'text-align:left;'
# 'font-weight:bold;'
'font-size:18px;'
# 'padding-top: 6px'
'')
self.desc_widget.setText('''Creates tags based on the folder structure and applies them to entries.\n The Structure below shows all the tags that would be added and to which files they would be added. It being empty means that there are no Tag to be created or assigned''')
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.scroll_contents = QWidget()
self.scroll_layout = QVBoxLayout(self.scroll_contents)
self.scroll_layout.setContentsMargins(6,0,6,0)
self.scroll_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.scroll_area = QScrollArea()
self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setFrameShadow(QFrame.Shadow.Plain)
self.scroll_area.setFrameShape(QFrame.Shape.NoFrame)
self.scroll_area.setWidget(self.scroll_contents)
self.apply_button = QPushButton()
self.apply_button.setText('&Apply')
self.apply_button.clicked.connect(lambda: self.folders_to_tags(self.library))
self.showEvent = self.on_open
self.root_layout.addWidget(self.desc_widget)
self.root_layout.addWidget(self.scroll_area)
self.root_layout.addWidget(self.apply_button)
def on_open(self,event):
for i in reversed(range(self.scroll_layout.count())):
self.scroll_layout.itemAt(i).widget().setParent(None)
data = self.generate_preview_data(self.library)
for folder in data["dirs"].values():
test = self.TreeItemTest(folder,None)
self.scroll_layout.addWidget(test)
def generate_preview_data(self,library:Library):
tree = dict(dirs={},files=[])
def add_tag_to_tree(list:list[Tag]):
branch = tree
for tag in list:
if tag.name not in branch["dirs"]:
branch["dirs"][tag.name] = dict(dirs={},tag=tag,files=[])
branch = branch["dirs"][tag.name]
def add_folders_to_tree(list:list[str])->Tag:
branch = tree
for folder in list:
if folder not in branch["dirs"]:
new_tag = Tag(-1, folder,"",[],[],"green")
branch["dirs"][folder] = dict(dirs={},tag=new_tag,files=[])
branch = branch["dirs"][folder]
return branch
for tag in library.tags:
reversed_tag = self.reverse_tag(tag,None)
logging.info(set(map(lambda tag:tag.name ,reversed_tag)))
add_tag_to_tree(reversed_tag)
for entry in library.entries:
folders = entry.path.split("\\")
if len(folders) == 1 and folders[0] == "": continue
branch = add_folders_to_tree(folders)
if branch:
field_indexes = library.get_field_index_in_entry(entry,6)
has_tag=False
for index in field_indexes:
content = library.get_field_attr(entry.fields[index],"content")
for tag_id in content:
tag = library.get_tag(tag_id)
if tag.name == branch["tag"].name:
has_tag=True
break
if not has_tag:
branch["files"].append(entry.filename)
def cut_branches_adding_nothing(branch:dict):
folders = set(branch["dirs"].keys())
for folder in folders:
logging.info(folder)
cut = cut_branches_adding_nothing(branch["dirs"][folder])
if cut:
branch['dirs'].pop(folder)
if not "tag" in branch: return
if branch["tag"].id == -1:#Needs to be first
return False
if len(branch["dirs"].keys()) == 0:
return True
cut_branches_adding_nothing(tree)
return tree
def folders_to_tags(self,library:Library):
logging.info("Converting folders to Tags")
tree = dict(dirs={})
def add_tag_to_tree(list:list[Tag]):
branch = tree
for tag in list:
if tag.name not in branch["dirs"]:
branch["dirs"][tag.name] = dict(dirs={},tag=tag)
branch = branch["dirs"][tag.name]
def add_folders_to_tree(list:list[str])->Tag:
branch = tree
for folder in list:
if folder not in branch["dirs"]:
new_tag = Tag(-1, folder,"",[],([branch["tag"].id] if "tag" in branch else []),"")
library.add_tag_to_library(new_tag)
branch["dirs"][folder] = dict(dirs={},tag=new_tag)
branch = branch["dirs"][folder]
return branch["tag"]
for tag in library.tags:
reversed_tag = self.reverse_tag(tag,None)
add_tag_to_tree(reversed_tag)
for entry in library.entries:
folders = entry.path.split("\\")
if len(folders)== 1 and folders[0]=="": continue
tag = add_folders_to_tree(folders)
if tag:
if not entry.has_tag(library,tag.id):
entry.add_tag(library,tag.id,6)
self.close()
logging.info("Done")
def reverse_tag(self,tag:Tag,list:list[Tag]) -> list[Tag]:
if list != None:
list.append(tag)
else:
list = [tag]
if len(tag.subtag_ids) == 0:
list.reverse()
return list
else:
for subtag_id in tag.subtag_ids:
subtag = self.library.get_tag(subtag_id)
return self.reverse_tag(subtag,list)
class ModifiedTagWidget(QWidget): # Needed to be modified because the original searched the display name in the library where it wasn't added yet
def __init__(self, tag:Tag,parentTag:Tag) -> None:
super().__init__()
self.tag = tag
self.setCursor(Qt.CursorShape.PointingHandCursor)
self.base_layout = QVBoxLayout(self)
self.base_layout.setObjectName('baseLayout')
self.base_layout.setContentsMargins(0, 0, 0, 0)
self.bg_button = QPushButton(self)
self.bg_button.setFlat(True)
if parentTag != None:
text = f"{tag.name} ({parentTag.name})".replace('&', '&&')
else:
text = tag.name.replace('&', '&&')
self.bg_button.setText(text)
self.bg_button.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
self.inner_layout = QHBoxLayout()
self.inner_layout.setObjectName('innerLayout')
self.inner_layout.setContentsMargins(2, 2, 2, 2)
self.bg_button.setLayout(self.inner_layout)
self.bg_button.setMinimumSize(math.ceil(22*1.5), 22)
self.bg_button.setStyleSheet(
f'QPushButton{{'
f'background: {get_tag_color(ColorType.PRIMARY, tag.color)};'
f"color: {get_tag_color(ColorType.TEXT, tag.color)};"
f'font-weight: 600;'
f"border-color:{get_tag_color(ColorType.BORDER, tag.color)};"
f'border-radius: 6px;'
f'border-style:inset;'
f'border-width: {math.ceil(1*self.devicePixelRatio())}px;'
f'padding-right: 4px;'
f'padding-bottom: 1px;'
f'padding-left: 4px;'
f'font-size: 13px'
f'}}'
f'QPushButton::hover{{'
f"border-color:{get_tag_color(ColorType.LIGHT_ACCENT, tag.color)};"
f'}}')
self.base_layout.addWidget(self.bg_button)
self.setMinimumSize(50,20)
class TreeItemTest(QWidget):
def __init__(self,data:dict,parentTag:Tag):
super().__init__()
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(20,0,0,0)
self.root_layout.setSpacing(1)
self.test = QWidget()
self.root_layout.addWidget(self.test)
self.tag_layout = FlowLayout(self.test)
self.tag_widget = FoldersToTagsModal.ModifiedTagWidget(data["tag"],parentTag)
self.tag_widget.bg_button.clicked.connect(lambda:self.hide_show())
self.tag_layout.addWidget(self.tag_widget)
self.children_widget = QWidget()
self.children_layout = QVBoxLayout(self.children_widget)
self.root_layout.addWidget(self.children_widget)
self.populate(data)
def hide_show(self):
self.children_widget.setHidden(not self.children_widget.isHidden())
def populate(self,data:dict):
for folder in data["dirs"].values():
item = FoldersToTagsModal.TreeItemTest(folder,data["tag"])
self.children_layout.addWidget(item)
for file in data["files"]:
label = QLabel()
label.setText(file)
self.children_layout.addWidget(label)
if len(data["files"]) == 0 and len(data["dirs"].values()) == 0:
self.hide_show()