Skip to content

Commit

Permalink
Merge pull request #191 from yggdrasil75/addregex
Browse files Browse the repository at this point in the history
Add option to use regex for find and replace
  • Loading branch information
jhc13 authored Jun 16, 2024
2 parents 1386d3f + ae25941 commit 8d541f3
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 36 deletions.
56 changes: 37 additions & 19 deletions taggui/dialogs/find_and_replace_dialog.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import re

from PySide6.QtCore import Qt, Slot
from PySide6.QtWidgets import (QDialog, QGridLayout, QLabel, QPushButton,
QVBoxLayout)
Expand Down Expand Up @@ -33,13 +35,15 @@ def __init__(self, parent, image_list_model: ImageListModel):
Qt.AlignmentFlag.AlignRight)
grid_layout.addWidget(QLabel('Whole tags only'), 3, 0,
Qt.AlignmentFlag.AlignRight)
self.find_line_edit = SettingsLineEdit(key='find_text')
self.find_line_edit.setClearButtonEnabled(True)
self.find_line_edit.textChanged.connect(self.display_match_count)
grid_layout.addWidget(self.find_line_edit, 0, 1)
self.replace_line_edit = SettingsLineEdit(key='replace_text')
self.replace_line_edit.setClearButtonEnabled(True)
grid_layout.addWidget(self.replace_line_edit, 1, 1)
grid_layout.addWidget(QLabel('Use regex for find text'), 4, 0,
Qt.AlignmentFlag.AlignRight)
self.find_text_line_edit = SettingsLineEdit(key='find_text')
self.find_text_line_edit.setClearButtonEnabled(True)
self.find_text_line_edit.textChanged.connect(self.display_match_count)
grid_layout.addWidget(self.find_text_line_edit, 0, 1)
self.replace_text_line_edit = SettingsLineEdit(key='replace_text')
self.replace_text_line_edit.setClearButtonEnabled(True)
grid_layout.addWidget(self.replace_text_line_edit, 1, 1)
self.scope_combo_box = SettingsComboBox(key='replace_scope')
self.scope_combo_box.addItems(list(Scope))
self.scope_combo_box.currentTextChanged.connect(
Expand All @@ -50,40 +54,54 @@ def __init__(self, parent, image_list_model: ImageListModel):
self.whole_tags_only_check_box.stateChanged.connect(
self.display_match_count)
grid_layout.addWidget(self.whole_tags_only_check_box, 3, 1)
self.use_regex_check_box = SettingsBigCheckBox(key='replace_use_regex',
default=False)
self.use_regex_check_box.stateChanged.connect(self.display_match_count)
grid_layout.addWidget(self.use_regex_check_box, 4, 1)
layout.addLayout(grid_layout)
self.replace_button = QPushButton('Replace')
self.replace_button.clicked.connect(self.replace)
self.replace_button.clicked.connect(self.display_match_count)
layout.addWidget(self.replace_button)
self.display_match_count()

def disable_replace_button(self):
self.replace_button.setText('Replace')
self.replace_button.setEnabled(False)

@Slot()
def display_match_count(self):
text = self.find_line_edit.text()
text = self.find_text_line_edit.text()
if not text:
self.replace_button.setText('Replace')
self.replace_button.setEnabled(False)
self.disable_replace_button()
return
self.replace_button.setEnabled(True)
scope = self.scope_combo_box.currentText()
whole_tags_only = self.whole_tags_only_check_box.isChecked()
match_count = self.image_list_model.get_text_match_count(
text, scope, whole_tags_only)
use_regex = self.use_regex_check_box.isChecked()
try:
match_count = self.image_list_model.get_text_match_count(
text, scope, whole_tags_only, use_regex)
except re.error:
self.disable_replace_button()
return
self.replace_button.setText(f'Replace {match_count} '
f'{pluralize("instance", match_count)}')

@Slot()
def replace(self):
scope = self.scope_combo_box.currentText()
use_regex = self.use_regex_check_box.isChecked()
if self.whole_tags_only_check_box.isChecked():
replace_text = self.replace_line_edit.text()
replace_text = self.replace_text_line_edit.text()
if replace_text:
self.image_list_model.rename_tags([self.find_line_edit.text()],
replace_text, scope)
self.image_list_model.rename_tags(
[self.find_text_line_edit.text()], replace_text, scope,
use_regex)
else:
self.image_list_model.delete_tags([self.find_line_edit.text()],
scope)
self.image_list_model.delete_tags(
[self.find_text_line_edit.text()], scope, use_regex)
else:
self.image_list_model.find_and_replace(
self.find_line_edit.text(), self.replace_line_edit.text(),
scope)
self.find_text_line_edit.text(),
self.replace_text_line_edit.text(), scope, use_regex)
71 changes: 54 additions & 17 deletions taggui/models/image_list_model.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import random
import re
import sys
from collections import Counter, deque
from dataclasses import dataclass
Expand Down Expand Up @@ -241,21 +242,31 @@ def is_image_in_scope(self, scope: Scope | str, image_index: int,
return self.image_list_selection_model.isSelected(proxy_index)

def get_text_match_count(self, text: str, scope: Scope | str,
whole_tags_only: bool) -> int:
whole_tags_only: bool, use_regex: bool) -> int:
"""Get the number of instances of a text in all captions."""
match_count = 0
for image_index, image in enumerate(self.images):
if not self.is_image_in_scope(scope, image_index, image):
continue
if whole_tags_only:
match_count += image.tags.count(text)
if use_regex:
match_count += len([
tag for tag in image.tags
if re.fullmatch(pattern=text, string=tag)
])
else:
match_count += image.tags.count(text)
else:
caption = self.tag_separator.join(image.tags)
match_count += caption.count(text)
if use_regex:
match_count += len(re.findall(pattern=text,
string=caption))
else:
match_count += caption.count(text)
return match_count

