Subtitles and menu text sections in the editor
This commit is contained in:
@@ -1,22 +1,26 @@
|
||||
"""Main window: filters + sound table (browser) on top, single-sound editor below."""
|
||||
"""Main window. Tabs: Sounds (filters + sound table on top, single-sound editor below, with the sound's
|
||||
subtitle), Subtitles and Menu text (app/textpanel.py)."""
|
||||
import os
|
||||
|
||||
from PySide6.QtCore import QByteArray, QEvent, QItemSelectionModel, QSize, Qt, QTimer
|
||||
from PySide6.QtGui import QFont, QKeySequence, QShortcut
|
||||
from PySide6.QtWidgets import (QAbstractItemView, QCheckBox, QComboBox, QDialog, QFileDialog, QFormLayout,
|
||||
QHBoxLayout, QHeaderView, QLabel, QLineEdit, QMainWindow, QMenu, QMessageBox,
|
||||
QPushButton, QScrollArea, QSplitter, QTableView, QToolBar, QVBoxLayout, QWidget)
|
||||
QPushButton, QScrollArea, QSplitter, QTableView, QTabWidget, QToolBar, QVBoxLayout,
|
||||
QWidget)
|
||||
|
||||
from ackit import settings as settings_mod, workspace as W
|
||||
from ackit.catalog import available_archives
|
||||
from ackit.game import Game, GameError, default_game_dir
|
||||
from ackit.modpack import export_mod, match_mod, prepare_import
|
||||
from ackit.modpack import export_mod, match_mod, match_texts, prepare_import
|
||||
from ackit.texts import TextConflictError
|
||||
from . import icons, theme
|
||||
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 .textpanel import TextPanel
|
||||
from .userdata import UserData
|
||||
from .workers import Runner
|
||||
|
||||
@@ -201,12 +205,28 @@ class MainWindow(QMainWindow):
|
||||
hs.addWidget(self.vsplit)
|
||||
hs.setStretchFactor(1, 1)
|
||||
hs.setSizes([SIDEBAR_MAX, 100000])
|
||||
# sections: sounds, subtitles, menu text
|
||||
self.tabs = QTabWidget()
|
||||
self.tabs.setDocumentMode(True)
|
||||
self.tabs.addTab(hs, icons.icon("wave"), "Sounds")
|
||||
self.subs = TextPanel(self, "subs")
|
||||
self.menu = TextPanel(self, "menu")
|
||||
self.tabs.addTab(self.subs, icons.icon("ab"), "Subtitles")
|
||||
self.tabs.addTab(self.menu, icons.icon("settings"), "Menu text")
|
||||
self._text_loaded = set()
|
||||
for panel in (self.subs, self.menu):
|
||||
panel.message.connect(lambda m: self.statusBar().showMessage(m, 8000))
|
||||
panel.changed.connect(self._texts_changed)
|
||||
self.subs.go_to_sound.connect(self.show_sound)
|
||||
self.editor.subtitle.changed.connect(self._texts_changed)
|
||||
self.editor.subtitle.open_in_list.connect(self.show_subtitle)
|
||||
self.tabs.currentChanged.connect(self._tab_changed)
|
||||
central = QWidget()
|
||||
cl = QVBoxLayout(central)
|
||||
cl.setContentsMargins(0, 0, 0, 0)
|
||||
cl.setSpacing(0)
|
||||
cl.addWidget(self.banner)
|
||||
cl.addWidget(hs, 1)
|
||||
cl.addWidget(self.tabs, 1)
|
||||
self.setCentralWidget(central)
|
||||
self.spinner = Spinner(14)
|
||||
self.statusBar().addWidget(self.spinner)
|
||||
@@ -275,6 +295,8 @@ class MainWindow(QMainWindow):
|
||||
self.cmb_lang.setCurrentText(lang)
|
||||
self.cmb_lang.blockSignals(False)
|
||||
self.populate()
|
||||
self._text_loaded.clear()
|
||||
self._tab_changed(self.tabs.currentIndex())
|
||||
self.banner.hide()
|
||||
self.lbl_game.setText("%s • store: %s" % (game.root, ws.store.root))
|
||||
self.statusBar().showMessage("Loaded %d sounds (%s)." % (len(ws.catalog.sounds), ", ".join(ws.catalog.archives)), 6000)
|
||||
@@ -434,6 +456,42 @@ class MainWindow(QMainWindow):
|
||||
self.cmb_tag.setCurrentText(cur)
|
||||
self.cmb_tag.blockSignals(False)
|
||||
|
||||
# --- text sections --------------------------------------------------------------------------
|
||||
def _tab_changed(self, i):
|
||||
panel = self.tabs.widget(i)
|
||||
if self.ws and panel in (self.subs, self.menu) and panel.kind not in self._text_loaded:
|
||||
self._text_loaded.add(panel.kind)
|
||||
panel.load()
|
||||
|
||||
def _texts_changed(self):
|
||||
for panel in (self.subs, self.menu):
|
||||
if panel.kind in self._text_loaded:
|
||||
panel.refresh()
|
||||
self.editor.subtitle.set_sound(self.editor.sound)
|
||||
|
||||
def show_sound(self, key):
|
||||
"""Sounds tab with `key` selected (from a subtitle)."""
|
||||
s = self.ws.catalog.by_key.get(key) if self.ws else None
|
||||
if not s:
|
||||
self.inform("Sound", "%s is not in the sounds loaded now (choose its language at the top)." % key)
|
||||
return
|
||||
self.tabs.setCurrentIndex(0)
|
||||
self.clear_filters()
|
||||
self.select_name(s.name)
|
||||
|
||||
def show_subtitle(self, key):
|
||||
"""Subtitles tab with `key` selected (from the sound editor)."""
|
||||
self.tabs.setCurrentWidget(self.subs)
|
||||
lang = key.split(":")[1]
|
||||
if self.subs.lang != lang:
|
||||
self.subs.load(lang)
|
||||
self.subs.search.clear()
|
||||
self.subs.cb_mod.setChecked(False)
|
||||
for cb in self.subs.filters.values():
|
||||
cb.setCurrentIndex(0)
|
||||
self.subs._filter()
|
||||
self.subs.select_key(key)
|
||||
|
||||
# --- changes ----------------------------------------------------------------------------------
|
||||
def _after_change(self, key=None):
|
||||
self.model.refresh_keys(modified=self.ws.modified_keys())
|
||||
@@ -468,6 +526,7 @@ class MainWindow(QMainWindow):
|
||||
self.spinner.hide()
|
||||
self.statusBar().showMessage(ok_msg, 8000)
|
||||
self._after_change()
|
||||
self._texts_changed()
|
||||
if self.editor.sound:
|
||||
self.editor.set_sound(self.editor.sound)
|
||||
self.check_problems()
|
||||
@@ -493,11 +552,20 @@ class MainWindow(QMainWindow):
|
||||
st = ws.state(s)
|
||||
if st in (W.LOST, W.CHANGED):
|
||||
out.append((s, st))
|
||||
return out
|
||||
texts = []
|
||||
for key in list(ws.texts.records()):
|
||||
try:
|
||||
st = ws.texts.state(key)
|
||||
except (GameError, OSError, ValueError):
|
||||
continue
|
||||
if st in (W.LOST, W.CHANGED):
|
||||
texts.append((key, st))
|
||||
return out, texts
|
||||
|
||||
def done(problems):
|
||||
self.problems = problems
|
||||
if not problems:
|
||||
def done(res):
|
||||
problems, texts = res
|
||||
self.problems, self.text_problems = problems, texts
|
||||
if not problems and not texts:
|
||||
if self.banner.isVisible() and not self.b_choose.isVisible():
|
||||
self.banner.hide()
|
||||
return
|
||||
@@ -510,6 +578,9 @@ class MainWindow(QMainWindow):
|
||||
if changed:
|
||||
txt.append("%d sound(s) were changed by something else since you modified them: %s. "
|
||||
"Revert puts the original back." % (len(changed), ", ".join(changed[:4])))
|
||||
if texts:
|
||||
txt.append("%d of your text changes were undone or changed outside this editor: %s"
|
||||
% (len(texts), ", ".join(k for k, _ in texts[:3]) + (" …" if len(texts) > 3 else "")))
|
||||
self.banner_text.setText("\n".join(txt))
|
||||
self.b_reapply.show()
|
||||
self.b_forget.show()
|
||||
@@ -522,15 +593,24 @@ class MainWindow(QMainWindow):
|
||||
ws = self.ws
|
||||
items = [(s, ws.store.get_blob(ws.store.get(s.key)["applied"]), ws.store.get(s.key).get("meta", {}))
|
||||
for s, st in getattr(self, "problems", []) if ws.store.get(s.key) and ws.store.get(s.key).get("applied")]
|
||||
if items:
|
||||
self._run_change("Re-applying…", lambda job: ws.apply(items, force=True, progress=job.report),
|
||||
"Re-applied %d change(s)." % len(items))
|
||||
texts = {k: ws.texts.records()[k]["applied"] for k, st in getattr(self, "text_problems", [])
|
||||
if k in ws.texts.records() and ws.texts.records()[k].get("applied") is not None}
|
||||
|
||||
def work(job):
|
||||
if items:
|
||||
ws.apply(items, force=True, progress=job.report)
|
||||
if texts:
|
||||
ws.texts.apply(texts, force=True, progress=job.report)
|
||||
if items or texts:
|
||||
self._run_change("Re-applying…", work, "Re-applied %d change(s)." % (len(items) + len(texts)))
|
||||
|
||||
def forget_problems(self):
|
||||
if not self.confirm("Forget", "Stop tracking these sounds? Their current game files are kept as they are."):
|
||||
if not self.confirm("Forget", "Stop tracking these changes? The current game files are kept as they are."):
|
||||
return
|
||||
for s, _ in getattr(self, "problems", []):
|
||||
self.ws.store.forget(s.key)
|
||||
for k, _ in getattr(self, "text_problems", []):
|
||||
self.ws.texts.records().pop(k, None)
|
||||
self.ws.store.save()
|
||||
self.ws.store.gc()
|
||||
self.banner.hide()
|
||||
@@ -541,11 +621,13 @@ class MainWindow(QMainWindow):
|
||||
if not self.ws:
|
||||
return
|
||||
mods = self.ws.modified_keys()
|
||||
if not mods:
|
||||
self.inform("Export mod", "You have not changed any sound yet.")
|
||||
texts = self.ws.texts.modified()
|
||||
if not mods and not texts:
|
||||
self.inform("Export mod", "You have not changed any sound or text yet.")
|
||||
return
|
||||
if keys is None:
|
||||
names = [(k, self.ws.catalog.by_key[k].name if k in self.ws.catalog.by_key else k) for k in mods]
|
||||
names += [(k, "Text " + k) for k in texts]
|
||||
dlg = ExportModDialog(names, self)
|
||||
if dlg.exec() != QDialog.DialogCode.Accepted:
|
||||
return
|
||||
@@ -555,8 +637,10 @@ class MainWindow(QMainWindow):
|
||||
self.settings.get("last_dir") or "", (title or "my_mod") + ".acmod"), "Unheard City mod (*.acmod)")
|
||||
if not path:
|
||||
return
|
||||
self._run_change("Exporting…", lambda job: export_mod(self.ws, path, keys, title, author, description),
|
||||
"Saved %s" % path)
|
||||
text_keys = [k for k in keys if k.startswith(("SUB:", "MENU:"))]
|
||||
keys = [k for k in keys if k not in text_keys]
|
||||
self._run_change("Exporting…", lambda job: export_mod(self.ws, path, keys, title, author, description,
|
||||
texts=text_keys), "Saved %s" % path)
|
||||
|
||||
def import_mod_dialog(self, path=None, confirm=True):
|
||||
if not self.ws:
|
||||
@@ -571,18 +655,27 @@ class MainWindow(QMainWindow):
|
||||
except Exception as e: # noqa: BLE001
|
||||
self.inform("Import mod", "Cannot read this package: %s" % e)
|
||||
return
|
||||
if confirm and ImportModDialog(man, items, self).exec() != QDialog.DialogCode.Accepted:
|
||||
dlg = ImportModDialog(man, items, self, match_texts(self.ws, man)) if confirm else None
|
||||
if dlg and dlg.exec() != QDialog.DialogCode.Accepted:
|
||||
return
|
||||
ws = self.ws
|
||||
|
||||
def work(job):
|
||||
prepare_import(ws, path, items, progress=job.report)
|
||||
changes = [(im.sound, im.wem, {"source": "mod:" + os.path.basename(path)}) for im in items if im.wem]
|
||||
try:
|
||||
ws.apply(changes, progress=job.report)
|
||||
except W.ConflictError:
|
||||
ws.apply(changes, force=True, progress=job.report)
|
||||
return len(changes)
|
||||
source = {"source": "mod:" + os.path.basename(path)}
|
||||
changes = [(im.sound, im.wem, source) for im in items if im.wem]
|
||||
if changes:
|
||||
try:
|
||||
ws.apply(changes, progress=job.report)
|
||||
except W.ConflictError:
|
||||
ws.apply(changes, force=True, progress=job.report)
|
||||
texts = {k: v for k, v, problem in match_texts(ws, man) if not problem}
|
||||
if texts:
|
||||
try:
|
||||
ws.texts.apply(texts, progress=job.report, meta=source)
|
||||
except TextConflictError:
|
||||
ws.texts.apply(texts, force=True, progress=job.report, meta=source)
|
||||
return len(changes) + len(texts)
|
||||
|
||||
self._run_change("Installing mod…", work, "Installed %s" % os.path.basename(path))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user