Version 3.2.0: fast lists with loading spinners, names from the game files, INT docs and screenshots, cleanup

This commit is contained in:
2026-10-10 14:58:17 +02:00
parent fbde809370
commit 1586abb8e9
40 changed files with 449 additions and 19279 deletions

View File

@@ -4,7 +4,7 @@ One TextPanel per section: filters + table on top, the editor of the selected te
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, QSortFilterProxyModel, Qt, QTimer, Signal
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)
@@ -12,6 +12,8 @@ from PySide6.QtWidgets import (QAbstractItemView, QCheckBox, QComboBox, QDialog,
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"}
@@ -74,20 +76,24 @@ class TextModel(QAbstractTableModel):
return None
class TextFilter(QSortFilterProxyModel):
class TextFilter(RowView):
"""The text table's rows: search words, column filters, modified only; sorted case-insensitively."""
def __init__(self, parent=None):
super().__init__(parent)
self.words, self.col_filters, self.only_modified = [], {}, False
self.setSortCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
super().__init__(parent)
def refilter(self):
if hasattr(self, "beginFilterChange"):
self.beginFilterChange()
self.endFilterChange(QSortFilterProxyModel.Direction.Rows)
else:
self.invalidateFilter()
def source_len(self):
return len(self.sourceModel().rows)
def filterAcceptsRow(self, row, parent):
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:
@@ -101,7 +107,7 @@ class TextFilter(QSortFilterProxyModel):
return True
def row_at(self, index):
return self.sourceModel().rows[self.mapToSource(index).row()]
return self.sourceModel().rows[self.rows[index.row()]]
class FindReplaceDialog(QDialog):
@@ -288,6 +294,9 @@ class TextPanel(QWidget):
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)
@@ -367,9 +376,28 @@ class TextPanel(QWidget):
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":
@@ -384,6 +412,9 @@ class TextPanel(QWidget):
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
@@ -394,6 +425,9 @@ class TextPanel(QWidget):
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)
@@ -405,8 +439,7 @@ class TextPanel(QWidget):
k.startswith("SUB:" if self.kind == "subs" else "MENU:")}
def refresh(self):
self.model.set_modified(self._modified())
self.proxy.refilter()
self.model.set_modified(self._modified()) # the view refilters/re-sorts itself
self._update_count()
if self.current:
self._show(self.current)
@@ -426,6 +459,9 @@ class TextPanel(QWidget):
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)