"""Table model for the sound browser + a combined filter (search text and facet filters).""" from PySide6.QtCore import QAbstractTableModel, QModelIndex, QSortFilterProxyModel, Qt from PySide6.QtGui import QFont, QPalette from PySide6.QtWidgets import QApplication from . import icons COLS = ["", "Name", "Category", "Character", "Length", "Archive", "Format", "Max size", "Status", "Notes / tags"] C_FAV, C_NAME, C_CAT, C_CHAR, C_LEN, C_ARCH, C_FMT, C_SIZE, C_STATUS, C_NOTES = range(len(COLS)) SORT_ROLE = Qt.ItemDataRole.UserRole + 1 SOUND_ROLE = Qt.ItemDataRole.UserRole + 2 class SoundModel(QAbstractTableModel): def __init__(self, parent=None): super().__init__(parent) self.sounds = [] self.modified = set() self.userdata = None self.hay = [] def reset(self, sounds, modified, userdata): self.beginResetModel() self.sounds, self.modified, self.userdata = list(sounds), set(modified), userdata self._build_hay() self.endResetModel() def _build_hay(self): ud = self.userdata self.hay = ["\n".join((s.name, s.category, s.character, s.characters_inferred, ud.note(s.key) if ud else "", " ".join(ud.tags(s.key)) if ud else "")).lower() for s in self.sounds] self.ids = [s.target.replace(":", " ").replace("/", " ").lower() for s in self.sounds] def refresh_keys(self, keys=None, modified=None): """Update status / notes of some rows (all if keys is None).""" if modified is not None: self.modified = set(modified) self._build_hay() if self.sounds: self.dataChanged.emit(self.index(0, 0), self.index(len(self.sounds) - 1, len(COLS) - 1)) def rowCount(self, parent=QModelIndex()): return 0 if parent.isValid() else len(self.sounds) def columnCount(self, parent=QModelIndex()): return len(COLS) def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole): if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole: return COLS[section] if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DecorationRole and section == C_FAV: return icons.icon("star", icons.ACCENT) if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.ToolTipRole: return {C_FAV: "Favorite", C_CHAR: "Verified speaker; ~grey = characters mentioned by the name", C_SIZE: "Largest replacement that fits (speech/effects keep the original's size)", C_STATUS: "Modified = contains your replacement"}.get(section) return None def data(self, index, role=Qt.ItemDataRole.DisplayRole): if not index.isValid(): return None s = self.sounds[index.row()] c = index.column() ud = self.userdata 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) 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() 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") if role == Qt.ItemDataRole.DecorationRole and c == C_STATUS and s.key in self.modified: return icons.icon("apply", icons.ACCENT) if role == Qt.ItemDataRole.ForegroundRole and c == C_CHAR and not s.character: return QApplication.palette().color(QPalette.ColorRole.PlaceholderText) if role == Qt.ItemDataRole.FontRole and s.key in self.modified and c in (C_NAME, C_STATUS): f = QFont() f.setBold(True) return f if role == Qt.ItemDataRole.ToolTipRole and c in (C_NAME, C_CHAR): tip = "%s\n%s %s" % (s.name, s.key, s.group + " / " + s.category) if s.character: tip += "\nSpeaker: " + s.character if s.characters_inferred: tip += "\nMentioned in the name: " + s.characters_inferred return tip return None class SoundFilter(QSortFilterProxyModel): def __init__(self, parent=None): super().__init__(parent) self.words = [] self.group = None self.category = None self.character = None self.include_inferred = False self.archive = None 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() def set(self, **kw): for k, v in kw.items(): setattr(self, k, v) self.refilter() def set_text(self, text): self.words = [w for w in text.lower().replace("_", " ").split() if w] self.refilter() def filterAcceptsRow(self, row, parent): m = self.sourceModel() s = m.sounds[row] if self.group and s.group != self.group: return False if self.category and s.category != self.category: return False if self.archive and s.archive != self.archive: return False if self.character: c = self.character.lower() if c not in s.character.lower() and not (self.include_inferred and c in s.characters_inferred.lower()): return False if self.modified_only and s.key not in m.modified: return False ud = m.userdata if self.favorites_only and not (ud and ud.favorite(s.key)): return False if self.tag and not (ud and self.tag in ud.tags(s.key)): return False if self.words: h = m.hay[row] for w in self.words: if w not in h and not (len(w) >= 6 and w in m.ids[row]): # IDs like b49cdf48 also work return False return True def sound_at(self, proxy_index): return self.sourceModel().sounds[self.mapToSource(proxy_index).row()]