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:
@@ -20,8 +20,9 @@ from PyQt6.QtWidgets import (
|
|||||||
QLabel, QPushButton, QLineEdit, QFileDialog, QFrame, QStatusBar,
|
QLabel, QPushButton, QLineEdit, QFileDialog, QFrame, QStatusBar,
|
||||||
QListWidget, QListWidgetItem, QAbstractItemView, QSplitter, QToolTip,
|
QListWidget, QListWidgetItem, QAbstractItemView, QSplitter, QToolTip,
|
||||||
QComboBox, QDialog, QPlainTextEdit, QCheckBox, QSpinBox, QDoubleSpinBox,
|
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
|
from PyQt6.QtGui import QPainter, QColor, QPen, QPixmap, QDragEnterEvent, QDropEvent, QCursor, QFont, QKeySequence, QShortcut
|
||||||
import mpv
|
import mpv
|
||||||
|
|
||||||
@@ -676,7 +677,7 @@ class TimelineWidget(QWidget):
|
|||||||
if self._hover_cache:
|
if self._hover_cache:
|
||||||
w = self.width()
|
w = self.width()
|
||||||
for (frac, output_path) in self._hover_cache:
|
for (frac, output_path) in self._hover_cache:
|
||||||
if abs(x - frac * w) <= 6:
|
if abs(x - frac * w) <= 10:
|
||||||
t = frac * self._duration
|
t = frac * self._duration
|
||||||
self.marker_clicked.emit(t, output_path)
|
self.marker_clicked.emit(t, output_path)
|
||||||
self._seek(x)
|
self._seek(x)
|
||||||
@@ -686,12 +687,12 @@ class TimelineWidget(QWidget):
|
|||||||
|
|
||||||
def mouseMoveEvent(self, event):
|
def mouseMoveEvent(self, event):
|
||||||
x = event.position().x()
|
x = event.position().x()
|
||||||
# Check marker hover (±4px) using pre-computed fractions.
|
# Check marker hover using pre-computed fractions.
|
||||||
if self._hover_cache:
|
if self._hover_cache:
|
||||||
w = self.width()
|
w = self.width()
|
||||||
for (frac, output_path) in self._hover_cache:
|
for (frac, output_path) in self._hover_cache:
|
||||||
if abs(x - frac * w) <= 4:
|
if abs(x - frac * w) <= 8:
|
||||||
QToolTip.showText(QCursor.pos(), output_path, self)
|
QToolTip.showText(QCursor.pos(), os.path.basename(output_path), self)
|
||||||
if event.buttons():
|
if event.buttons():
|
||||||
self._seek(x)
|
self._seek(x)
|
||||||
return
|
return
|
||||||
@@ -711,7 +712,7 @@ class TimelineWidget(QWidget):
|
|||||||
w = self.width()
|
w = self.width()
|
||||||
hit_path = None
|
hit_path = None
|
||||||
for (frac, output_path) in self._hover_cache:
|
for (frac, output_path) in self._hover_cache:
|
||||||
if abs(x - frac * w) <= 6:
|
if abs(x - frac * w) <= 10:
|
||||||
hit_path = output_path
|
hit_path = output_path
|
||||||
break
|
break
|
||||||
if hit_path is None:
|
if hit_path is None:
|
||||||
@@ -1240,6 +1241,16 @@ class SettingsDialog(QDialog):
|
|||||||
self._log.appendPlainText(f"ERROR: {msg}")
|
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():
|
def main():
|
||||||
# Force desktop OpenGL (not GLES) so mpv's render context produces non-black output.
|
# Force desktop OpenGL (not GLES) so mpv's render context produces non-black output.
|
||||||
# Must be set before QApplication.
|
# Must be set before QApplication.
|
||||||
@@ -1252,6 +1263,8 @@ def main():
|
|||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
locale.setlocale(locale.LC_NUMERIC, "C") # QApplication resets locale; re-apply for libmpv
|
locale.setlocale(locale.LC_NUMERIC, "C") # QApplication resets locale; re-apply for libmpv
|
||||||
|
_kf = _KeyFilter(app)
|
||||||
|
app.installEventFilter(_kf)
|
||||||
app.setStyle("Fusion")
|
app.setStyle("Fusion")
|
||||||
app.setStyleSheet("""
|
app.setStyleSheet("""
|
||||||
QWidget { background: #1e1e1e; color: #ddd; }
|
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("K"), self, context=ctx).activated.connect(self._on_pause)
|
||||||
QShortcut(QKeySequence("E"), self, context=ctx).activated.connect(self._on_export)
|
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("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):
|
def _load_file(self, path: str):
|
||||||
self._file_path = path
|
self._file_path = path
|
||||||
@@ -1616,6 +1630,13 @@ class MainWindow(QMainWindow):
|
|||||||
self._btn_play.setEnabled(True)
|
self._btn_play.setEnabled(True)
|
||||||
self._btn_pause.setEnabled(True)
|
self._btn_pause.setEnabled(True)
|
||||||
self._btn_export.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._fps = self._mpv.get_fps()
|
||||||
self._crop_bar.set_source_ratio(*self._mpv.get_video_size())
|
self._crop_bar.set_source_ratio(*self._mpv.get_video_size())
|
||||||
# Reset export settings to defaults for the new video
|
# 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:
|
def _on_marker_clicked(self, start_time: float, output_path: str) -> None:
|
||||||
self._overwrite_path = output_path
|
self._overwrite_path = output_path
|
||||||
self._lbl_next.setText(f"↺ {os.path.basename(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.setEnabled(True)
|
||||||
self._btn_delete.setText(f"Delete {os.path.basename(output_path)}")
|
self._btn_delete.setText(f"Delete {os.path.basename(output_path)}")
|
||||||
# Restore config from the original export
|
# Restore config from the original export
|
||||||
@@ -1695,6 +1718,8 @@ class MainWindow(QMainWindow):
|
|||||||
def _on_marker_deselected(self) -> None:
|
def _on_marker_deselected(self) -> None:
|
||||||
if self._overwrite_path:
|
if self._overwrite_path:
|
||||||
self._overwrite_path = ""
|
self._overwrite_path = ""
|
||||||
|
self._btn_export.setText("Export")
|
||||||
|
self._btn_export.setStyleSheet("")
|
||||||
self._update_next_label()
|
self._update_next_label()
|
||||||
if not self._last_export_path:
|
if not self._last_export_path:
|
||||||
self._btn_delete.setEnabled(False)
|
self._btn_delete.setEnabled(False)
|
||||||
@@ -1705,6 +1730,13 @@ class MainWindow(QMainWindow):
|
|||||||
if not target:
|
if not target:
|
||||||
return
|
return
|
||||||
name = os.path.basename(target)
|
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
|
# Delete from disk
|
||||||
if os.path.isdir(target):
|
if os.path.isdir(target):
|
||||||
shutil.rmtree(target, ignore_errors=True)
|
shutil.rmtree(target, ignore_errors=True)
|
||||||
@@ -1993,6 +2025,8 @@ class MainWindow(QMainWindow):
|
|||||||
self._export_counter += 1
|
self._export_counter += 1
|
||||||
self._update_next_label()
|
self._update_next_label()
|
||||||
self._btn_export.setEnabled(True)
|
self._btn_export.setEnabled(True)
|
||||||
|
self._btn_export.setText("Export")
|
||||||
|
self._btn_export.setStyleSheet("")
|
||||||
self._btn_delete.setEnabled(True)
|
self._btn_delete.setEnabled(True)
|
||||||
self._btn_delete.setText("Delete")
|
self._btn_delete.setText("Delete")
|
||||||
self._refresh_markers()
|
self._refresh_markers()
|
||||||
@@ -2004,10 +2038,11 @@ class MainWindow(QMainWindow):
|
|||||||
self._txt_label.addItems(self._db.get_labels())
|
self._txt_label.addItems(self._db.get_labels())
|
||||||
self._txt_label.setCurrentText(current)
|
self._txt_label.setCurrentText(current)
|
||||||
self._txt_label.blockSignals(False)
|
self._txt_label.blockSignals(False)
|
||||||
self._playlist.advance()
|
|
||||||
|
|
||||||
def _on_export_error(self, msg: str):
|
def _on_export_error(self, msg: str):
|
||||||
self._btn_export.setEnabled(True)
|
self._btn_export.setEnabled(True)
|
||||||
|
self._btn_export.setText("Export")
|
||||||
|
self._btn_export.setStyleSheet("")
|
||||||
self.statusBar().showMessage(f"Export error: {msg}")
|
self.statusBar().showMessage(f"Export error: {msg}")
|
||||||
|
|
||||||
# --- Mask generation ---
|
# --- Mask generation ---
|
||||||
|
|||||||
Reference in New Issue
Block a user