feat: double-click a merge clip to set its join crossfade/curve override + row labels
This commit is contained in:
@@ -4684,6 +4684,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._merge_list.setSelectionMode(
|
self._merge_list.setSelectionMode(
|
||||||
QAbstractItemView.SelectionMode.SingleSelection)
|
QAbstractItemView.SelectionMode.SingleSelection)
|
||||||
self._merge_list.setToolTip("Clips to merge, top to bottom")
|
self._merge_list.setToolTip("Clips to merge, top to bottom")
|
||||||
|
self._merge_list.itemDoubleClicked.connect(self._on_merge_item_double_clicked)
|
||||||
self._merge_temps: set[str] = set()
|
self._merge_temps: set[str] = set()
|
||||||
self._spn_crossfade = QDoubleSpinBox()
|
self._spn_crossfade = QDoubleSpinBox()
|
||||||
self._spn_crossfade.setRange(0.0, 10.0)
|
self._spn_crossfade.setRange(0.0, 10.0)
|
||||||
@@ -4695,9 +4696,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._spn_crossfade.valueChanged.connect(
|
self._spn_crossfade.valueChanged.connect(
|
||||||
lambda v: self._settings.setValue("audio_crossfade", v))
|
lambda v: self._settings.setValue("audio_crossfade", v))
|
||||||
self._cmb_curve = QComboBox()
|
self._cmb_curve = QComboBox()
|
||||||
for label, name in (("Triangular", "tri"), ("Exponential", "exp"),
|
for label, name in self._MERGE_CURVES:
|
||||||
("Logarithmic", "log"), ("Quarter sine", "qsin"),
|
|
||||||
("Half sine", "hsin")):
|
|
||||||
self._cmb_curve.addItem(label, name)
|
self._cmb_curve.addItem(label, name)
|
||||||
self._cmb_curve.setToolTip("Crossfade curve at each join")
|
self._cmb_curve.setToolTip("Crossfade curve at each join")
|
||||||
self._btn_merge_add_sel = QPushButton("+ Selection")
|
self._btn_merge_add_sel = QPushButton("+ Selection")
|
||||||
@@ -5181,6 +5180,9 @@ class MainWindow(QMainWindow):
|
|||||||
APP_VERSION = "1.5"
|
APP_VERSION = "1.5"
|
||||||
_SPLIT_HEADER_H = 22 # deck split-column header height (keep both deck spots in sync)
|
_SPLIT_HEADER_H = 22 # deck split-column header height (keep both deck spots in sync)
|
||||||
_WAVE_MIN_VIEW = 3.0 # minimum waveform view window (seconds)
|
_WAVE_MIN_VIEW = 3.0 # minimum waveform view window (seconds)
|
||||||
|
_MERGE_CURVES = (("Triangular", "tri"), ("Exponential", "exp"),
|
||||||
|
("Logarithmic", "log"), ("Quarter sine", "qsin"),
|
||||||
|
("Half sine", "hsin"))
|
||||||
CHANGELOG: list[tuple[str, list[str]]] = [
|
CHANGELOG: list[tuple[str, list[str]]] = [
|
||||||
("1.5", [
|
("1.5", [
|
||||||
"<b>Interactive waveform</b> — the Audio tab's waveform is now a "
|
"<b>Interactive waveform</b> — the Audio tab's waveform is now a "
|
||||||
@@ -6760,9 +6762,29 @@ class MainWindow(QMainWindow):
|
|||||||
item.setData(Qt.ItemDataRole.UserRole + 2, None) # crossfade override (None = global)
|
item.setData(Qt.ItemDataRole.UserRole + 2, None) # crossfade override (None = global)
|
||||||
item.setData(Qt.ItemDataRole.UserRole + 3, None) # curve override (None = global)
|
item.setData(Qt.ItemDataRole.UserRole + 3, None) # curve override (None = global)
|
||||||
self._merge_list.addItem(item)
|
self._merge_list.addItem(item)
|
||||||
|
self._refresh_merge_labels()
|
||||||
finally:
|
finally:
|
||||||
QApplication.restoreOverrideCursor()
|
QApplication.restoreOverrideCursor()
|
||||||
|
|
||||||
|
def _merge_item_label(self, item, index: int, count: int) -> str:
|
||||||
|
name = os.path.basename(item.data(Qt.ItemDataRole.UserRole))
|
||||||
|
dur = item.data(Qt.ItemDataRole.UserRole + 1)
|
||||||
|
base = f"{name} ({dur:.2f}s)" if dur is not None else name
|
||||||
|
if index < count - 1: # has an outgoing join
|
||||||
|
xo = item.data(Qt.ItemDataRole.UserRole + 2)
|
||||||
|
co = item.data(Qt.ItemDataRole.UserRole + 3)
|
||||||
|
if xo is not None or co is not None:
|
||||||
|
xf = xo if xo is not None else self._spn_crossfade.value()
|
||||||
|
cv = co if co is not None else self._cmb_curve.currentData()
|
||||||
|
base += f" ⤲{xf:.2f}s {cv}" # ⤲ join marker
|
||||||
|
return base
|
||||||
|
|
||||||
|
def _refresh_merge_labels(self) -> None:
|
||||||
|
n = self._merge_list.count()
|
||||||
|
for i in range(n):
|
||||||
|
it = self._merge_list.item(i)
|
||||||
|
it.setText(self._merge_item_label(it, i, n))
|
||||||
|
|
||||||
def _on_merge_add_file(self) -> None:
|
def _on_merge_add_file(self) -> None:
|
||||||
default_dir = (self._settings.value("audio_extract_dir", "")
|
default_dir = (self._settings.value("audio_extract_dir", "")
|
||||||
or os.path.dirname(self._file_path or ""))
|
or os.path.dirname(self._file_path or ""))
|
||||||
@@ -6811,11 +6833,45 @@ class MainWindow(QMainWindow):
|
|||||||
item = self._merge_list.takeItem(row)
|
item = self._merge_list.takeItem(row)
|
||||||
self._merge_list.insertItem(new, item)
|
self._merge_list.insertItem(new, item)
|
||||||
self._merge_list.setCurrentRow(new)
|
self._merge_list.setCurrentRow(new)
|
||||||
|
self._refresh_merge_labels()
|
||||||
|
|
||||||
def _merge_remove_selected(self) -> None:
|
def _merge_remove_selected(self) -> None:
|
||||||
row = self._merge_list.currentRow()
|
row = self._merge_list.currentRow()
|
||||||
if row >= 0:
|
if row >= 0:
|
||||||
self._merge_list.takeItem(row)
|
self._merge_list.takeItem(row)
|
||||||
|
self._refresh_merge_labels()
|
||||||
|
|
||||||
|
def _apply_merge_override(self, item, crossfade: float, curve: str) -> None:
|
||||||
|
item.setData(Qt.ItemDataRole.UserRole + 2, float(crossfade))
|
||||||
|
item.setData(Qt.ItemDataRole.UserRole + 3, curve)
|
||||||
|
self._refresh_merge_labels()
|
||||||
|
|
||||||
|
def _on_merge_item_double_clicked(self, item) -> None:
|
||||||
|
row = self._merge_list.row(item)
|
||||||
|
if row >= self._merge_list.count() - 1:
|
||||||
|
self._show_status("The last clip has no outgoing join to crossfade", 4000)
|
||||||
|
return
|
||||||
|
dlg = QDialog(self)
|
||||||
|
dlg.setWindowTitle("Join crossfade")
|
||||||
|
form = QFormLayout(dlg)
|
||||||
|
spn = QDoubleSpinBox()
|
||||||
|
spn.setRange(0.0, 10.0); spn.setDecimals(2); spn.setSingleStep(0.1); spn.setSuffix(" s")
|
||||||
|
xo = item.data(Qt.ItemDataRole.UserRole + 2)
|
||||||
|
spn.setValue(float(xo) if xo is not None else self._spn_crossfade.value())
|
||||||
|
cmb = QComboBox()
|
||||||
|
for label, name in self._MERGE_CURVES:
|
||||||
|
cmb.addItem(label, name)
|
||||||
|
co = item.data(Qt.ItemDataRole.UserRole + 3)
|
||||||
|
co = co if co is not None else self._cmb_curve.currentData()
|
||||||
|
idx = cmb.findData(co); cmb.setCurrentIndex(idx if idx >= 0 else 0)
|
||||||
|
form.addRow("Crossfade:", spn)
|
||||||
|
form.addRow("Curve:", cmb)
|
||||||
|
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok
|
||||||
|
| QDialogButtonBox.StandardButton.Cancel)
|
||||||
|
bb.accepted.connect(dlg.accept); bb.rejected.connect(dlg.reject)
|
||||||
|
form.addRow(bb)
|
||||||
|
if dlg.exec():
|
||||||
|
self._apply_merge_override(item, spn.value(), cmb.currentData())
|
||||||
|
|
||||||
def _merge_paths(self) -> list[str]:
|
def _merge_paths(self) -> list[str]:
|
||||||
return [self._merge_list.item(i).data(Qt.ItemDataRole.UserRole)
|
return [self._merge_list.item(i).data(Qt.ItemDataRole.UserRole)
|
||||||
|
|||||||
@@ -616,3 +616,24 @@ def test_merge_preview_blocks_short_clip(win, tmp_path, monkeypatch):
|
|||||||
def test_merge_preview_empty_is_noop(win):
|
def test_merge_preview_empty_is_noop(win):
|
||||||
win._merge_list.clear()
|
win._merge_list.clear()
|
||||||
win._on_merge_preview()
|
win._on_merge_preview()
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_override_applies_and_labels(win, tmp_path):
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
|
a = tmp_path / "a.wav"; b = tmp_path / "b.wav"; a.write_bytes(b""); b.write_bytes(b"")
|
||||||
|
win._merge_list.clear(); win._merge_add_paths([str(a), str(b)])
|
||||||
|
it0 = win._merge_list.item(0)
|
||||||
|
win._apply_merge_override(it0, 0.8, "exp")
|
||||||
|
assert it0.data(Qt.ItemDataRole.UserRole + 2) == 0.8
|
||||||
|
assert it0.data(Qt.ItemDataRole.UserRole + 3) == "exp"
|
||||||
|
assert "0.80s exp" in it0.text()
|
||||||
|
assert "⤲" not in win._merge_list.item(1).text() # last item shows no join info
|
||||||
|
xfs, cvs = win._merge_join_values()
|
||||||
|
assert xfs == [0.8] and cvs == ["exp"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_double_click_last_is_noop(win, tmp_path):
|
||||||
|
a = tmp_path / "a.wav"; a.write_bytes(b"")
|
||||||
|
win._merge_list.clear(); win._merge_add_paths([str(a)])
|
||||||
|
win._on_merge_item_double_clicked(win._merge_list.item(0)) # only/last -> no dialog, no raise
|
||||||
|
assert win._merge_list.count() == 1
|
||||||
|
|||||||
Reference in New Issue
Block a user