Version 3.2.3: editor layout with aligned controls and full-width button bars; docs on recordings shared between languages

This commit is contained in:
2026-10-11 01:17:35 +02:00
parent b6b96307ca
commit e3040b8e6e
13 changed files with 119 additions and 57 deletions

View File

@@ -54,6 +54,15 @@ class EditorPanel(QWidget):
b.setToolTip(tip)
return b
@staticmethod
def _fill(*items):
"""A row of buttons filling the column's width: [(widget, share)], equal edges in every column."""
row = QHBoxLayout()
row.setSpacing(6)
for w, share in items:
row.addWidget(w, share)
return row
def _wave(self, kind, selectable=False):
w = Waveform(selectable=selectable)
w.accent, w.bg = theme.WAVE[kind], theme.WAVE_BG
@@ -63,7 +72,7 @@ class EditorPanel(QWidget):
"""Header on top; below, side by side (resizable): LISTEN | REPLACE (with the apply buttons) | SUBTITLE."""
self.setObjectName("EditorPanel")
lay = QVBoxLayout(self)
lay.setContentsMargins(10, 6, 10, 6)
lay.setContentsMargins(8, 6, 8, 6)
lay.setSpacing(4)
self.title = QLabel()
self.title.setObjectName("EditorTitle")
@@ -87,41 +96,47 @@ class EditorPanel(QWidget):
# listen ----------------------------------------------------------------------------------
g = QGroupBox("LISTEN")
gl = QGridLayout(g)
gl.setContentsMargins(8, 8, 8, 8)
gl.setVerticalSpacing(4)
self.wf_orig = self._wave("orig")
self.wf_inst = self._wave("inst")
self.b_play_orig = self._btn("Original", "play", lambda: self.play("orig"), "Play the original (Space)", icons.STEEL)
self.b_play_inst = self._btn("Installed", "play", lambda: self.play("inst"), "Play what the game has now", "#8f4a4e")
self.b_stop = self._btn("Stop", "stop", self.stop, "Stop (Esc)")
self.b_ab = self._btn("A/B", "ab", self.ab_switch, "Switch between original and replacement at the same position",
icons.ACCENT)
self.cb_loop = self._btn("Loop", "loop", None, "Repeat playback")
# playback options next to the volume: icons with tooltips
self.b_ab = self._btn("", "ab", self.ab_switch, "A/B: switch between the original and the replacement at "
"the same position", icons.ACCENT)
self.cb_loop = self._btn("", "loop", None, "Loop: repeat playback")
self.cb_loop.setCheckable(True)
for b in (self.b_ab, self.cb_loop):
b.setFixedWidth(b.sizeHint().height() + 4)
self.sl_vol = QSlider(Qt.Orientation.Horizontal)
self.sl_vol.setRange(0, 100)
self.sl_vol.setValue(80)
self.sl_vol.setMaximumWidth(120)
self.sl_vol.setMinimumWidth(80)
self.sl_vol.setToolTip("Volume")
self.sl_vol.valueChanged.connect(lambda v: self.ctx.player.set_volume(v / 100.0))
self.lbl_time = QLabel("0:00.00")
self.lbl_time.setObjectName("Muted")
self.lbl_time.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
self.lbl_time.setMinimumWidth(self.lbl_time.fontMetrics().horizontalAdvance("00:00.00") + 4)
gl.addWidget(QLabel("Original"), 0, 0)
gl.addWidget(self.wf_orig, 0, 1)
self.lbl_inst = QLabel("Installed")
gl.addWidget(self.lbl_inst, 1, 0)
gl.addWidget(self.wf_inst, 1, 1)
row = QHBoxLayout()
for w in (self.b_play_orig, self.b_play_inst, self.b_stop):
row.addWidget(w)
row.addStretch(1)
gl.addLayout(row, 2, 0, 1, 2)
row = QHBoxLayout()
for w in (self.b_ab, self.cb_loop, QLabel("Volume"), self.sl_vol, self.lbl_time):
row.addWidget(w)
row.addStretch(1)
gl.addLayout(row, 3, 0, 1, 2)
gl.setRowStretch(2, 1) # controls at the bottom, level with the other columns' actions
gl.addLayout(self._fill((self.b_play_orig, 1), (self.b_play_inst, 1), (self.b_stop, 1)), 4, 0, 1, 2)
vol = QHBoxLayout() # playback options: A/B, loop, volume, position
vol.setSpacing(6)
vol.addWidget(self.b_ab)
vol.addWidget(self.cb_loop)
vol.addSpacing(6)
vol.addWidget(QLabel("Volume"))
vol.addWidget(self.sl_vol, 1)
vol.addWidget(self.lbl_time)
gl.addLayout(vol, 3, 0, 1, 2)
gl.setColumnStretch(1, 1)
gl.setRowStretch(4, 1)
self.wf_orig.seekRequested.connect(lambda t: self.play("orig", t))
self.wf_inst.seekRequested.connect(lambda t: self.play("inst", t))
listen = g
@@ -129,25 +144,29 @@ class EditorPanel(QWidget):
# replace ---------------------------------------------------------------------------------
g = QGroupBox("REPLACE")
rl = QVBoxLayout(g)
rl.setSpacing(4)
rl.setContentsMargins(8, 8, 8, 8)
rl.setSpacing(6)
src = QHBoxLayout()
src.setSpacing(6)
self.b_load = self._btn("Load file…", "open", self.load_file_dialog,
"WAV, FLAC, OGG, OPUS, MP3, AIFF (M4A/WMA need ffmpeg installed) (Ctrl+O)")
self.cmb_in = QComboBox()
self.cmb_in.setMinimumWidth(110)
self.cmb_in.setMaximumWidth(200)
self.cmb_in.setToolTip("Recording device")
self.b_rec = self._btn("Record", "record", self.toggle_record, "Record from the selected input")
self.meter = QProgressBar()
self.meter.setRange(0, 100)
self.meter.setTextVisible(False)
self.meter.setMaximumWidth(60)
self.meter.setFixedWidth(60)
self.meter.setToolTip("Input level")
self.lbl_rec = QLabel("")
self.lbl_rec.setObjectName("Muted")
for w in (self.b_load, self.cmb_in, self.b_rec, self.meter, self.lbl_rec):
src.addWidget(self.b_load, 1) # the row fills the column: file | microphone, level
src.addSpacing(10)
src.addWidget(self.cmb_in, 2)
src.addWidget(self.b_rec, 1)
for w in (self.meter, self.lbl_rec):
src.addWidget(w)
src.addStretch(1)
rl.addLayout(src)
self.wf_new = self._wave("new", selectable=True)
self.wf_new.setToolTip("Click: play from here. Right-drag (or Shift+drag): select the part to keep.")
@@ -160,12 +179,12 @@ class EditorPanel(QWidget):
s_.setDecimals(2)
s_.setSuffix(" s")
s_.setRange(0, 36000)
s_.setMaximumWidth(92)
s_.setMaximumWidth(80)
s_.valueChanged.connect(self._changed)
self.sp_gain = QDoubleSpinBox()
self.sp_gain.setRange(-40, 40)
self.sp_gain.setSuffix(" dB")
self.sp_gain.setMaximumWidth(92)
self.sp_gain.setMaximumWidth(80)
self.sp_gain.valueChanged.connect(self._changed)
self.cb_norm = QCheckBox("Normalize")
self.cb_match = QCheckBox("Match loudness")
@@ -175,15 +194,24 @@ class EditorPanel(QWidget):
self.b_silence = self._btn("Trim silence", None, self.trim_silence, "Cut silence at the start and end")
self.b_play_new = self._btn("Replacement", "play", lambda: self.play("new"), "Play the processed replacement",
icons.ACCENT)
proc = QHBoxLayout()
for w in (QLabel("Keep"), self.sp_a, QLabel("to"), self.sp_b, self.b_silence, self.b_play_new):
proc.addWidget(w)
proc.addStretch(1)
rl.addLayout(proc)
proc = QHBoxLayout()
for w in (QLabel("Gain"), self.sp_gain, self.cb_norm, self.cb_match):
proc.addWidget(w)
proc.addStretch(1)
# edit controls in a grid: labels in one column, the fields aligned, playback on the right
proc = QGridLayout()
proc.setHorizontalSpacing(6)
proc.setVerticalSpacing(6)
proc.addWidget(QLabel("Keep"), 0, 0)
proc.addWidget(self.sp_a, 0, 1)
proc.addWidget(QLabel("to"), 0, 2, Qt.AlignmentFlag.AlignCenter)
proc.addWidget(self.sp_b, 0, 3)
proc.addLayout(self._fill((self.b_silence, 1), (self.b_play_new, 1)), 0, 4) # the rest of the width
proc.addWidget(QLabel("Gain"), 1, 0)
proc.addWidget(self.sp_gain, 1, 1)
checks = QHBoxLayout()
checks.setSpacing(12)
checks.addWidget(self.cb_norm)
checks.addWidget(self.cb_match)
checks.addStretch(1)
proc.addLayout(checks, 1, 2, 1, 3)
proc.setColumnStretch(4, 1)
rl.addLayout(proc)
info = QHBoxLayout()
self.lbl_new = QLabel()
@@ -200,7 +228,6 @@ class EditorPanel(QWidget):
info.addWidget(self.lbl_warn, 1)
rl.addLayout(info)
# apply (belongs to the replacement) ------------------------------------------------------
act = QHBoxLayout()
self.b_apply = self._btn("Apply to game", "apply", self.apply,
"Encode the replacement and install it in the game files (Ctrl+S)", "#ffffff")
self.b_apply.setObjectName("Primary")
@@ -209,9 +236,9 @@ class EditorPanel(QWidget):
"Save the original (or the replacement) as a WAV file")
self.busy = Spinner(16)
self.lbl_busy = QLabel()
for w in (self.b_apply, self.b_revert, self.b_export, self.busy, self.lbl_busy):
act.addWidget(w)
act.addStretch(1)
info.addWidget(self.busy, 0, Qt.AlignmentFlag.AlignTop) # encoding/writing: next to the status
info.addWidget(self.lbl_busy, 0, Qt.AlignmentFlag.AlignTop)
act = self._fill((self.b_apply, 2), (self.b_revert, 1), (self.b_export, 1))
rl.addStretch(1)
rl.addLayout(act)
replace = g
@@ -220,12 +247,13 @@ class EditorPanel(QWidget):
from .textpanel import SubtitleBox
gs = QGroupBox("SUBTITLE")
gsl = QVBoxLayout(gs)
gsl.setContentsMargins(8, 8, 8, 8)
self.subtitle = SubtitleBox(self.ctx, gs)
gsl.addWidget(self.subtitle)
gsl.addStretch(1)
gsl.addWidget(self.subtitle, 1)
self.columns = QSplitter(Qt.Orientation.Horizontal)
self.columns.setChildrenCollapsible(False)
self.columns.setHandleWidth(4)
for w, stretch in ((listen, 3), (replace, 4), (gs, 3)):
self.columns.addWidget(w)
self.columns.setStretchFactor(self.columns.count() - 1, stretch)

