Version 3.2.0: fast lists with loading spinners, names from the game files, INT docs and screenshots, cleanup
This commit is contained in:
@@ -19,7 +19,7 @@ from .audioio import Player, find_device, output_devices
|
||||
from .dialogs import ExportModDialog, ImportModDialog, SettingsDialog, pick_game_dir
|
||||
from .editor import EditorPanel
|
||||
from .models import C_FAV, C_NAME, COLS, SoundFilter, SoundModel
|
||||
from .spinner import Spinner
|
||||
from .spinner import BusyOverlay, Spinner
|
||||
from .textpanel import TextPanel
|
||||
from .userdata import UserData
|
||||
from .workers import Runner
|
||||
@@ -181,6 +181,7 @@ class MainWindow(QMainWindow):
|
||||
self.proxy.setSourceModel(self.model)
|
||||
self.table = QTableView()
|
||||
self.table.setModel(self.proxy)
|
||||
self.table_busy = BusyOverlay(self.table) # while the sound list loads (start, language change)
|
||||
self.table.setSortingEnabled(True)
|
||||
self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.table.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
@@ -294,6 +295,7 @@ class MainWindow(QMainWindow):
|
||||
self.loading = True
|
||||
self.statusBar().showMessage("Loading sound list…")
|
||||
self.spinner.show()
|
||||
self.table_busy.start("Loading sounds…")
|
||||
store = self.settings.get("store_root")
|
||||
full = self.settings.get("full_backup", True)
|
||||
|
||||
@@ -305,6 +307,7 @@ class MainWindow(QMainWindow):
|
||||
def done(ws):
|
||||
self.loading = False
|
||||
self.spinner.hide()
|
||||
self.table_busy.stop()
|
||||
self.ws = ws
|
||||
self.settings["game_dir"] = game.root
|
||||
self.save_settings()
|
||||
@@ -327,6 +330,7 @@ class MainWindow(QMainWindow):
|
||||
def failed(e, tb):
|
||||
self.loading = False
|
||||
self.spinner.hide()
|
||||
self.table_busy.stop()
|
||||
self.show_no_game("Could not load the game audio: %s" % e)
|
||||
|
||||
self.runner.start(work, done, failed, lambda m: self.statusBar().showMessage(m))
|
||||
@@ -512,8 +516,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
# --- changes ----------------------------------------------------------------------------------
|
||||
def _after_change(self, key=None):
|
||||
self.model.refresh_keys(modified=self.ws.modified_keys())
|
||||
self.proxy.refilter()
|
||||
self.model.refresh_keys(modified=self.ws.modified_keys()) # the view refilters/re-sorts itself
|
||||
self._update_count()
|
||||
|
||||
def revert_sounds(self, sounds):
|
||||
|
||||
211
app/models.py
211
app/models.py
@@ -1,5 +1,5 @@
|
||||
"""Table model for the sound browser + a combined filter (search text and facet filters)."""
|
||||
from PySide6.QtCore import QAbstractTableModel, QModelIndex, QSortFilterProxyModel, Qt
|
||||
from PySide6.QtCore import QAbstractProxyModel, QAbstractTableModel, QModelIndex, Qt
|
||||
from PySide6.QtGui import QFont, QPalette
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
@@ -40,6 +40,44 @@ class SoundModel(QAbstractTableModel):
|
||||
if self.sounds:
|
||||
self.dataChanged.emit(self.index(0, 0), self.index(len(self.sounds) - 1, len(COLS) - 1))
|
||||
|
||||
def sort_key(self, row, c):
|
||||
"""Value a column sorts by (plain Python values: the view sorts all rows in one Python sort)."""
|
||||
s, ud = self.sounds[row], self.userdata
|
||||
if c == C_FAV:
|
||||
return 0 if ud and ud.favorite(s.key) else 1
|
||||
if c == C_LEN:
|
||||
return s.seconds
|
||||
if c == C_SIZE:
|
||||
return 1e12 if s.is_pack_media else s.size
|
||||
if c == C_FMT:
|
||||
return (s.rate, s.channels)
|
||||
if c == C_STATUS:
|
||||
return 0 if s.key in self.modified else 1
|
||||
return self.display(s, c).lower()
|
||||
|
||||
def display(self, s, c):
|
||||
ud = self.userdata
|
||||
if c == C_NAME:
|
||||
return s.name
|
||||
if c == C_CAT:
|
||||
return s.category
|
||||
if c == C_CHAR:
|
||||
return s.character or (("~ " + s.characters_inferred) if s.characters_inferred else "")
|
||||
if c == C_LEN:
|
||||
return "%.2f s" % s.seconds
|
||||
if c == C_ARCH:
|
||||
return s.archive
|
||||
if c == C_FMT:
|
||||
return "%d Hz %s" % (s.rate, {1: "mono", 2: "stereo"}.get(s.channels, "%d ch" % s.channels))
|
||||
if c == C_SIZE:
|
||||
return "any" if s.is_pack_media else "%.1f KB" % (s.size / 1024.0)
|
||||
if c == C_STATUS:
|
||||
return "Modified" if s.key in self.modified else ""
|
||||
if c == C_NOTES and ud:
|
||||
t = ud.tags(s.key)
|
||||
return " ".join("#" + x for x in t) + (" " if t else "") + ud.note(s.key)
|
||||
return ""
|
||||
|
||||
def rowCount(self, parent=QModelIndex()):
|
||||
return 0 if parent.isValid() else len(self.sounds)
|
||||
|
||||
@@ -66,41 +104,9 @@ class SoundModel(QAbstractTableModel):
|
||||
if role == SOUND_ROLE:
|
||||
return s
|
||||
if role == Qt.ItemDataRole.DisplayRole:
|
||||
if c == C_FAV:
|
||||
return ""
|
||||
if c == C_NAME:
|
||||
return s.name
|
||||
if c == C_CAT:
|
||||
return s.category
|
||||
if c == C_CHAR:
|
||||
return s.character or (("~ " + s.characters_inferred) if s.characters_inferred else "")
|
||||
if c == C_LEN:
|
||||
return "%.2f s" % s.seconds
|
||||
if c == C_ARCH:
|
||||
return s.archive
|
||||
if c == C_FMT:
|
||||
return "%d Hz %s" % (s.rate, {1: "mono", 2: "stereo"}.get(s.channels, "%d ch" % s.channels))
|
||||
if c == C_SIZE:
|
||||
return "any" if s.is_pack_media else "%.1f KB" % (s.size / 1024.0)
|
||||
if c == C_STATUS:
|
||||
return "Modified" if s.key in self.modified else ""
|
||||
if c == C_NOTES:
|
||||
if not ud:
|
||||
return ""
|
||||
t = ud.tags(s.key)
|
||||
return " ".join("#" + x for x in t) + (" " if t else "") + ud.note(s.key)
|
||||
return self.display(s, c)
|
||||
if role == SORT_ROLE:
|
||||
if c == C_FAV:
|
||||
return 0 if ud and ud.favorite(s.key) else 1
|
||||
if c == C_LEN:
|
||||
return s.seconds
|
||||
if c == C_SIZE:
|
||||
return 1e12 if s.is_pack_media else s.size
|
||||
if c == C_FMT:
|
||||
return (s.rate, s.channels)
|
||||
if c == C_STATUS:
|
||||
return 0 if s.key in self.modified else 1
|
||||
return (self.data(index, Qt.ItemDataRole.DisplayRole) or "").lower()
|
||||
return self.sort_key(index.row(), c)
|
||||
if role == Qt.ItemDataRole.DecorationRole and c == C_FAV:
|
||||
fav = bool(ud and ud.favorite(s.key))
|
||||
return icons.icon("star" if fav else "star-off", icons.ACCENT if fav else "#4a4a4a")
|
||||
@@ -122,9 +128,122 @@ class SoundModel(QAbstractTableModel):
|
||||
return None
|
||||
|
||||
|
||||
class SoundFilter(QSortFilterProxyModel):
|
||||
class RowView(QAbstractProxyModel):
|
||||
"""The rows a table shows: the source rows a subclass accepts, in the chosen order. Computed in Python in
|
||||
one pass (filter, then one sort over cached keys): ~10 ms for 21,000 sounds, where QSortFilterProxyModel
|
||||
needed up to ~1.5 s per sort or widened filter (each comparison called back into Python). Selection and the
|
||||
current row survive every change (persistent indexes are moved, as Qt's own proxies do).
|
||||
Subclasses: source_len(), accepts(row), sort_key(row, column)."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.sort_column, self.sort_order = -1, Qt.SortOrder.AscendingOrder
|
||||
self.rows = [] # proxy row -> source row
|
||||
self.pos = {} # source row -> proxy row
|
||||
self._keys = {} # column -> [sort key per source row] (until the source changes)
|
||||
|
||||
def source_len(self):
|
||||
return self.sourceModel().rowCount()
|
||||
|
||||
def accepts(self, row):
|
||||
return True
|
||||
|
||||
def sort_key(self, row, column):
|
||||
v = self.sourceModel().index(row, column).data()
|
||||
return v.lower() if isinstance(v, str) else v
|
||||
|
||||
# --- source model --------------------------------------------------------------------------------
|
||||
def setSourceModel(self, model):
|
||||
"""One source model for the view's lifetime."""
|
||||
self.beginResetModel()
|
||||
super().setSourceModel(model)
|
||||
model.modelAboutToBeReset.connect(self.beginResetModel)
|
||||
model.modelReset.connect(self._source_reset)
|
||||
model.dataChanged.connect(self._source_changed)
|
||||
self._recompute_all()
|
||||
self.endResetModel()
|
||||
|
||||
def _recompute_all(self):
|
||||
self._keys = {}
|
||||
self.rows = self._compute()
|
||||
self.pos = {r: i for i, r in enumerate(self.rows)}
|
||||
|
||||
def _source_reset(self):
|
||||
self._recompute_all()
|
||||
self.endResetModel()
|
||||
|
||||
def _source_changed(self, top_left, bottom_right, roles=()):
|
||||
"""Values changed (status, notes, favorites, texts): sort keys and filters may depend on them."""
|
||||
self._keys = {}
|
||||
self.refilter()
|
||||
if self.rows:
|
||||
self.dataChanged.emit(self.index(0, 0), self.index(len(self.rows) - 1, self.columnCount() - 1))
|
||||
|
||||
# --- filter and order ----------------------------------------------------------------------------
|
||||
def refilter(self):
|
||||
"""Recompute the rows; the view keeps its selection and current row (layout change)."""
|
||||
rows = self._compute()
|
||||
if rows == self.rows:
|
||||
return
|
||||
self.layoutAboutToBeChanged.emit()
|
||||
old = self.persistentIndexList()
|
||||
src = [(self.rows[i.row()], i.column()) if i.isValid() and i.row() < len(self.rows) else None for i in old]
|
||||
self.rows = rows
|
||||
self.pos = {r: i for i, r in enumerate(rows)}
|
||||
new = [self.index(self.pos[s[0]], s[1]) if s and s[0] in self.pos else QModelIndex() for s in src]
|
||||
self.changePersistentIndexList(old, new)
|
||||
self.layoutChanged.emit()
|
||||
|
||||
def sort(self, column, order=Qt.SortOrder.AscendingOrder):
|
||||
self.sort_column, self.sort_order = column, order
|
||||
self.refilter()
|
||||
|
||||
def _compute(self):
|
||||
if self.sourceModel() is None:
|
||||
return []
|
||||
n = self.source_len()
|
||||
rows = [r for r in range(n) if self.accepts(r)]
|
||||
c = self.sort_column
|
||||
if c >= 0:
|
||||
if c not in self._keys:
|
||||
self._keys[c] = [self.sort_key(r, c) for r in range(n)]
|
||||
rows.sort(key=self._keys[c].__getitem__, reverse=self.sort_order == Qt.SortOrder.DescendingOrder)
|
||||
return rows
|
||||
|
||||
# --- QAbstractProxyModel -------------------------------------------------------------------------
|
||||
def index(self, row, column, parent=QModelIndex()):
|
||||
if parent.isValid() or not (0 <= row < len(self.rows)) or not (0 <= column < self.columnCount()):
|
||||
return QModelIndex()
|
||||
return self.createIndex(row, column)
|
||||
|
||||
def parent(self, index=QModelIndex()):
|
||||
return QModelIndex()
|
||||
|
||||
def rowCount(self, parent=QModelIndex()):
|
||||
return 0 if parent.isValid() else len(self.rows)
|
||||
|
||||
def columnCount(self, parent=QModelIndex()):
|
||||
m = self.sourceModel()
|
||||
return 0 if parent.isValid() or m is None else m.columnCount()
|
||||
|
||||
def mapToSource(self, proxy_index):
|
||||
if not proxy_index.isValid() or proxy_index.row() >= len(self.rows):
|
||||
return QModelIndex()
|
||||
return self.sourceModel().index(self.rows[proxy_index.row()], proxy_index.column())
|
||||
|
||||
def mapFromSource(self, source_index):
|
||||
if not source_index.isValid() or source_index.row() not in self.pos:
|
||||
return QModelIndex()
|
||||
return self.index(self.pos[source_index.row()], source_index.column())
|
||||
|
||||
def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
|
||||
return self.sourceModel().headerData(section, orientation, role)
|
||||
|
||||
|
||||
class SoundFilter(RowView):
|
||||
"""The sound browser's rows: search words and facet filters, sorted by any column."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
self.words = []
|
||||
self.group = None
|
||||
self.category = None
|
||||
@@ -134,15 +253,7 @@ class SoundFilter(QSortFilterProxyModel):
|
||||
self.modified_only = False
|
||||
self.favorites_only = False
|
||||
self.tag = None
|
||||
self.setSortRole(SORT_ROLE)
|
||||
self.setDynamicSortFilter(False)
|
||||
|
||||
def refilter(self):
|
||||
if hasattr(self, "beginFilterChange"): # Qt >= 6.10
|
||||
self.beginFilterChange()
|
||||
self.endFilterChange(QSortFilterProxyModel.Direction.Rows)
|
||||
else:
|
||||
self.invalidateRowsFilter()
|
||||
super().__init__(parent)
|
||||
|
||||
def set(self, **kw):
|
||||
for k, v in kw.items():
|
||||
@@ -153,7 +264,13 @@ class SoundFilter(QSortFilterProxyModel):
|
||||
self.words = [w for w in text.lower().replace("_", " ").split() if w]
|
||||
self.refilter()
|
||||
|
||||
def filterAcceptsRow(self, row, parent):
|
||||
def source_len(self):
|
||||
return len(self.sourceModel().sounds)
|
||||
|
||||
def sort_key(self, row, column):
|
||||
return self.sourceModel().sort_key(row, column)
|
||||
|
||||
def accepts(self, row):
|
||||
m = self.sourceModel()
|
||||
s = m.sounds[row]
|
||||
if self.group and s.group != self.group:
|
||||
@@ -181,4 +298,4 @@ class SoundFilter(QSortFilterProxyModel):
|
||||
return True
|
||||
|
||||
def sound_at(self, proxy_index):
|
||||
return self.sourceModel().sounds[self.mapToSource(proxy_index).row()]
|
||||
return self.sourceModel().sounds[self.rows[proxy_index.row()]]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Small activity spinner: a rotating accent-coloured arc. Runs only while visible."""
|
||||
from PySide6.QtCore import QRectF, QTimer, Qt
|
||||
from PySide6.QtGui import QColor, QPainter, QPen
|
||||
from PySide6.QtCore import QEvent, QRectF, QTimer, Qt
|
||||
from PySide6.QtGui import QColor, QPainter, QPalette, QPen
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from . import icons
|
||||
@@ -51,3 +51,68 @@ class Spinner(QWidget):
|
||||
def paintEvent(self, _):
|
||||
p = QPainter(self)
|
||||
paint_arc(p, QRectF(self.rect()), self.angle)
|
||||
|
||||
|
||||
class BusyOverlay(QWidget):
|
||||
"""A list being (re)loaded: a light veil over the widget with the spinner and a message in its centre, the
|
||||
same arc as the waveform's loading state. Covers the target and follows its size; blocks clicks while on."""
|
||||
|
||||
def __init__(self, target):
|
||||
super().__init__(target)
|
||||
self.target = target
|
||||
self.message = ""
|
||||
self.angle = 0
|
||||
self.timer = QTimer(self)
|
||||
self.timer.setInterval(STEP_MS)
|
||||
self.timer.timeout.connect(self._step)
|
||||
target.installEventFilter(self)
|
||||
self.hide()
|
||||
|
||||
def start(self, message):
|
||||
self.message = message
|
||||
self.setGeometry(self.target.rect())
|
||||
self.raise_()
|
||||
self.show()
|
||||
self.timer.start()
|
||||
self.update()
|
||||
|
||||
def stop(self):
|
||||
self.timer.stop()
|
||||
self.hide()
|
||||
|
||||
@property
|
||||
def busy(self):
|
||||
return not self.isHidden() # on, whether or not the window is on screen yet
|
||||
|
||||
def _step(self):
|
||||
self.angle = (self.angle + STEP_DEG) % 360
|
||||
if self.isVisible():
|
||||
self.update()
|
||||
|
||||
def eventFilter(self, obj, e):
|
||||
if obj is self.target and e.type() == QEvent.Type.Resize:
|
||||
self.setGeometry(self.target.rect())
|
||||
return False
|
||||
|
||||
def paintEvent(self, _):
|
||||
p = QPainter(self)
|
||||
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
pal = self.palette()
|
||||
veil = QColor(pal.color(QPalette.ColorRole.Base))
|
||||
veil.setAlpha(210)
|
||||
p.fillRect(self.rect(), veil)
|
||||
fm = p.fontMetrics()
|
||||
d, gap, pad = 20, 12, 16
|
||||
tw = fm.horizontalAdvance(self.message)
|
||||
w, h = pad + d + gap + tw + pad, max(d, fm.height()) + 2 * pad - 8
|
||||
box = QRectF((self.width() - w) / 2, (self.height() - h) / 2, w, h)
|
||||
border = QColor(pal.color(QPalette.ColorRole.Text))
|
||||
border.setAlpha(40)
|
||||
p.setPen(QPen(border, 1))
|
||||
p.setBrush(pal.color(QPalette.ColorRole.Window))
|
||||
p.drawRoundedRect(box, 8, 8)
|
||||
y = box.center().y()
|
||||
paint_arc(p, QRectF(box.left() + pad, y - d / 2, d, d), self.angle)
|
||||
p.setPen(pal.color(QPalette.ColorRole.Text))
|
||||
p.drawText(QRectF(box.left() + pad + d + gap, y - fm.height() / 2, tw + 2, fm.height()),
|
||||
Qt.AlignmentFlag.AlignVCenter | Qt.AlignmentFlag.AlignLeft, self.message)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user