mirror of
https://github.com/yusufipk/dikte.git
synced 2026-09-11 10:56:10 +00:00
Keep the wheel off the boxes and a floor under the window
Two things the scrolling tabs brought with them. A combo box and a spin box read the wheel as a change of value, and Qt hands them the focus before delivering it. Now that every tab scrolls, rolling down the API tab with the pointer over the model box picks a different model on the way past, and Save writes it down. The boxes take the focus by click or by tab only, and a wheel that arrives at one without the focus is refused rather than swallowed, so it carries on up to the scroll area and the page moves instead. A tab that scrolls also asks for no height of its own, which left nothing to stop the window being dragged down to a tab bar and half a button. It has a floor now, and the floor never asks for more room than the screen was just found to have.
This commit is contained in:
+40
-6
@@ -4,12 +4,12 @@ import os
|
||||
import shutil
|
||||
import threading
|
||||
|
||||
from PyQt6.QtCore import QRect, Qt, QUrl, pyqtSignal
|
||||
from PyQt6.QtCore import QEvent, QObject, QRect, Qt, QUrl, pyqtSignal
|
||||
from PyQt6.QtGui import QDesktopServices, QGuiApplication, QKeySequence, QShortcut
|
||||
from PyQt6.QtWidgets import (
|
||||
QAbstractItemView, QCheckBox, QComboBox, QDialog, QDialogButtonBox,
|
||||
QFileDialog, QFormLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit,
|
||||
QListWidget, QListWidgetItem, QMenu, QMessageBox, QPlainTextEdit,
|
||||
QAbstractItemView, QAbstractSpinBox, QCheckBox, QComboBox, QDialog,
|
||||
QDialogButtonBox, QFileDialog, QFormLayout, QGroupBox, QHBoxLayout, QLabel,
|
||||
QLineEdit, QListWidget, QListWidgetItem, QMenu, QMessageBox, QPlainTextEdit,
|
||||
QPushButton, QScrollArea, QSpinBox, QTabWidget, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
@@ -159,6 +159,26 @@ class WrappedLabel(QLabel):
|
||||
self.fontMetrics().boundingRect(box, wrap, self.text()).height())
|
||||
|
||||
|
||||
class WheelGuard(QObject):
|
||||
"""Keeps a rolled wheel off the box the pointer only passed over.
|
||||
|
||||
A combo box and a spin box both read the wheel as a change of value, and
|
||||
every tab scrolls now: rolling down the API tab with the pointer over the
|
||||
model box would pick a different model on the way past, and the setting is
|
||||
saved without anybody having chosen it. The wheel counts once the box has
|
||||
been clicked into; before that it is handed back to the page underneath,
|
||||
which is what the roll was for.
|
||||
"""
|
||||
|
||||
def eventFilter(self, box, event):
|
||||
if event.type() == QEvent.Type.Wheel and not box.hasFocus():
|
||||
# Refused rather than swallowed. An unaccepted wheel event carries
|
||||
# on up the parents to the scroll area, so the page still moves.
|
||||
event.ignore()
|
||||
return True
|
||||
return super().eventFilter(box, event)
|
||||
|
||||
|
||||
class LocalModelBox(QGroupBox):
|
||||
"""The program, the model, and the two downloads that put them there.
|
||||
|
||||
@@ -516,6 +536,10 @@ class SettingsWindow(QDialog):
|
||||
self.transcriber = FileTranscriber(conf, self)
|
||||
self.setWindowTitle(t("Dikte Settings"))
|
||||
|
||||
# One for the whole window, parented to it so it outlives the boxes it
|
||||
# watches and goes when they do.
|
||||
self._wheel_guard = WheelGuard(self)
|
||||
|
||||
tabs = self.tabs = QTabWidget(self)
|
||||
tabs.addTab(self._scrolled(self._general_tab()), t("General"))
|
||||
self.api_tab_index = tabs.addTab(
|
||||
@@ -560,8 +584,7 @@ class SettingsWindow(QDialog):
|
||||
if not conf.transcribe_ready():
|
||||
self.tabs.setCurrentIndex(self.api_tab_index)
|
||||
|
||||
@staticmethod
|
||||
def _scrolled(page):
|
||||
def _scrolled(self, page):
|
||||
"""A tab that scrolls instead of growing the window to fit."""
|
||||
# Every tab goes through here. A page kept at its full height passes
|
||||
# that height on as the window's minimum, and a tall one (the API tab
|
||||
@@ -571,6 +594,12 @@ class SettingsWindow(QDialog):
|
||||
area.setWidgetResizable(True)
|
||||
area.setFrameShape(QScrollArea.Shape.NoFrame)
|
||||
area.setWidget(page)
|
||||
for box in page.findChildren((QComboBox, QAbstractSpinBox)):
|
||||
# Focus by click or by tab, not by wheel. Qt hands the focus over
|
||||
# before it delivers the wheel, so a box left on the default policy
|
||||
# would have it by the time the guard below asked.
|
||||
box.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
box.installEventFilter(self._wheel_guard)
|
||||
return area
|
||||
|
||||
def _size_to_screen(self, width, height):
|
||||
@@ -582,6 +611,11 @@ class SettingsWindow(QDialog):
|
||||
# and the buttons along the bottom stay on screen.
|
||||
width = min(width, room.width() - 40)
|
||||
height = min(height, room.height() - 80)
|
||||
# Scrolling tabs ask for no height of their own, which leaves nothing to
|
||||
# stop the window being dragged down to a tab bar and half a button. The
|
||||
# floor is a floor and not a demand: it never asks for more room than
|
||||
# the screen has just been found to have.
|
||||
self.setMinimumSize(min(520, width), min(380, height))
|
||||
self.resize(width, height)
|
||||
|
||||
# ---- tabs ----------------------------------------------------------
|
||||
|
||||
@@ -11,6 +11,8 @@ import unittest
|
||||
from typing import ClassVar
|
||||
from unittest import mock
|
||||
|
||||
from PyQt6.QtCore import QPoint, QPointF, Qt
|
||||
from PyQt6.QtGui import QWheelEvent
|
||||
from PyQt6.QtWidgets import QApplication, QMessageBox
|
||||
|
||||
import cleanup
|
||||
@@ -116,6 +118,10 @@ class Settings(DikteTest):
|
||||
"_load_models"))
|
||||
self.enterContext(mock.patch.object(settings_ui.SettingsWindow,
|
||||
"_load_transcribe_models"))
|
||||
# The local model boxes fetch their own list the moment they are shown,
|
||||
# from a thread, which is nobody's test failing but a real request.
|
||||
self.enterContext(mock.patch.object(settings_ui.LocalModelBox,
|
||||
"_fetch_models"))
|
||||
self.enterContext(mock.patch.object(settings_ui.hotkey, "APPLICATIONS_DIR",
|
||||
self.path("applications")))
|
||||
self.enterContext(mock.patch.object(settings_ui.hotkey, "SHORTCUTS_FILE",
|
||||
@@ -127,6 +133,14 @@ class Settings(DikteTest):
|
||||
self.addCleanup(window.close)
|
||||
return window
|
||||
|
||||
@staticmethod
|
||||
def wheel():
|
||||
"""One notch of a mouse wheel, rolled downwards."""
|
||||
return QWheelEvent(QPointF(5, 5), QPointF(5, 5), QPoint(0, 0),
|
||||
QPoint(0, -120), Qt.MouseButton.NoButton,
|
||||
Qt.KeyboardModifier.NoModifier,
|
||||
Qt.ScrollPhase.NoScrollPhase, False)
|
||||
|
||||
def test_the_window_opens_with_every_tab_on_it(self):
|
||||
window = self.window(cfg.Config())
|
||||
tabs = window.findChildren(settings_ui.QTabWidget)[0]
|
||||
@@ -143,6 +157,41 @@ class Settings(DikteTest):
|
||||
self.assertLess(window.minimumSizeHint().height(), 500,
|
||||
window.tabs.tabText(index))
|
||||
|
||||
def test_the_window_cannot_be_dragged_down_to_a_stub(self):
|
||||
# A tab that scrolls asks for no height of its own, which leaves nothing
|
||||
# to stop the window being pulled down to a tab bar and half a button.
|
||||
window = self.window(cfg.Config())
|
||||
window.resize(1, 1)
|
||||
self.assertGreaterEqual(window.width(), 500)
|
||||
self.assertGreaterEqual(window.height(), 360)
|
||||
|
||||
def test_the_wheel_passes_over_a_box_it_was_not_aimed_at(self):
|
||||
# Every tab scrolls now, and a combo box reads the wheel as a change of
|
||||
# value: rolling down the page with the pointer over the language box
|
||||
# would pick another language on the way past, and Save would write it
|
||||
# down. The box takes the wheel once it has been clicked into.
|
||||
window = self.window(cfg.Config())
|
||||
# Shown and activated, because a box in a window nobody is looking at
|
||||
# can be given the focus but never has it.
|
||||
window.show()
|
||||
window.activateWindow()
|
||||
QApplication.processEvents()
|
||||
box = window.ui_language
|
||||
# Not the wheel focus a combo box has by default: Qt hands the focus
|
||||
# over before it delivers the wheel, which would make "has the focus"
|
||||
# true for the very roll being refused.
|
||||
self.assertEqual(box.focusPolicy(), Qt.FocusPolicy.StrongFocus)
|
||||
before = box.currentIndex()
|
||||
rolled = self.wheel()
|
||||
QApplication.sendEvent(box, rolled)
|
||||
self.assertEqual(box.currentIndex(), before)
|
||||
# Refused, not swallowed. An unaccepted wheel event is the one Qt
|
||||
# carries on up to the scroll area, so the page moves instead.
|
||||
self.assertFalse(rolled.isAccepted())
|
||||
box.setFocus()
|
||||
QApplication.sendEvent(box, self.wheel())
|
||||
self.assertNotEqual(box.currentIndex(), before)
|
||||
|
||||
def test_a_wrapped_label_keeps_the_room_its_lines_need(self):
|
||||
# The program path shares a row with a button, and a row is measured
|
||||
# before its width is known: the label has to claim the second line back
|
||||
|
||||
Reference in New Issue
Block a user