def find_and_replace(self, find_text: str, replace_text: str,
scope: Scope | str):
scope: Scope | str, use_regex: bool):
"""
Find and replace arbitrary text in captions, within and across tag
boundaries.
Expand All @@ -269,10 +280,16 @@ def find_and_replace(self, find_text: str, replace_text: str,
if not self.is_image_in_scope(scope, image_index, image):
continue
caption = self.tag_separator.join(image.tags)
if find_text not in caption:
continue
if use_regex:
if not re.search(pattern=find_text, string=caption):
continue
caption = re.sub(pattern=find_text, repl=replace_text,
string=caption)
else:
if find_text not in caption:
continue
caption = caption.replace(find_text, replace_text)
changed_image_indices.append(image_index)
caption = caption.replace(find_text, replace_text)
image.tags = caption.split(self.tag_separator)
self.write_image_tags_to_disk(image)
if changed_image_indices:
Expand Down Expand Up @@ -465,39 +482,59 @@ def add_tags(self, tags: list[str], image_indices: list[QModelIndex]):

@Slot(list, str)
def rename_tags(self, old_tags: list[str], new_tag: str,
scope: Scope | str = Scope.ALL_IMAGES):
scope: Scope | str = Scope.ALL_IMAGES,
use_regex: bool = False):
self.add_to_undo_stack(
action_name=f'Rename {pluralize("Tag", len(old_tags))}',
should_ask_for_confirmation=True)
changed_image_indices = []
for image_index, image in enumerate(self.images):
if not self.is_image_in_scope(scope, image_index, image):
continue
if not any(old_tag in image.tags for old_tag in old_tags):
continue
if use_regex:
pattern = old_tags[0]
if not any(re.fullmatch(pattern=pattern, string=image_tag)
for image_tag in image.tags):
continue
image.tags = [new_tag if re.fullmatch(pattern=pattern,
string=image_tag)
else image_tag for image_tag in image.tags]
else:
if not any(old_tag in image.tags for old_tag in old_tags):
continue
image.tags = [new_tag if image_tag in old_tags else image_tag
for image_tag in image.tags]
changed_image_indices.append(image_index)
image.tags = [new_tag if image_tag in old_tags else image_tag
for image_tag in image.tags]
self.write_image_tags_to_disk(image)
if changed_image_indices:
self.dataChanged.emit(self.index(changed_image_indices[0]),
self.index(changed_image_indices[-1]))

@Slot(list)
def delete_tags(self, tags: list[str],
scope: Scope | str = Scope.ALL_IMAGES):
scope: Scope | str = Scope.ALL_IMAGES,
use_regex: bool = False):
self.add_to_undo_stack(
action_name=f'Delete {pluralize("Tag", len(tags))}',
should_ask_for_confirmation=True)
changed_image_indices = []
for image_index, image in enumerate(self.images):
if not self.is_image_in_scope(scope, image_index, image):
continue
if not any(tag in image.tags for tag in tags):
continue
if use_regex:
pattern = tags[0]
if not any(re.fullmatch(pattern=pattern, string=image_tag)
for image_tag in image.tags):
continue
image.tags = [image_tag for image_tag in image.tags
if not re.fullmatch(pattern=pattern,
string=image_tag)]
else:
if not any(tag in image.tags for tag in tags):
continue
image.tags = [image_tag for image_tag in image.tags
if image_tag not in tags]
changed_image_indices.append(image_index)
image.tags = [image_tag for image_tag in image.tags
if image_tag not in tags]
self.write_image_tags_to_disk(image)
if changed_image_indices:
self.dataChanged.emit(self.index(changed_image_indices[0]),
Expand Down

0 comments on commit 8d541f3

Please sign in to comment.