Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add folders to tags tool #58

Merged
merged 10 commits into from
Apr 27, 2024
262 changes: 261 additions & 1 deletion tagstudio/src/qt/ts_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -3909,6 +3909,11 @@ def start(self):
self.sort_fields_action.setToolTip('Alt+S')
macros_menu.addAction(self.sort_fields_action)

folders_to_tags_action = QAction('Folders to Tags', menu_bar)
ftt_modal = FoldersToTagsModal(self.lib, self)
folders_to_tags_action.triggered.connect(lambda:ftt_modal.show())
macros_menu.addAction(folders_to_tags_action)

self.set_macro_menu_viability()

menu_bar.addMenu(file_menu)
Expand Down Expand Up @@ -4712,7 +4717,7 @@ def create_collage(self) -> None:

if not data_only_mode:
time.sleep(5)

self.collage = Image.new('RGB', (img_size,img_size))
i = 0
self.completed = 0
Expand Down Expand Up @@ -4747,3 +4752,258 @@ def try_save_collage(self, increment_progress:bool):
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()