fix: UX audit — shortcuts in text fields, delete confirmation, overwrite indicator

- Suppress global shortcuts (E/J/L/K/M/Space/P/arrows) when typing in
  text fields via ShortcutOverride event filter
- Add delete confirmation dialog before removing clips from disk + DB
- Export button turns red "Overwrite" when a marker is selected
- Reset stale overwrite/delete state when switching files
- Remove auto-advance after export; add N shortcut to advance manually
- Widen marker hit zones (±6→±10px click, ±4→±8px hover)
- Marker tooltip shows filename instead of full path

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-12 15:49:27 +02:00
parent 89e0478777
commit bb6e3c623a
+42 -7
View File
@@ -20,8 +20,9 @@ from PyQt6.QtWidgets import (
QLabel, QPushButton, QLineEdit, QFileDialog, QFrame, QStatusBar,
QListWidget, QListWidgetItem, QAbstractItemView, QSplitter, QToolTip,
QComboBox, QDialog, QPlainTextEdit, QCheckBox, QSpinBox, QDoubleSpinBox,
QMessageBox,
)
from PyQt6.QtCore import Qt, QThread, QTimer, pyqtSignal, QSettings
from PyQt6.QtCore import Qt, QObject, QThread, QTimer, pyqtSignal, QSettings
from PyQt6.QtGui import QPainter, QColor, QPen, QPixmap, QDragEnterEvent, QDropEvent, QCursor, QFont, QKeySequence, QShortcut
import mpv
@@ -676,7 +677,7 @@ class TimelineWidget(QWidget):
if self._hover_cache:
w = self.width()
for (frac, output_path) in self._hover_cache:
if abs(x - frac * w) <= 6:
if abs(x - frac * w) <= 10:
t = frac * self._duration
self.marker_clicked.emit(t, output_path)
self._seek(x)
@@ -686,12 +687,12 @@ class TimelineWidget(QWidget):
def mouseMoveEvent(self, event):
x = event.position().x()
# Check marker hover (±4px) using pre-computed fractions.
# Check marker hover using pre-computed fractions.
if self._hover_cache:
w = self.width()
for (frac, output_path) in self._hover_cache:
if abs(x - frac * w) <= 4:
QToolTip.showText(QCursor.pos(), output_path, self)
if abs(x - frac * w) <= 8:
QToolTip.showText(QCursor.pos(), os.path.basename(output_path), self)
if event.buttons():
self._seek(x)
return
@@ -711,7 +712,7 @@ class TimelineWidget(QWidget):
w = self.width()
hit_path = None
for (frac, output_path) in self._hover_cache:
if abs(x - frac * w) <= 6:
if abs(x - frac * w) <= 10:
hit_path = output_path
break
if hit_path is None:
@@ -1240,6 +1241,16 @@ class SettingsDialog(QDialog):
self._log.appendPlainText(f"ERROR: {msg}")
class _KeyFilter(QObject):
"""Suppress global keyboard shortcuts when a text input widget has focus."""
def eventFilter(self, obj, event):
from PyQt6.QtCore import QEvent
if event.type() == QEvent.Type.ShortcutOverride and isinstance(obj, QLineEdit):
event.accept()
return True
return super().eventFilter(obj, event)
def main():
# Force desktop OpenGL (not GLES) so mpv's render context produces non-black output.
# Must be set before QApplication.
@@ -1252,6 +1263,8 @@ def main():
app = QApplication(sys.argv)
locale.setlocale(locale.LC_NUMERIC, "C") # QApplication resets locale; re-apply for libmpv
_kf = _KeyFilter(app)
app.installEventFilter(_kf)
app.setStyle("Fusion")
app.setStyleSheet("""
QWidget { background: #1e1e1e; color: #ddd; }
@@ -1600,6 +1613,7 @@ class MainWindow(QMainWindow):
QShortcut(QKeySequence("K"), self, context=ctx).activated.connect(self._on_pause)
QShortcut(QKeySequence("E"), self, context=ctx).activated.connect(self._on_export)
QShortcut(QKeySequence("M"), self, context=ctx).activated.connect(self._jump_to_next_marker)
QShortcut(QKeySequence("N"), self, context=ctx).activated.connect(self._playlist.advance)
def _load_file(self, path: str):
self._file_path = path
@@ -1616,6 +1630,13 @@ class MainWindow(QMainWindow):
self._btn_play.setEnabled(True)
self._btn_pause.setEnabled(True)
self._btn_export.setEnabled(True)
# Reset stale state from previous file
self._overwrite_path = ""
self._last_export_path = ""
self._btn_export.setText("Export")
self._btn_export.setStyleSheet("")
self._btn_delete.setEnabled(False)
self._btn_delete.setText("Delete")
self._fps = self._mpv.get_fps()
self._crop_bar.set_source_ratio(*self._mpv.get_video_size())
# Reset export settings to defaults for the new video
@@ -1663,6 +1684,8 @@ class MainWindow(QMainWindow):
def _on_marker_clicked(self, start_time: float, output_path: str) -> None:
self._overwrite_path = output_path
self._lbl_next.setText(f"{os.path.basename(output_path)}")
self._btn_export.setText("Overwrite")
self._btn_export.setStyleSheet("QPushButton { background: #6a3030; border-color: #a04040; }")
self._btn_delete.setEnabled(True)
self._btn_delete.setText(f"Delete {os.path.basename(output_path)}")
# Restore config from the original export
@@ -1695,6 +1718,8 @@ class MainWindow(QMainWindow):
def _on_marker_deselected(self) -> None:
if self._overwrite_path:
self._overwrite_path = ""
self._btn_export.setText("Export")
self._btn_export.setStyleSheet("")
self._update_next_label()
if not self._last_export_path:
self._btn_delete.setEnabled(False)
@@ -1705,6 +1730,13 @@ class MainWindow(QMainWindow):
if not target:
return
name = os.path.basename(target)
reply = QMessageBox.question(
self, "Delete clip",
f"Delete {name} from disk and database?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply != QMessageBox.StandardButton.Yes:
return
# Delete from disk
if os.path.isdir(target):
shutil.rmtree(target, ignore_errors=True)
@@ -1993,6 +2025,8 @@ class MainWindow(QMainWindow):
self._export_counter += 1
self._update_next_label()
self._btn_export.setEnabled(True)
self._btn_export.setText("Export")
self._btn_export.setStyleSheet("")
self._btn_delete.setEnabled(True)
self._btn_delete.setText("Delete")
self._refresh_markers()
@@ -2004,10 +2038,11 @@ class MainWindow(QMainWindow):
self._txt_label.addItems(self._db.get_labels())
self._txt_label.setCurrentText(current)
self._txt_label.blockSignals(False)
self._playlist.advance()
def _on_export_error(self, msg: str):
self._btn_export.setEnabled(True)
self._btn_export.setText("Export")
self._btn_export.setStyleSheet("")
self.statusBar().showMessage(f"Export error: {msg}")
# --- Mask generation ---