View File

@@ -143,12 +143,16 @@ class SubtitleBox(QWidget):
super().__init__(parent)
self.win = win
self.keys = []
self.lay = QFormLayout(self)
outer = QVBoxLayout(self)
outer.setContentsMargins(0, 0, 0, 0)
outer.setSpacing(6)
self.lay = QFormLayout() # one line edit per subtitle line
self.lay.setContentsMargins(0, 0, 0, 0)
self.edits = []
self.lbl = QLabel()
self.lbl.setObjectName("Muted")
row = QHBoxLayout()
row.setSpacing(6)
self.b_apply = QPushButton(icons.icon("apply"), "Apply")
self.b_revert = QPushButton(icons.icon("revert"), "Revert")
self.b_open = QPushButton(icons.icon("search"), "Show in list")
@@ -158,16 +162,14 @@ class SubtitleBox(QWidget):
self.b_apply.clicked.connect(self.apply)
self.b_revert.clicked.connect(self.revert)
self.b_open.clicked.connect(lambda: self.keys and self.open_in_list.emit(self.keys[0]))
for b in (self.b_apply, self.b_revert):
row.addWidget(b)
row.addStretch(1)
row2 = QHBoxLayout()
row2.addWidget(self.b_open)
row2.addStretch(1)
for b in (self.b_apply, self.b_revert): # two halves of the column, like the sound's action bar
row.addWidget(b, 1)
self.lbl.setWordWrap(True)
self.lay.addRow(self.lbl)
self.lay.addRow(row)
self.lay.addRow(row2)
outer.addLayout(self.lay)
outer.addWidget(self.lbl) # room counter
outer.addWidget(self.b_open) # the way to the Subtitles tab, across the column
outer.addStretch(1) # actions at the bottom, level with the sound's
outer.addLayout(row)
def set_sound(self, s):
for e in self.edits: