Initial commit
This commit is contained in:
184
app/models.py
Normal file
184
app/models.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""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()]
|
||||
Reference in New Issue
Block a user