Version 3.4.0: Edit tab (edit list, large subtitle, replacements with effects and presets), Sounds tab only browses, ackit fx (pedalboard + own reverb) and replace --fx, first Linux build, catalog without a mod package

This commit is contained in:
2026-10-28 13:54:12 +01:00
parent a706dae419
commit 6de470174e
36 changed files with 2872 additions and 278 deletions

View File

@@ -1,12 +1,13 @@
"""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)."""
"""Main window. Tabs: Sounds (browse: filters + sound table, a preview below; app/preview.py), Edit (the edit list,
the editor and the effects: app/edit.py; the only place where sounds are replaced), Subtitles and Menu text
(app/textpanel.py)."""
import os
from PySide6.QtCore import QByteArray, QEvent, QItemSelectionModel, QSize, Qt, QTimer
from PySide6.QtCore import QByteArray, 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, QTabWidget, QToolBar, QVBoxLayout,
QPushButton, QSplitter, QTableView, QTabWidget, QToolBar, QVBoxLayout,
QWidget)
from ackit import settings as settings_mod, workspace as W
@@ -17,15 +18,15 @@ 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 .edit import EditPanel
from .models import C_FAV, C_NAME, COLS, SoundFilter, SoundModel
from .preview import PreviewPanel
from .spinner import BusyOverlay, Spinner
from .textpanel import TextPanel
from .userdata import UserData
from .workers import Runner
SIDEBAR_MIN, SIDEBAR_MAX = 220, 320
TABLE_MIN = 160 # the sound list never gets smaller than this when fitting the editor
ANY = "Any"
@@ -196,20 +197,16 @@ class MainWindow(QMainWindow):
self.table.sortByColumn(C_NAME, Qt.SortOrder.AscendingOrder)
self.table.selectionModel().currentRowChanged.connect(self._current_changed)
self.table.clicked.connect(self._clicked)
self.table.doubleClicked.connect(lambda _: self.editor.play("orig"))
self.table.doubleClicked.connect(lambda _: self.preview.play("orig"))
self.table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.table.customContextMenuRequested.connect(self._context_menu)
# editor
self.editor = EditorPanel(self)
self.editor.applied.connect(self._after_change)
self.editor.reverted.connect(self._after_change)
self.editor.message.connect(lambda m: self.statusBar().showMessage(m, 8000))
scroll = QScrollArea()
scroll.setWidget(self.editor)
scroll.setWidgetResizable(True)
# preview of the selected sound (no editing here)
self.preview = PreviewPanel(self)
self.preview.edit_requested.connect(self.edit_selected)
self.preview.add_requested.connect(self.add_selected_to_edit)
# sidebar | sound list on top, the editor below across the whole width
# sidebar | sound list on top, the preview below across the whole width
hs = QSplitter(Qt.Orientation.Horizontal)
hs.addWidget(side)
hs.addWidget(self.table)
@@ -217,18 +214,19 @@ class MainWindow(QMainWindow):
hs.setSizes([SIDEBAR_MAX, 100000])
self.vsplit = QSplitter(Qt.Orientation.Vertical)
self.vsplit.addWidget(hs)
self.vsplit.addWidget(scroll)
self.vsplit.setStretchFactor(0, 1) # extra height goes to the list; the editor keeps its fitted height
self.vsplit.addWidget(self.preview)
self.vsplit.setStretchFactor(0, 1) # extra height goes to the list
self.vsplit.setStretchFactor(1, 0)
self.scroll = scroll
self._fitted = False
self._user_sized = False # once the divider is dragged, keep the user's choice
self.vsplit.splitterMoved.connect(lambda *_: setattr(self, "_user_sized", True))
self.editor.installEventFilter(self)
# sections: sounds, subtitles, menu text
self.vsplit.setChildrenCollapsible(False)
# sections: sounds (browse), edit, subtitles, menu text
self.tabs = QTabWidget()
self.tabs.setDocumentMode(True)
self.tabs.addTab(self.vsplit, icons.icon("wave"), "Sounds")
self.edit = EditPanel(self)
self.editor = self.edit.editor # the one sound editor
self.tabs.addTab(self.edit, icons.icon("edit"), "Edit")
self.edit.message.connect(lambda m: self.statusBar().showMessage(m, 8000))
self.editor.message.connect(lambda m: self.statusBar().showMessage(m, 8000))
self.subs = TextPanel(self, "subs")
self.menu = TextPanel(self, "menu")
self.tabs.addTab(self.subs, icons.icon("ab"), "Subtitles")
@@ -255,12 +253,15 @@ class MainWindow(QMainWindow):
sc = Qt.ShortcutContext.WindowShortcut
QShortcut(QKeySequence("Ctrl+F"), self, self.search.setFocus, context=sc)
QShortcut(QKeySequence("Ctrl+S"), self, lambda: self.editor.apply(), context=sc)
QShortcut(QKeySequence("Ctrl+O"), self, lambda: self.editor.load_file_dialog(), context=sc)
QShortcut(QKeySequence("Escape"), self, self.editor.stop, context=sc)
QShortcut(QKeySequence(Qt.Key.Key_Space), self.table, self.editor.toggle_play,
context=Qt.ShortcutContext.WidgetShortcut)
QShortcut(QKeySequence(Qt.Key.Key_F2), self.table, self.edit_notes, context=Qt.ShortcutContext.WidgetShortcut)
edit = Qt.ShortcutContext.WidgetWithChildrenShortcut # editing shortcuts: in the Edit tab only
QShortcut(QKeySequence("Ctrl+S"), self.edit, lambda: self.editor.apply(), context=edit)
QShortcut(QKeySequence("Ctrl+O"), self.edit, lambda: self.editor.load_file_dialog(), context=edit)
QShortcut(QKeySequence("Escape"), self, self.player.stop, context=sc)
wsc = Qt.ShortcutContext.WidgetShortcut
QShortcut(QKeySequence(Qt.Key.Key_Space), self.table, self.preview.toggle_play, context=wsc)
QShortcut(QKeySequence(Qt.Key.Key_Return), self.table, self.edit_selected, context=wsc)
QShortcut(QKeySequence(Qt.Key.Key_Enter), self.table, self.edit_selected, context=wsc)
QShortcut(QKeySequence(Qt.Key.Key_F2), self.table, self.edit_notes, context=wsc)
# --- dialogs (overridable in tests) -----------------------------------------------------------
def confirm(self, title, text):
@@ -355,6 +356,7 @@ class MainWindow(QMainWindow):
fill(self.cmb_arch, list(ws.catalog.archives))
fill(self.cmb_tag, self.userdata.all_tags())
self._filters_changed()
self.edit.populate(ws)
def _fill_categories(self):
g = self.cmb_group.currentText()
@@ -405,7 +407,22 @@ class MainWindow(QMainWindow):
def _current_changed(self, cur, prev):
if cur.isValid():
self.editor.set_sound(self.proxy.sound_at(cur))
self.preview.set_sound(self.proxy.sound_at(cur))
def edit_selected(self):
"""Selected sounds onto the edit list; the Edit tab opens on the first."""
sel = self.selected_sounds() or ([self.preview.sound] if self.preview.sound else [])
if sel:
self.edit_sounds(sel)
def edit_sounds(self, sounds):
self.edit.add(sounds, open_first=True)
self.tabs.setCurrentWidget(self.edit)
def add_selected_to_edit(self):
sel = self.selected_sounds()
self.edit.add(sel)
self.statusBar().showMessage("%d sound(s) on the edit list (Edit tab)" % len(sel), 5000)
def _clicked(self, index):
if index.column() == C_FAV:
@@ -429,7 +446,10 @@ class MainWindow(QMainWindow):
if not sel:
return
m = QMenu(self)
m.addAction("Play original", lambda: self.editor.play("orig"))
m.addAction(icons.icon("edit"), "Edit (Enter)", self.edit_selected)
m.addAction(icons.icon("add"), "Add to edit list", self.add_selected_to_edit)
m.addSeparator()
m.addAction("Play original (Space)", lambda: self.preview.play("orig"))
m.addAction("Notes && tags… (F2)", self.edit_notes)
m.addAction("Toggle favorite", lambda: self._toggle_fav(sel))
m.addSeparator()
@@ -492,6 +512,8 @@ class MainWindow(QMainWindow):
if panel.kind in self._text_loaded:
panel.refresh()
self.editor.subtitle.set_sound(self.editor.sound)
if self.preview.sound:
self.preview.set_sound(self.preview.sound)
def show_sound(self, key):
"""Sounds tab with `key` selected (from a subtitle)."""
@@ -515,8 +537,12 @@ class MainWindow(QMainWindow):
# --- changes ----------------------------------------------------------------------------------
def _after_change(self, key=None):
self.model.refresh_keys(modified=self.ws.modified_keys()) # the view refilters/re-sorts itself
modified = self.ws.modified_keys()
self.model.refresh_keys(modified=modified) # the view refilters/re-sorts itself
self._update_count()
self.edit.refresh(modified)
if self.preview.sound:
self.preview.set_sound(self.preview.sound)
def revert_sounds(self, sounds):
sounds = [s for s in sounds if self.ws.store.get(s.key)]
@@ -572,7 +598,7 @@ class MainWindow(QMainWindow):
self._after_change()
self._texts_changed()
if self.editor.sound:
self.editor.set_sound(self.editor.sound)
self.edit.load(self.editor.sound)
# --- changed underneath -----------------------------------------------------------------------
def check_problems(self):
@@ -744,31 +770,8 @@ class MainWindow(QMainWindow):
except OSError:
pass
def fit_editor(self):
"""Give the editor panel the height of its contents (everything visible), the list gets the rest."""
total = sum(self.vsplit.sizes()) or self.vsplit.height()
lay = self.editor.layout()
need = self.editor.sizeHint().height()
if lay.hasHeightForWidth(): # wrapped labels: height at the actual width
need = max(need, lay.heightForWidth(self.scroll.viewport().width()))
need += 2 * self.scroll.frameWidth()
if self.editor.minimumSizeHint().width() > self.scroll.viewport().width(): # narrow window: room for the h-bar
need += self.scroll.horizontalScrollBar().sizeHint().height()
editor_h = min(need, max(0, total - TABLE_MIN))
self.vsplit.setSizes([total - editor_h, editor_h])
def eventFilter(self, obj, e):
if obj is self.editor and e.type() == QEvent.Type.LayoutRequest and self._fitted and not self._user_sized:
QTimer.singleShot(0, self.fit_editor)
return super().eventFilter(obj, e)
def showEvent(self, e):
super().showEvent(e)
if not self._fitted:
self._fitted = True
QTimer.singleShot(0, self.fit_editor)
def closeEvent(self, e):
self.player.stop()
self.edit.flush()
self.save_settings()
super().closeEvent(e)