Files
HS-Rename/gui/rule_widgets.py
2026-03-29 20:50:00 -05:00

376 lines
12 KiB
Python

"""
Rule configuration widgets: each rule type has a small form that updates the Rule model.
"""
from PyQt6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QSpinBox,
QComboBox,
QCheckBox,
QGroupBox,
QFormLayout,
QStackedWidget,
QPushButton,
QFileDialog,
)
from PyQt6.QtCore import pyqtSignal
from engine.rules import (
ReplaceRule,
InsertRule,
RemoveRule,
CaseRule,
NumberingRule,
EpisodeRenumberRule,
RegexRule,
PrefixSuffixRule,
CsvMappingRule,
)
class ReplaceRuleWidget(QWidget):
ruleChanged = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
layout = QFormLayout(self)
self.enabled_cb = QCheckBox("Use this rule")
self.enabled_cb.setChecked(False)
self.enabled_cb.toggled.connect(self._emit)
layout.addRow(self.enabled_cb)
self.find = QLineEdit()
self.find.setPlaceholderText("Text to find")
self.find.textChanged.connect(self._emit)
self.replace = QLineEdit()
self.replace.setPlaceholderText("Replace with")
self.replace.textChanged.connect(self._emit)
self.caseSensitive = QCheckBox("Case sensitive")
self.caseSensitive.toggled.connect(self._emit)
self.wholeWord = QCheckBox("Whole word only")
self.wholeWord.toggled.connect(self._emit)
layout.addRow("Find:", self.find)
layout.addRow("Replace with:", self.replace)
layout.addRow(self.caseSensitive)
layout.addRow(self.wholeWord)
def _emit(self):
self.ruleChanged.emit()
def getRule(self) -> ReplaceRule:
r = ReplaceRule(
find=self.find.text(),
replace=self.replace.text(),
case_sensitive=self.caseSensitive.isChecked(),
whole_word=self.wholeWord.isChecked(),
)
r.enabled = self.enabled_cb.isChecked()
return r
class RegexRuleWidget(QWidget):
ruleChanged = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
layout = QFormLayout(self)
self.enabled_cb = QCheckBox("Use this rule")
self.enabled_cb.setChecked(False)
self.enabled_cb.toggled.connect(self._emit)
layout.addRow(self.enabled_cb)
self.pattern = QLineEdit()
self.pattern.setPlaceholderText(r"e.g. S(\d+)E(\d+)")
self.pattern.textChanged.connect(self._emit)
self.replacement = QLineEdit()
self.replacement.setPlaceholderText(r"e.g. S\1E\2")
self.replacement.textChanged.connect(self._emit)
layout.addRow("Regex pattern:", self.pattern)
layout.addRow("Replacement:", self.replacement)
def _emit(self):
self.ruleChanged.emit()
def getRule(self) -> RegexRule:
r = RegexRule(pattern=self.pattern.text(), replacement=self.replacement.text())
r.enabled = self.enabled_cb.isChecked()
return r
class InsertRuleWidget(QWidget):
ruleChanged = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
layout = QFormLayout(self)
self.enabled_cb = QCheckBox("Use this rule")
self.enabled_cb.setChecked(False)
self.enabled_cb.toggled.connect(self._emit)
layout.addRow(self.enabled_cb)
self.text = QLineEdit()
self.text.setPlaceholderText("Text to insert")
self.text.textChanged.connect(self._emit)
self.position = QComboBox()
self.position.addItems(["At start", "At end", "At position..."])
self.position.setCurrentIndex(0)
self.position.currentIndexChanged.connect(self._emit)
self.positionSpin = QSpinBox()
self.positionSpin.setMinimum(0)
self.positionSpin.setMaximum(9999)
self.positionSpin.valueChanged.connect(self._emit)
layout.addRow("Text:", self.text)
layout.addRow("Position:", self.position)
layout.addRow("Index:", self.positionSpin)
def _emit(self):
self.ruleChanged.emit()
def getRule(self) -> InsertRule:
idx = self.position.currentIndex()
if idx == 0:
pos = 0
elif idx == 1:
pos = -1
else:
pos = self.positionSpin.value()
r = InsertRule(text=self.text.text(), position=pos)
r.enabled = self.enabled_cb.isChecked()
return r
class RemoveRuleWidget(QWidget):
ruleChanged = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
layout = QFormLayout(self)
self.enabled_cb = QCheckBox("Use this rule")
self.enabled_cb.setChecked(False)
self.enabled_cb.toggled.connect(self._emit)
layout.addRow(self.enabled_cb)
self.removeType = QComboBox()
self.removeType.addItems([
"Remove text",
"Remove all digits",
"Remove first N characters",
"Remove last N characters",
])
self.removeType.currentIndexChanged.connect(self._emit)
self.value = QLineEdit()
self.value.setPlaceholderText("Text or number")
self.value.textChanged.connect(self._emit)
layout.addRow("Type:", self.removeType)
layout.addRow("Value:", self.value)
def _emit(self):
self.ruleChanged.emit()
def getRule(self) -> RemoveRule:
idx = self.removeType.currentIndex()
types = ["chars", "digits", "first_n", "last_n"]
r = RemoveRule(remove_type=types[idx], value=self.value.text())
r.enabled = self.enabled_cb.isChecked()
return r
class CaseRuleWidget(QWidget):
ruleChanged = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
layout = QFormLayout(self)
self.enabled_cb = QCheckBox("Use this rule")
self.enabled_cb.setChecked(False)
self.enabled_cb.toggled.connect(self._emit)
layout.addRow(self.enabled_cb)
self.caseType = QComboBox()
self.caseType.addItems(["Title Case", "UPPER", "lower", "Sentence case"])
self.caseType.currentIndexChanged.connect(self._emit)
layout.addRow("Case:", self.caseType)
def _emit(self):
self.ruleChanged.emit()
def getRule(self) -> CaseRule:
types = ["title", "upper", "lower", "sentence"]
r = CaseRule(case_type=types[self.caseType.currentIndex()])
r.enabled = self.enabled_cb.isChecked()
return r
class NumberingRuleWidget(QWidget):
ruleChanged = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
layout = QFormLayout(self)
self.enabled_cb = QCheckBox("Use this rule")
self.enabled_cb.setChecked(False)
self.enabled_cb.toggled.connect(self._emit)
layout.addRow(self.enabled_cb)
self.start = QSpinBox()
self.start.setMinimum(-9999)
self.start.setMaximum(99999)
self.start.setValue(1)
self.start.valueChanged.connect(self._emit)
self.step = QSpinBox()
self.step.setMinimum(1)
self.step.setMaximum(100)
self.step.setValue(1)
self.step.valueChanged.connect(self._emit)
self.padding = QSpinBox()
self.padding.setMinimum(1)
self.padding.setMaximum(10)
self.padding.setValue(2)
self.padding.valueChanged.connect(self._emit)
self.where = QComboBox()
self.where.addItems(["Prefix", "Suffix", "Insert at index"])
self.where.currentIndexChanged.connect(self._emit)
self.separator = QLineEdit()
self.separator.setText(" ")
self.separator.textChanged.connect(self._emit)
self.insertAt = QSpinBox()
self.insertAt.setMinimum(0)
self.insertAt.setMaximum(9999)
self.insertAt.valueChanged.connect(self._emit)
layout.addRow("Start:", self.start)
layout.addRow("Step:", self.step)
layout.addRow("Padding:", self.padding)
layout.addRow("Where:", self.where)
layout.addRow("Separator:", self.separator)
layout.addRow("Insert at index:", self.insertAt)
def _emit(self):
self.ruleChanged.emit()
def getRule(self) -> NumberingRule:
where_map = ["prefix", "suffix", "insert_at"]
r = NumberingRule(
start=self.start.value(),
step=self.step.value(),
padding=self.padding.value(),
where=where_map[self.where.currentIndex()],
insert_at=self.insertAt.value(),
separator=self.separator.text() or " ",
)
r.enabled = self.enabled_cb.isChecked()
return r
class EpisodeRenumberRuleWidget(QWidget):
ruleChanged = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
layout = QFormLayout(self)
self.enabled_cb = QCheckBox("Use this rule")
self.enabled_cb.setChecked(False)
self.enabled_cb.toggled.connect(self._emit)
layout.addRow(self.enabled_cb)
self.start = QSpinBox()
self.start.setMinimum(1)
self.start.setMaximum(9999)
self.start.setValue(1)
self.start.valueChanged.connect(self._emit)
self.step = QSpinBox()
self.step.setMinimum(1)
self.step.setValue(1)
self.step.valueChanged.connect(self._emit)
self.padding = QSpinBox()
self.padding.setMinimum(1)
self.padding.setMaximum(3)
self.padding.setValue(2)
self.padding.valueChanged.connect(self._emit)
layout.addRow("First episode number:", self.start)
layout.addRow("Step:", self.step)
layout.addRow("Zero-pad width:", self.padding)
info = QLabel(
"Matches S01E05 - Title (single) or S01E05-E06 - Title (two-part). "
"Ranges keep their length: S01E01-E02, S01E03-E04, … Order follows the file list / table sort."
)
info.setWordWrap(True)
layout.addRow(info)
def _emit(self):
self.ruleChanged.emit()
def getRule(self) -> EpisodeRenumberRule:
r = EpisodeRenumberRule(
start=self.start.value(),
step=self.step.value(),
padding=self.padding.value(),
)
r.enabled = self.enabled_cb.isChecked()
return r
class PrefixSuffixRuleWidget(QWidget):
ruleChanged = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
layout = QFormLayout(self)
self.enabled_cb = QCheckBox("Use this rule")
self.enabled_cb.setChecked(False)
self.enabled_cb.toggled.connect(self._emit)
layout.addRow(self.enabled_cb)
self.prefix = QLineEdit()
self.prefix.setPlaceholderText("Prefix")
self.prefix.textChanged.connect(self._emit)
self.suffix = QLineEdit()
self.suffix.setPlaceholderText("Suffix")
self.suffix.textChanged.connect(self._emit)
layout.addRow("Prefix:", self.prefix)
layout.addRow("Suffix:", self.suffix)
def _emit(self):
self.ruleChanged.emit()
def getRule(self) -> PrefixSuffixRule:
r = PrefixSuffixRule(prefix=self.prefix.text(), suffix=self.suffix.text())
r.enabled = self.enabled_cb.isChecked()
return r
class CsvMappingRuleWidget(QWidget):
ruleChanged = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
layout = QFormLayout(self)
self.enabled_cb = QCheckBox("Use this rule")
self.enabled_cb.setChecked(False)
self.enabled_cb.toggled.connect(self._emit)
layout.addRow(self.enabled_cb)
path_row = QHBoxLayout()
self.path_edit = QLineEdit()
self.path_edit.setPlaceholderText("Path to CSV file…")
self.path_edit.textChanged.connect(self._emit)
browse_btn = QPushButton("Browse…")
browse_btn.clicked.connect(self._browse)
path_row.addWidget(self.path_edit, 1)
path_row.addWidget(browse_btn)
layout.addRow("CSV file:", path_row)
info = QLabel('CSV must have columns "Original Name" and "Target Name". Lookup is by current filename.')
info.setWordWrap(True)
layout.addRow(info)
def _browse(self):
path, _ = QFileDialog.getOpenFileName(
self,
"Select CSV",
"",
"CSV (*.csv);;All files (*)",
)
if path:
self.path_edit.setText(path)
def _emit(self):
self.ruleChanged.emit()
def getRule(self) -> CsvMappingRule:
r = CsvMappingRule(csv_path=self.path_edit.text().strip())
r.enabled = self.enabled_cb.isChecked()
return r