599 lines
24 KiB
Python
599 lines
24 KiB
Python
"""Text sections of the main window: Subtitles (linked to the speech sounds) and Menu text.
|
|
|
|
One TextPanel per section: filters + table on top, the editor of the selected text below. Texts are written
|
|
through Workspace.texts (ackit/texts.py): subtitles in place into every copy (limited to the original's
|
|
length), menu texts into the localization files. Revert per text or all; changed-underneath warnings.
|
|
"""
|
|
from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, QTimer, Signal
|
|
from PySide6.QtWidgets import (QAbstractItemView, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFormLayout,
|
|
QHBoxLayout, QHeaderView, QLabel, QLineEdit, QPushButton, QSplitter, QTableView,
|
|
QVBoxLayout, QWidget)
|
|
|
|
from ackit import subtitles
|
|
from ackit.texts import CHANGED, FOREIGN, LOST, MODIFIED, ORIGINAL, TextConflictError
|
|
from . import icons
|
|
from .models import RowView
|
|
from .spinner import BusyOverlay
|
|
|
|
ANY = "Any"
|
|
STATE_TEXT = {ORIGINAL: "", MODIFIED: "modified", LOST: "lost", CHANGED: "changed outside", FOREIGN: "not original"}
|
|
|
|
|
|
class Row:
|
|
__slots__ = ("key", "cols", "text", "sound", "search")
|
|
|
|
def __init__(self, key, cols, text, sound=None):
|
|
self.key, self.cols, self.text, self.sound = key, cols, text, sound
|
|
self.search = (" ".join(str(c) for c in cols) + " " + text).lower()
|
|
|
|
|
|
class TextModel(QAbstractTableModel):
|
|
def __init__(self, headers, parent=None):
|
|
super().__init__(parent)
|
|
self.headers = headers + ["Text", "State"]
|
|
self.rows = []
|
|
self.modified = {} # key -> applied text
|
|
|
|
def reset(self, rows, modified):
|
|
self.beginResetModel()
|
|
self.rows, self.modified = rows, dict(modified)
|
|
self.endResetModel()
|
|
|
|
def set_modified(self, modified):
|
|
self.modified = dict(modified)
|
|
if self.rows:
|
|
self.dataChanged.emit(self.index(0, 0), self.index(len(self.rows) - 1, len(self.headers) - 1))
|
|
|
|
def rowCount(self, parent=QModelIndex()):
|
|
return 0 if parent.isValid() else len(self.rows)
|
|
|
|
def columnCount(self, parent=QModelIndex()):
|
|
return len(self.headers)
|
|
|
|
def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
|
|
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
|
|
return self.headers[section]
|
|
return None
|
|
|
|
def text_of(self, r):
|
|
return self.modified.get(r.key, r.text)
|
|
|
|
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
|
|
r = self.rows[index.row()]
|
|
c = index.column()
|
|
n = len(r.cols)
|
|
if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.ToolTipRole):
|
|
if c < n:
|
|
return r.cols[c]
|
|
if c == n:
|
|
return self.text_of(r)
|
|
return "modified" if r.key in self.modified else ""
|
|
if role == Qt.ItemDataRole.UserRole:
|
|
return r
|
|
if role == Qt.ItemDataRole.ForegroundRole and c == n + 1 and r.key in self.modified:
|
|
from PySide6.QtGui import QColor
|
|
return QColor(icons.ACCENT)
|
|
return None
|
|
|
|
|
|
class TextFilter(RowView):
|
|
"""The text table's rows: search words, column filters, modified only; sorted case-insensitively."""
|
|
|
|
def __init__(self, parent=None):
|
|
self.words, self.col_filters, self.only_modified = [], {}, False
|
|
super().__init__(parent)
|
|
|
|
def source_len(self):
|
|
return len(self.sourceModel().rows)
|
|
|
|
def sort_key(self, row, column):
|
|
m = self.sourceModel()
|
|
r = m.rows[row]
|
|
n = len(r.cols)
|
|
v = r.cols[column] if column < n else m.text_of(r) if column == n else ("modified" if r.key in m.modified else "")
|
|
return v.lower() if isinstance(v, str) else v
|
|
|
|
def accepts(self, row):
|
|
m = self.sourceModel()
|
|
r = m.rows[row]
|
|
if self.only_modified and r.key not in m.modified:
|
|
return False
|
|
for c, v in self.col_filters.items():
|
|
if v != ANY and r.cols[c] != v:
|
|
return False
|
|
if self.words:
|
|
hay = r.search + " " + m.modified.get(r.key, "").lower()
|
|
return all(w in hay for w in self.words)
|
|
return True
|
|
|
|
def row_at(self, index):
|
|
return self.sourceModel().rows[self.rows[index.row()]]
|
|
|
|
|
|
class FindReplaceDialog(QDialog):
|
|
def __init__(self, count, parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("Find and replace")
|
|
f = QFormLayout(self)
|
|
self.find, self.repl = QLineEdit(), QLineEdit()
|
|
self.case = QCheckBox("Match case")
|
|
f.addRow(QLabel("In the %d text(s) shown by the current filter:" % count))
|
|
f.addRow("Find", self.find)
|
|
f.addRow("Replace with", self.repl)
|
|
f.addRow(self.case)
|
|
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
|
bb.accepted.connect(self.accept)
|
|
bb.rejected.connect(self.reject)
|
|
f.addRow(bb)
|
|
|
|
|
|
def replace_in(text, find, repl, case):
|
|
if case:
|
|
return text.replace(find, repl)
|
|
import re
|
|
return re.sub(re.escape(find), lambda _: repl, text, flags=re.IGNORECASE)
|
|
|
|
|
|
class SubtitleBox(QWidget):
|
|
"""The subtitle of the sound open in the sound editor (side by side with the audio)."""
|
|
changed = Signal()
|
|
open_in_list = Signal(str) # SUB key
|
|
|
|
def __init__(self, win, parent=None):
|
|
super().__init__(parent)
|
|
self.win = win
|
|
self.keys = []
|
|
outer = QVBoxLayout(self)
|
|
outer.setContentsMargins(0, 0, 0, 0)
|
|
outer.setSpacing(6)
|
|
self.lay = QFormLayout() # one line edit per subtitle line
|
|
self.lay.setContentsMargins(0, 0, 0, 0)
|
|
self.edits = []
|
|
self.lbl = QLabel()
|
|
self.lbl.setObjectName("Muted")
|
|
row = QHBoxLayout()
|
|
row.setSpacing(6)
|
|
self.b_apply = QPushButton(icons.icon("apply"), "Apply")
|
|
self.b_revert = QPushButton(icons.icon("revert"), "Revert")
|
|
self.b_open = QPushButton(icons.icon("search"), "Show in list")
|
|
self.b_apply.setToolTip("Write this subtitle into the game (every copy)")
|
|
self.b_revert.setToolTip("Put the original subtitle back")
|
|
self.b_open.setToolTip("Open this subtitle in the Subtitles tab")
|
|
self.b_apply.clicked.connect(self.apply)
|
|
self.b_revert.clicked.connect(self.revert)
|
|
self.b_open.clicked.connect(lambda: self.keys and self.open_in_list.emit(self.keys[0]))
|
|
for b in (self.b_apply, self.b_revert): # two halves of the column, like the sound's action bar
|
|
row.addWidget(b, 1)
|
|
self.lbl.setWordWrap(True)
|
|
outer.addLayout(self.lay)
|
|
outer.addWidget(self.lbl) # room counter
|
|
outer.addWidget(self.b_open) # the way to the Subtitles tab, across the column
|
|
outer.addStretch(1) # actions at the bottom, level with the sound's
|
|
outer.addLayout(row)
|
|
|
|
def set_sound(self, s):
|
|
for e in self.edits:
|
|
self.lay.removeRow(e)
|
|
self.edits, self.keys = [], []
|
|
ws = self.win.ws
|
|
lines = ws.texts.for_sound(s, self.win.language) if ws and s else []
|
|
if not lines:
|
|
self.lbl.setText("No subtitle for this sound." if s else "")
|
|
for b in (self.b_apply, self.b_revert, self.b_open):
|
|
b.setEnabled(False)
|
|
return
|
|
tx = ws.texts
|
|
for i, ln in enumerate(lines):
|
|
k = subtitles.key(ln.lang, ln.speech, ln.line)
|
|
e = QLineEdit(tx.records().get(k, {}).get("applied") or ln.text)
|
|
e.setMinimumWidth(80)
|
|
e.setCursorPosition(0) # long subtitles: show their beginning
|
|
e.setToolTip("Original: %s\nRoom: %d characters" % (ln.text, ln.room))
|
|
e.textChanged.connect(self._check)
|
|
self.lay.insertRow(i, "Line %d" % (i + 1) if len(lines) > 1 else "Subtitle", e)
|
|
self.edits.append(e)
|
|
self.keys.append(k)
|
|
self.b_open.setEnabled(True)
|
|
self.b_revert.setEnabled(any(k in tx.records() for k in self.keys))
|
|
self._check()
|
|
|
|
def _check(self):
|
|
tx = self.win.ws.texts
|
|
msgs = []
|
|
ok = True
|
|
for k, e in zip(self.keys, self.edits):
|
|
try:
|
|
tx.check(k, e.text())
|
|
except ValueError as ex:
|
|
ok = False
|
|
msgs.append(str(ex))
|
|
rooms = [tx.limit(k) for k in self.keys]
|
|
self.lbl.setText(" • ".join(msgs) if msgs else "Room: %s characters (%s)" % (
|
|
" / ".join(str(r) for r in rooms), self.keys[0].split(":")[1]))
|
|
self.b_apply.setEnabled(ok)
|
|
|
|
def values(self):
|
|
return {k: e.text() for k, e in zip(self.keys, self.edits)}
|
|
|
|
def apply(self):
|
|
tx = self.win.ws.texts
|
|
changes = {k: v for k, v in self.values().items() if v != tx.original(k) or k in tx.records()}
|
|
if not changes:
|
|
return
|
|
self._run(lambda job, force: tx.apply(changes, force=force, progress=job.report), "Subtitle applied.")
|
|
|
|
def revert(self):
|
|
tx = self.win.ws.texts
|
|
self._run(lambda job, force: tx.revert(self.keys, progress=job.report), "Subtitle reverted.")
|
|
|
|
def _run(self, fn, ok_msg):
|
|
def done(_):
|
|
self.win.statusBar().showMessage(ok_msg, 6000)
|
|
self.changed.emit()
|
|
|
|
def failed(e, tb):
|
|
if isinstance(e, TextConflictError) and self.win.confirm("Changed outside", "%s\n\nOverwrite anyway?" % e):
|
|
self.win.runner.start(lambda job: fn(job, True), done, failed)
|
|
return
|
|
self.win.inform("Error", str(e))
|
|
|
|
self.win.runner.start(lambda job: fn(job, False), done, failed)
|
|
|
|
|
|
class TextPanel(QWidget):
|
|
"""kind 'subs' (subtitles) or 'menu' (menu text)."""
|
|
changed = Signal()
|
|
message = Signal(str)
|
|
go_to_sound = Signal(str) # sound key (subtitles)
|
|
|
|
def __init__(self, win, kind):
|
|
super().__init__(win)
|
|
self.win, self.kind = win, kind
|
|
self.lang = None
|
|
self.current = None # selected Row
|
|
self._build()
|
|
|
|
# --- UI ---------------------------------------------------------------------------------------
|
|
def _build(self):
|
|
v = QVBoxLayout(self)
|
|
v.setContentsMargins(6, 6, 6, 6)
|
|
top = QHBoxLayout()
|
|
self.search = QLineEdit()
|
|
self.search.setClearButtonEnabled(True)
|
|
self.search.setPlaceholderText("Search texts, names, characters…" if self.kind == "subs" else
|
|
"Search texts, files, keys…")
|
|
self.search.addAction(icons.icon("search", icons.MUTED), QLineEdit.ActionPosition.LeadingPosition)
|
|
self._timer = QTimer(self)
|
|
self._timer.setSingleShot(True)
|
|
self._timer.setInterval(150)
|
|
self._timer.timeout.connect(self._filter)
|
|
self.search.textChanged.connect(lambda _: self._timer.start())
|
|
self.lbl_lang = QLabel()
|
|
self.lbl_lang.setObjectName("Muted")
|
|
self.lbl_lang.setToolTip("Change the language with the Language list at the top of the window")
|
|
top.addWidget(self.search, 1)
|
|
top.addWidget(self.lbl_lang)
|
|
self.filters = {}
|
|
names = ("Character", "Category") if self.kind == "subs" else ("File",)
|
|
for name in names:
|
|
cb = QComboBox()
|
|
cb.setMinimumWidth(120)
|
|
cb.currentTextChanged.connect(lambda _: self._filter())
|
|
top.addWidget(QLabel(name))
|
|
top.addWidget(cb)
|
|
self.filters[name] = cb
|
|
self.cb_mod = QCheckBox("Modified only")
|
|
self.cb_mod.toggled.connect(lambda _: self._filter())
|
|
top.addWidget(self.cb_mod)
|
|
self.lbl_count = QLabel()
|
|
top.addWidget(self.lbl_count)
|
|
v.addLayout(top)
|
|
|
|
headers = ["Speech line", "#", "Character", "Category", "Room"] if self.kind == "subs" else \
|
|
["File", "Section", "Key"]
|
|
self.model = TextModel(headers, self)
|
|
self.proxy = TextFilter(self)
|
|
self.proxy.setSourceModel(self.model)
|
|
self.table = QTableView()
|
|
self.table.setModel(self.proxy)
|
|
self.busy = BusyOverlay(self.table) # while the texts of a language load (background job)
|
|
self._load_gen = 0
|
|
self._pending_key = None
|
|
self.table.setSortingEnabled(True)
|
|
self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
self.table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
|
self.table.verticalHeader().hide()
|
|
self.table.verticalHeader().setDefaultSectionSize(22)
|
|
self.table.setAlternatingRowColors(True)
|
|
self.table.setWordWrap(False)
|
|
h = self.table.horizontalHeader()
|
|
h.setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
|
|
h.setStretchLastSection(False)
|
|
widths = (330, 30, 120, 130, 50) if self.kind == "subs" else (140, 150, 220)
|
|
for c, w in enumerate(widths):
|
|
self.table.setColumnWidth(c, w)
|
|
self.table.setColumnWidth(len(widths), 560)
|
|
self.table.selectionModel().currentRowChanged.connect(lambda cur, _: self._select(cur))
|
|
|
|
# editor
|
|
ed = QWidget()
|
|
f = QFormLayout(ed)
|
|
self.title = QLabel("No text selected")
|
|
self.title.setObjectName("EditorTitle")
|
|
self.lbl_orig = QLabel()
|
|
self.lbl_orig.setWordWrap(True)
|
|
self.lbl_orig.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
|
self.edit = QLineEdit()
|
|
self.edit.textChanged.connect(self._edited)
|
|
self.edit.returnPressed.connect(self.apply_current)
|
|
self.lbl_info = QLabel()
|
|
row = QHBoxLayout()
|
|
self.b_apply = QPushButton(icons.icon("apply"), "Apply")
|
|
self.b_revert = QPushButton(icons.icon("revert"), "Revert")
|
|
self.b_replace = QPushButton(icons.icon("search"), "Find and replace…")
|
|
self.b_revert_all = QPushButton(icons.icon("revert"), "Revert all texts here")
|
|
self.b_apply.clicked.connect(self.apply_current)
|
|
self.b_revert.clicked.connect(self.revert_current)
|
|
self.b_replace.clicked.connect(self.find_replace)
|
|
self.b_revert_all.clicked.connect(self.revert_all)
|
|
for b in (self.b_apply, self.b_revert):
|
|
row.addWidget(b)
|
|
if self.kind == "subs":
|
|
self.b_sound = QPushButton(icons.icon("play"), "Go to the sound")
|
|
self.b_sound.clicked.connect(lambda: self.current and self.current.sound and
|
|
self.go_to_sound.emit(self.current.sound))
|
|
row.addWidget(self.b_sound)
|
|
row.addStretch(1)
|
|
row.addWidget(self.b_replace)
|
|
row.addWidget(self.b_revert_all)
|
|
f.addRow(self.title)
|
|
f.addRow("Original", self.lbl_orig)
|
|
f.addRow("Text", self.edit)
|
|
f.addRow("", self.lbl_info)
|
|
f.addRow(row)
|
|
split = QSplitter(Qt.Orientation.Vertical)
|
|
split.addWidget(self.table)
|
|
split.addWidget(ed)
|
|
split.setStretchFactor(0, 1)
|
|
split.setStretchFactor(1, 0)
|
|
v.addWidget(split, 1)
|
|
self._select(None)
|
|
|
|
# --- data ---------------------------------------------------------------------------------------
|
|
def ws(self):
|
|
return self.win.ws
|
|
|
|
def languages(self):
|
|
tx = self.ws().texts
|
|
return tx.subtitle_languages() if self.kind == "subs" else tx.menu_languages()
|
|
|
|
def load(self, lang=None):
|
|
"""(Re)load the rows of a language (default: the window's language)."""
|
|
ws = self.ws()
|
|
if not ws:
|
|
return
|
|
lang = lang or self.win.language
|
|
if lang not in self.languages():
|
|
lang = None
|
|
self.lang = lang
|
|
self.lbl_lang.setText("Language: %s" % lang if lang else "No %s in this language" % (
|
|
"subtitles" if self.kind == "subs" else "menu text"))
|
|
self._load_gen += 1
|
|
gen = self._load_gen
|
|
if not lang:
|
|
self.busy.stop()
|
|
self.model.reset([], {})
|
|
return
|
|
self.busy.start("Loading subtitles…" if self.kind == "subs" else "Loading menu text…")
|
|
|
|
def done(rows):
|
|
if gen == self._load_gen: # a newer load replaces this one
|
|
self.busy.stop()
|
|
self._show_rows(rows)
|
|
|
|
def failed(e, tb):
|
|
if gen == self._load_gen:
|
|
self.busy.stop()
|
|
self.message.emit("Could not load the texts: %s" % e)
|
|
|
|
self.win.runner.start(lambda job: self._rows(ws, lang), done, failed)
|
|
|
|
def _rows(self, ws, lang):
|
|
"""The table rows of a language (runs as a background job)."""
|
|
tx = ws.texts
|
|
rows = []
|
|
if self.kind == "subs":
|
|
by_key = ws.catalog.by_key
|
|
for s in tx.subtitles(lang):
|
|
snd = by_key.get(s.sounds[0]) if s.sounds else None # None: the line's archive is not loaded
|
|
who = (snd.character or snd.characters_inferred) if snd else ""
|
|
cat = snd.category if snd else ""
|
|
rows.append(Row(subtitles.key(lang, s.speech, s.line),
|
|
[s.speech, s.line + 1, who, cat, s.room], s.text, s.sounds[0] if s.sounds else None))
|
|
else:
|
|
for e in tx.menu(lang):
|
|
rows.append(Row("MENU:" + e.key, [e.file, e.section, e.name + ("#%d" % e.occurrence if e.occurrence > 1 else "")],
|
|
self._menu_original(e)))
|
|
return rows
|
|
|
|
def _show_rows(self, rows):
|
|
self.model.reset(rows, self._modified())
|
|
for i, cb in enumerate(self.filters.values()):
|
|
col = (2 + i) if self.kind == "subs" else 0
|
|
cur = cb.currentText()
|
|
cb.blockSignals(True)
|
|
cb.clear()
|
|
cb.addItems([ANY] + sorted({str(r.cols[col]) for r in rows if r.cols[col]}))
|
|
cb.setCurrentText(cur if cur else ANY)
|
|
cb.blockSignals(False)
|
|
self._filter()
|
|
if self._pending_key: # asked for while the rows were loading
|
|
key, self._pending_key = self._pending_key, None
|
|
self.select_key(key)
|
|
|
|
def _menu_original(self, e):
|
|
r = self.ws().texts.records().get("MENU:" + e.key)
|
|
return r["orig"] if r else e.value
|
|
|
|
def _modified(self):
|
|
return {k: r["applied"] for k, r in self.ws().texts.records().items()
|
|
if r.get("applied") is not None and k.split(":")[1] == self.lang and
|
|
k.startswith("SUB:" if self.kind == "subs" else "MENU:")}
|
|
|
|
def refresh(self):
|
|
self.model.set_modified(self._modified()) # the view refilters/re-sorts itself
|
|
self._update_count()
|
|
if self.current:
|
|
self._show(self.current)
|
|
|
|
def _filter(self):
|
|
self.proxy.words = [w for w in self.search.text().lower().split() if w]
|
|
self.proxy.only_modified = self.cb_mod.isChecked()
|
|
cols = (2, 3) if self.kind == "subs" else (0,)
|
|
self.proxy.col_filters = {c: cb.currentText() for c, cb in zip(cols, self.filters.values())}
|
|
self.proxy.refilter()
|
|
self._update_count()
|
|
|
|
def _update_count(self):
|
|
self.lbl_count.setText("%d / %d" % (self.proxy.rowCount(), self.model.rowCount()))
|
|
|
|
def visible_rows(self):
|
|
return [self.proxy.row_at(self.proxy.index(i, 0)) for i in range(self.proxy.rowCount())]
|
|
|
|
def select_key(self, key):
|
|
if self.busy.busy: # rows still loading: select when they arrive
|
|
self._pending_key = key
|
|
return True
|
|
for i in range(self.proxy.rowCount()):
|
|
if self.proxy.row_at(self.proxy.index(i, 0)).key == key:
|
|
self.table.selectRow(i)
|
|
self.table.scrollTo(self.proxy.index(i, 0))
|
|
return True
|
|
return False
|
|
|
|
# --- editing ------------------------------------------------------------------------------------
|
|
def _select(self, index):
|
|
r = self.proxy.row_at(index) if index is not None and index.isValid() else None
|
|
self.current = r
|
|
self._show(r)
|
|
|
|
def _show(self, r):
|
|
enabled = r is not None
|
|
for w in (self.edit, self.b_apply, self.b_revert):
|
|
w.setEnabled(enabled)
|
|
if self.kind == "subs":
|
|
self.b_sound.setEnabled(bool(r and r.sound))
|
|
if not r:
|
|
self.title.setText("No text selected")
|
|
self.lbl_orig.setText("")
|
|
self.edit.setText("")
|
|
self.lbl_info.setText("")
|
|
return
|
|
tx = self.ws().texts
|
|
self.title.setText(r.key)
|
|
self.lbl_orig.setText(r.text)
|
|
try:
|
|
cur = tx.current(r.key)
|
|
st = tx.state(r.key, cur)
|
|
except Exception as e: # noqa: BLE001 - unreadable file: show it, do not crash
|
|
cur, st = r.text, "error: %s" % e
|
|
self.edit.blockSignals(True)
|
|
self.edit.setText(cur)
|
|
self.edit.blockSignals(False)
|
|
self._state = st
|
|
self.b_revert.setEnabled(r.key in tx.records())
|
|
self._edited()
|
|
|
|
def _edited(self):
|
|
r = self.current
|
|
if not r:
|
|
return
|
|
tx = self.ws().texts
|
|
lim = tx.limit(r.key)
|
|
n = len(self.edit.text())
|
|
info = []
|
|
if lim is not None:
|
|
info.append("%d / %d characters" % (n, lim))
|
|
st = STATE_TEXT.get(self._state, self._state)
|
|
if st:
|
|
info.append("state: " + st)
|
|
try:
|
|
tx.check(r.key, self.edit.text())
|
|
ok = True
|
|
except ValueError as e:
|
|
info.append(str(e))
|
|
ok = False
|
|
self.lbl_info.setText(" • ".join(info))
|
|
self.b_apply.setEnabled(ok)
|
|
|
|
def _run(self, busy, fn, ok_msg):
|
|
def done(_):
|
|
self.message.emit(ok_msg)
|
|
self.refresh()
|
|
self.changed.emit()
|
|
|
|
def failed(e, tb):
|
|
if isinstance(e, TextConflictError) and self.win.confirm(
|
|
"Changed outside", "%s\n\nOverwrite anyway?" % e):
|
|
self.win.runner.start(lambda job: fn(job, True), done, failed)
|
|
return
|
|
self.win.inform("Error", str(e))
|
|
|
|
self.message.emit(busy)
|
|
self.win.runner.start(lambda job: fn(job, False), done, failed)
|
|
|
|
def apply_current(self):
|
|
r = self.current
|
|
if not r or not self.b_apply.isEnabled():
|
|
return
|
|
text = self.edit.text()
|
|
self._run("Writing…", lambda job, force: self.ws().texts.apply({r.key: text}, force=force, progress=job.report),
|
|
"Applied %s" % r.key)
|
|
|
|
def revert_current(self):
|
|
r = self.current
|
|
if r:
|
|
self._run("Reverting…", lambda job, force: self.ws().texts.revert([r.key], progress=job.report),
|
|
"Reverted %s" % r.key)
|
|
|
|
def revert_all(self):
|
|
keys = list(self._modified())
|
|
if not keys:
|
|
self.win.inform("Revert", "No %s text is modified." % self.lang)
|
|
return
|
|
if not self.win.confirm("Revert", "Put the original back for %d text(s)?" % len(keys)):
|
|
return
|
|
self._run("Reverting…", lambda job, force: self.ws().texts.revert(keys, progress=job.report),
|
|
"Reverted %d text(s)." % len(keys))
|
|
|
|
def find_replace(self, find=None, repl=None, case=False):
|
|
rows = self.visible_rows()
|
|
if find is None:
|
|
dlg = FindReplaceDialog(len(rows), self)
|
|
if dlg.exec() != QDialog.DialogCode.Accepted or not dlg.find.text():
|
|
return
|
|
find, repl, case = dlg.find.text(), dlg.repl.text(), dlg.case.isChecked()
|
|
tx = self.ws().texts
|
|
changes, skipped = {}, []
|
|
for r in rows:
|
|
old = self.model.text_of(r)
|
|
new = replace_in(old, find, repl, case)
|
|
if new == old:
|
|
continue
|
|
try:
|
|
tx.check(r.key, new)
|
|
changes[r.key] = new
|
|
except ValueError as e:
|
|
skipped.append("%s: %s" % (r.key, e))
|
|
if not changes:
|
|
self.win.inform("Find and replace", "Nothing to replace." + ("\n" + "\n".join(skipped[:10]) if skipped else ""))
|
|
return
|
|
if skipped and not self.win.confirm("Find and replace", "%d text(s) cannot take the replacement and are "
|
|
"skipped:\n%s\n\nReplace in the other %d?" % (
|
|
len(skipped), "\n".join(skipped[:10]), len(changes))):
|
|
return
|
|
self._run("Replacing in %d text(s)…" % len(changes),
|
|
lambda job, force: tx.apply(changes, force=force, progress=job.report),
|
|
"Replaced in %d text(s)." % len(changes))
|
|
return len(changes)
|