feat(settings): prevent spinbox wheel changes
This commit is contained in:
+47
-12
@@ -5,7 +5,8 @@ from __future__ import annotations
|
|||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from PySide6.QtCore import Signal
|
from PySide6.QtCore import QPointF, Signal
|
||||||
|
from PySide6.QtGui import QWheelEvent
|
||||||
|
|
||||||
from ... import ai as ai_module
|
from ... import ai as ai_module
|
||||||
from ... import chrome
|
from ... import chrome
|
||||||
@@ -28,6 +29,39 @@ def AIModelTestWorker(*args, **kwargs):
|
|||||||
def CMHubSettingsWorker(*args, **kwargs):
|
def CMHubSettingsWorker(*args, **kwargs):
|
||||||
return _call_package_attr("CMHubSettingsWorker", _RealCMHubSettingsWorker, *args, **kwargs)
|
return _call_package_attr("CMHubSettingsWorker", _RealCMHubSettingsWorker, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsSpinBox(QSpinBox):
|
||||||
|
"""Forward wheel input to the settings scroll area instead of changing a value."""
|
||||||
|
|
||||||
|
def wheelEvent(self, event):
|
||||||
|
target = self._outer_scroll_viewport()
|
||||||
|
if target is None:
|
||||||
|
event.ignore()
|
||||||
|
return
|
||||||
|
global_position = event.globalPosition()
|
||||||
|
local_position = target.mapFromGlobal(global_position.toPoint())
|
||||||
|
forwarded = QWheelEvent(
|
||||||
|
QPointF(local_position),
|
||||||
|
global_position,
|
||||||
|
event.pixelDelta(),
|
||||||
|
event.angleDelta(),
|
||||||
|
event.buttons(),
|
||||||
|
event.modifiers(),
|
||||||
|
event.phase(),
|
||||||
|
event.inverted(),
|
||||||
|
)
|
||||||
|
QApplication.sendEvent(target, forwarded)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def _outer_scroll_viewport(self):
|
||||||
|
parent = self.parentWidget()
|
||||||
|
while parent is not None:
|
||||||
|
if isinstance(parent, QScrollArea):
|
||||||
|
return parent.viewport()
|
||||||
|
parent = parent.parentWidget()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class SettingsTab(QWidget):
|
class SettingsTab(QWidget):
|
||||||
"""Tab 5: AI model definitions stored in data/config/ai_models.json."""
|
"""Tab 5: AI model definitions stored in data/config/ai_models.json."""
|
||||||
|
|
||||||
@@ -117,7 +151,7 @@ class SettingsTab(QWidget):
|
|||||||
self.cmhub_image_alias_combo.setObjectName("cmhubImageAliasCombo")
|
self.cmhub_image_alias_combo.setObjectName("cmhubImageAliasCombo")
|
||||||
self.cmhub_vision_alias_combo = QComboBox()
|
self.cmhub_vision_alias_combo = QComboBox()
|
||||||
self.cmhub_vision_alias_combo.setObjectName("cmhubVisionAliasCombo")
|
self.cmhub_vision_alias_combo.setObjectName("cmhubVisionAliasCombo")
|
||||||
self.cmhub_connect_timeout_spin = QSpinBox()
|
self.cmhub_connect_timeout_spin = SettingsSpinBox()
|
||||||
self.cmhub_connect_timeout_spin.setObjectName("cmhubConnectTimeoutSpin")
|
self.cmhub_connect_timeout_spin.setObjectName("cmhubConnectTimeoutSpin")
|
||||||
self.cmhub_connect_timeout_spin.setRange(1, 3600)
|
self.cmhub_connect_timeout_spin.setRange(1, 3600)
|
||||||
self.cmhub_connect_timeout_spin.setValue(appconfig.CMHUB_CONNECT_TIMEOUT_DEFAULT)
|
self.cmhub_connect_timeout_spin.setValue(appconfig.CMHUB_CONNECT_TIMEOUT_DEFAULT)
|
||||||
@@ -161,7 +195,7 @@ class SettingsTab(QWidget):
|
|||||||
self.api_key_edit = QLineEdit()
|
self.api_key_edit = QLineEdit()
|
||||||
self.api_key_edit.setObjectName("modelApiKeyEdit")
|
self.api_key_edit.setObjectName("modelApiKeyEdit")
|
||||||
self.api_key_edit.setEchoMode(QLineEdit.Password)
|
self.api_key_edit.setEchoMode(QLineEdit.Password)
|
||||||
self.connect_timeout_spin = QSpinBox()
|
self.connect_timeout_spin = SettingsSpinBox()
|
||||||
self.connect_timeout_spin.setObjectName("connectTimeoutSpin")
|
self.connect_timeout_spin.setObjectName("connectTimeoutSpin")
|
||||||
self.connect_timeout_spin.setRange(1, 3600)
|
self.connect_timeout_spin.setRange(1, 3600)
|
||||||
self.connect_timeout_spin.setValue(30)
|
self.connect_timeout_spin.setValue(30)
|
||||||
@@ -173,19 +207,19 @@ class SettingsTab(QWidget):
|
|||||||
self.default_text_model_combo.setObjectName("defaultTextModelCombo")
|
self.default_text_model_combo.setObjectName("defaultTextModelCombo")
|
||||||
self.default_image_model_combo = QComboBox()
|
self.default_image_model_combo = QComboBox()
|
||||||
self.default_image_model_combo.setObjectName("defaultImageModelCombo")
|
self.default_image_model_combo.setObjectName("defaultImageModelCombo")
|
||||||
self.title_concurrency_spin = QSpinBox()
|
self.title_concurrency_spin = SettingsSpinBox()
|
||||||
self.title_concurrency_spin.setObjectName("titleConcurrencySpin")
|
self.title_concurrency_spin.setObjectName("titleConcurrencySpin")
|
||||||
self.title_concurrency_spin.setRange(
|
self.title_concurrency_spin.setRange(
|
||||||
appconfig.AI_CONCURRENCY_MIN,
|
appconfig.AI_CONCURRENCY_MIN,
|
||||||
appconfig.AI_CONCURRENCY_MAX,
|
appconfig.AI_CONCURRENCY_MAX,
|
||||||
)
|
)
|
||||||
self.image_concurrency_spin = QSpinBox()
|
self.image_concurrency_spin = SettingsSpinBox()
|
||||||
self.image_concurrency_spin.setObjectName("imageConcurrencySpin")
|
self.image_concurrency_spin.setObjectName("imageConcurrencySpin")
|
||||||
self.image_concurrency_spin.setRange(
|
self.image_concurrency_spin.setRange(
|
||||||
appconfig.AI_CONCURRENCY_MIN,
|
appconfig.AI_CONCURRENCY_MIN,
|
||||||
appconfig.AI_CONCURRENCY_MAX,
|
appconfig.AI_CONCURRENCY_MAX,
|
||||||
)
|
)
|
||||||
self.retry_spin = QSpinBox()
|
self.retry_spin = SettingsSpinBox()
|
||||||
self.retry_spin.setObjectName("retrySpin")
|
self.retry_spin.setObjectName("retrySpin")
|
||||||
self.retry_spin.setRange(appconfig.AI_RETRY_MIN, appconfig.AI_RETRY_MAX)
|
self.retry_spin.setRange(appconfig.AI_RETRY_MIN, appconfig.AI_RETRY_MAX)
|
||||||
self.resolution_combo = QComboBox()
|
self.resolution_combo = QComboBox()
|
||||||
@@ -219,16 +253,16 @@ class SettingsTab(QWidget):
|
|||||||
self.db_path_edit.setObjectName("dbPathEdit")
|
self.db_path_edit.setObjectName("dbPathEdit")
|
||||||
self.db_path_edit.setEnabled(False)
|
self.db_path_edit.setEnabled(False)
|
||||||
self.db_path_edit.setVisible(False)
|
self.db_path_edit.setVisible(False)
|
||||||
self.default_debug_port_spin = QSpinBox()
|
self.default_debug_port_spin = SettingsSpinBox()
|
||||||
self.default_debug_port_spin.setObjectName("defaultDebugPortSpin")
|
self.default_debug_port_spin.setObjectName("defaultDebugPortSpin")
|
||||||
self.default_debug_port_spin.setRange(1, 65535)
|
self.default_debug_port_spin.setRange(1, 65535)
|
||||||
self.debug_port_start_spin = QSpinBox()
|
self.debug_port_start_spin = SettingsSpinBox()
|
||||||
self.debug_port_start_spin.setObjectName("debugPortStartSpin")
|
self.debug_port_start_spin.setObjectName("debugPortStartSpin")
|
||||||
self.debug_port_start_spin.setRange(1, 65535)
|
self.debug_port_start_spin.setRange(1, 65535)
|
||||||
self.debug_port_end_spin = QSpinBox()
|
self.debug_port_end_spin = SettingsSpinBox()
|
||||||
self.debug_port_end_spin.setObjectName("debugPortEndSpin")
|
self.debug_port_end_spin.setObjectName("debugPortEndSpin")
|
||||||
self.debug_port_end_spin.setRange(1, 65535)
|
self.debug_port_end_spin.setRange(1, 65535)
|
||||||
self.cdp_ready_timeout_spin = QSpinBox()
|
self.cdp_ready_timeout_spin = SettingsSpinBox()
|
||||||
self.cdp_ready_timeout_spin.setObjectName("cdpReadyTimeoutSpin")
|
self.cdp_ready_timeout_spin.setObjectName("cdpReadyTimeoutSpin")
|
||||||
self.cdp_ready_timeout_spin.setRange(1, 3600)
|
self.cdp_ready_timeout_spin.setRange(1, 3600)
|
||||||
self.save_config_button = QPushButton("保存设置")
|
self.save_config_button = QPushButton("保存设置")
|
||||||
@@ -236,11 +270,11 @@ class SettingsTab(QWidget):
|
|||||||
self.unsaved_changes_label.setObjectName("settingsUnsavedChangesLabel")
|
self.unsaved_changes_label.setObjectName("settingsUnsavedChangesLabel")
|
||||||
self.unsaved_changes_label.setStyleSheet("color: #bc4c00; font-weight: 600;")
|
self.unsaved_changes_label.setStyleSheet("color: #bc4c00; font-weight: 600;")
|
||||||
self.unsaved_changes_label.setVisible(False)
|
self.unsaved_changes_label.setVisible(False)
|
||||||
self.max_items_per_run_spin = QSpinBox()
|
self.max_items_per_run_spin = SettingsSpinBox()
|
||||||
self.max_items_per_run_spin.setObjectName("maxItemsPerRunSpin")
|
self.max_items_per_run_spin.setObjectName("maxItemsPerRunSpin")
|
||||||
self.max_items_per_run_spin.setRange(1, 9999)
|
self.max_items_per_run_spin.setRange(1, 9999)
|
||||||
self.max_items_per_run_spin.setToolTip("作为每批最大更新条数;正式更新会分批处理当前筛选全部可更新记录。")
|
self.max_items_per_run_spin.setToolTip("作为每批最大更新条数;正式更新会分批处理当前筛选全部可更新记录。")
|
||||||
self.max_parallel_accounts_spin = QSpinBox()
|
self.max_parallel_accounts_spin = SettingsSpinBox()
|
||||||
self.max_parallel_accounts_spin.setObjectName("maxParallelAccountsSpin")
|
self.max_parallel_accounts_spin.setObjectName("maxParallelAccountsSpin")
|
||||||
self.max_parallel_accounts_spin.setRange(
|
self.max_parallel_accounts_spin.setRange(
|
||||||
appconfig.SHOPEE_PARALLEL_ACCOUNTS_MIN,
|
appconfig.SHOPEE_PARALLEL_ACCOUNTS_MIN,
|
||||||
@@ -409,6 +443,7 @@ class SettingsTab(QWidget):
|
|||||||
panel_layout.addStretch(1)
|
panel_layout.addStretch(1)
|
||||||
|
|
||||||
scroll = QScrollArea()
|
scroll = QScrollArea()
|
||||||
|
self.settings_scroll_area = scroll
|
||||||
scroll.setWidgetResizable(True)
|
scroll.setWidgetResizable(True)
|
||||||
scroll_content = QWidget()
|
scroll_content = QWidget()
|
||||||
scroll_layout = QHBoxLayout(scroll_content)
|
scroll_layout = QHBoxLayout(scroll_content)
|
||||||
|
|||||||
+1
-1
@@ -170,7 +170,7 @@
|
|||||||
|
|
||||||
## 设置
|
## 设置
|
||||||
|
|
||||||
- 设置页整体布局:内容区居中,左右留白已从 T-506 初始实现缩短到约 40%;实现上用最大内容宽度 + 自适应 margin,而不是写死窗口像素。所有设置组默认响应式 3 列表单:短字段占 1 格,URL/API Key/路径等长字段跨 2 格或 3 格;窄窗口自动降为 2 列/1 列。点击「保存设置」成功后,状态栏显示“设置已保存”,并弹出轻量提示框。T-531 已完成:保存成功会清除未保存标记;保存失败时保留未保存标记并阻止离开。
|
- 设置页整体布局:内容区居中,左右留白已从 T-506 初始实现缩短到约 40%;实现上用最大内容宽度 + 自适应 margin,而不是写死窗口像素。所有设置组默认响应式 3 列表单:短字段占 1 格,URL/API Key/路径等长字段跨 2 格或 3 格;窄窗口自动降为 2 列/1 列。点击「保存设置」成功后,状态栏显示“设置已保存”,并弹出轻量提示框。T-531 已完成:保存成功会清除未保存标记;保存失败时保留未保存标记并阻止离开。设置页所有数字微调框禁用滚轮改值,滚动输入交给外层设置页,仍可通过箭头、键盘和直接输入修改。
|
||||||
- 生成网关(T-677/T-680):在原「AI 模型配置」标题下使用同行标准单选框「默认网关 / 自定义网关」替代历史「AI 后端」下拉和不明显的分段按钮;启动/放弃未保存更改时按已保存的 `ai.backend=cmhub/direct` 自动勾选对应项,圆点与焦点状态表达当前选择。切换只改变待保存状态并立即切换原有面板,点击「保存设置」才写入配置;默认网关的 `data/config/cmhub.json` 与自定义模型的 `data/config/ai_models.json` 始终并存,模型新增、保存、删除仍立即写盘,不受来源待保存语义影响。除这两个选择控件外,不改变设置页其它组件、表单布局或业务逻辑。
|
- 生成网关(T-677/T-680):在原「AI 模型配置」标题下使用同行标准单选框「默认网关 / 自定义网关」替代历史「AI 后端」下拉和不明显的分段按钮;启动/放弃未保存更改时按已保存的 `ai.backend=cmhub/direct` 自动勾选对应项,圆点与焦点状态表达当前选择。切换只改变待保存状态并立即切换原有面板,点击「保存设置」才写入配置;默认网关的 `data/config/cmhub.json` 与自定义模型的 `data/config/ai_models.json` 始终并存,模型新增、保存、删除仍立即写盘,不受来源待保存语义影响。除这两个选择控件外,不改变设置页其它组件、表单布局或业务逻辑。
|
||||||
- 默认网关面板展示 Base URL、API Key、生文/生图/图片理解别名、连接超时、批量生成前检查余额、刷新别名和测试连接/查余额;Base URL 输入框旁提示“只填网关根,如 https://host”,保存/刷新前规整掉 `/api`、`/api/v1` 或其它路径。测试连接成功优先显示 `账号「<账号名>」连接默认网关成功`,邮箱脱敏;没有账号信息时显示「默认网关连接成功」。Key 只本地明文保存、UI 打码显示,不进入日志或导出。
|
- 默认网关面板展示 Base URL、API Key、生文/生图/图片理解别名、连接超时、批量生成前检查余额、刷新别名和测试连接/查余额;Base URL 输入框旁提示“只填网关根,如 https://host”,保存/刷新前规整掉 `/api`、`/api/v1` 或其它路径。测试连接成功优先显示 `账号「<账号名>」连接默认网关成功`,邮箱脱敏;没有账号信息时显示「默认网关连接成功」。Key 只本地明文保存、UI 打码显示,不进入日志或导出。
|
||||||
- 自定义网关复用既有模型列表、其下紧凑显示的模型配置字段、默认文本模型与默认图像模型;不再显示重复的“模型详情”标题,模型选择区到配置表单保留 8px 间距。文本模型保留 `chat` / `auto`;图片模型固定使用「OpenAI 图片编辑接口」(`images_edits`),②和⑥通过 `/v1/images/edits` 以有序重复 `image[]` 上传,第一张是商品主体图、每次只请求 `n=1` 输出。历史 `chat` / `auto` 图片模型不会被改写或隐藏,但会标示「图片接口不支持」,生成前会被拦截。图片响应只接受 `data[].b64_json` / `data[].url`,不适配聊天响应或服务商私有结构。说明行明确「图片理解仅支持默认网关」。
|
- 自定义网关复用既有模型列表、其下紧凑显示的模型配置字段、默认文本模型与默认图像模型;不再显示重复的“模型详情”标题,模型选择区到配置表单保留 8px 间距。文本模型保留 `chat` / `auto`;图片模型固定使用「OpenAI 图片编辑接口」(`images_edits`),②和⑥通过 `/v1/images/edits` 以有序重复 `image[]` 上传,第一张是商品主体图、每次只请求 `n=1` 输出。历史 `chat` / `auto` 图片模型不会被改写或隐藏,但会标示「图片接口不支持」,生成前会被拦截。图片响应只接受 `data[].b64_json` / `data[].url`,不适配聊天响应或服务商私有结构。说明行明确「图片理解仅支持默认网关」。
|
||||||
|
|||||||
+5
-2
@@ -3,7 +3,7 @@ id: T-683
|
|||||||
title: 禁用设置页数字微调框滚轮改值
|
title: 禁用设置页数字微调框滚轮改值
|
||||||
phase: 5
|
phase: 5
|
||||||
deps: [T-682]
|
deps: [T-682]
|
||||||
status: TODO
|
status: DONE
|
||||||
created: 2026-07-21
|
created: 2026-07-21
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -54,4 +54,7 @@ git diff --check
|
|||||||
|
|
||||||
## 执行记录
|
## 执行记录
|
||||||
|
|
||||||
- 待实现。
|
- 新增设置页专用 `SettingsSpinBox`,其滚轮事件不调用数值步进,而是按原始位置、增量和修饰键转发给最近的 `QScrollArea` 视口;设置页保留 `settings_scroll_area` 引用供明确的交互回归。
|
||||||
|
- 设置页全部 11 个数字微调框均替换为该组件:两类连接超时、标题/图片并发、重试、三个端口、Chrome 就绪超时、每批最大更新条数和同时更新蝦皮账号。箭头、键盘与直接输入未修改。
|
||||||
|
- 更新设置页交互说明和 GUI 回归:确认全部数字框都是专用组件,滚轮后数值及 dirty 状态不变、外层页面继续滚动,键盘上箭头仍会改值并进入未保存状态。
|
||||||
|
- 验证通过:`py -3.10 -m unittest tests.test_gui`(213 项)、`py -3.10 -m unittest discover -s tests`(654 项)、`py -3.10 -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`git diff --check`。
|
||||||
|
|||||||
+83
-2
@@ -29,8 +29,8 @@ from app import (
|
|||||||
if gui.QT_IMPORT_ERROR is not None:
|
if gui.QT_IMPORT_ERROR is not None:
|
||||||
raise unittest.SkipTest("PySide6 未安装")
|
raise unittest.SkipTest("PySide6 未安装")
|
||||||
|
|
||||||
from PySide6.QtCore import QItemSelectionModel, QModelIndex, QRect, QSize, QTimer, Qt
|
from PySide6.QtCore import QItemSelectionModel, QModelIndex, QPoint, QPointF, QRect, QSize, QTimer, Qt
|
||||||
from PySide6.QtGui import QImage, QKeyEvent, QTextCursor
|
from PySide6.QtGui import QImage, QKeyEvent, QTextCursor, QWheelEvent
|
||||||
from PySide6.QtTest import QTest
|
from PySide6.QtTest import QTest
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QAbstractItemView,
|
QAbstractItemView,
|
||||||
@@ -43,6 +43,7 @@ from PySide6.QtWidgets import (
|
|||||||
QProgressBar,
|
QProgressBar,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
QRadioButton,
|
QRadioButton,
|
||||||
|
QSpinBox,
|
||||||
QTableView,
|
QTableView,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -74,6 +75,7 @@ from app.gui import file_manager
|
|||||||
import app.gui.workers as gui_workers
|
import app.gui.workers as gui_workers
|
||||||
from app.gui.tabs.collect import _format_collect_elapsed
|
from app.gui.tabs.collect import _format_collect_elapsed
|
||||||
from app.gui.tabs.generate import CoverGalleryDialog, OriginalImageDialog
|
from app.gui.tabs.generate import CoverGalleryDialog, OriginalImageDialog
|
||||||
|
from app.gui.tabs.settings import SettingsSpinBox
|
||||||
from app.gui.main_window import _fit_and_center_window
|
from app.gui.main_window import _fit_and_center_window
|
||||||
|
|
||||||
|
|
||||||
@@ -3189,6 +3191,85 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
|||||||
|
|
||||||
self.assert_removed(temp_dir)
|
self.assert_removed(temp_dir)
|
||||||
|
|
||||||
|
def test_settings_spin_boxes_forward_wheel_to_page_scroll(self):
|
||||||
|
with self.make_temp_dir() as temp_dir:
|
||||||
|
cfg = self.make_config(temp_dir)
|
||||||
|
cfg["ai"] = appconfig.default_config()["ai"]
|
||||||
|
tab = SettingsTab(
|
||||||
|
config=cfg,
|
||||||
|
config_path=cfg["config_path"],
|
||||||
|
ai_models_path=cfg["ai_models_path"],
|
||||||
|
)
|
||||||
|
self.addCleanup(tab.close)
|
||||||
|
tab.resize(900, 360)
|
||||||
|
tab.show()
|
||||||
|
self.app.processEvents()
|
||||||
|
|
||||||
|
spin_boxes = (
|
||||||
|
tab.cmhub_connect_timeout_spin,
|
||||||
|
tab.connect_timeout_spin,
|
||||||
|
tab.title_concurrency_spin,
|
||||||
|
tab.image_concurrency_spin,
|
||||||
|
tab.retry_spin,
|
||||||
|
tab.default_debug_port_spin,
|
||||||
|
tab.debug_port_start_spin,
|
||||||
|
tab.debug_port_end_spin,
|
||||||
|
tab.cdp_ready_timeout_spin,
|
||||||
|
tab.max_items_per_run_spin,
|
||||||
|
tab.max_parallel_accounts_spin,
|
||||||
|
)
|
||||||
|
self.assertEqual(set(spin_boxes), set(tab.findChildren(QSpinBox)))
|
||||||
|
self.assertTrue(all(isinstance(spin, SettingsSpinBox) for spin in spin_boxes))
|
||||||
|
values = {spin: spin.value() for spin in spin_boxes}
|
||||||
|
tab._set_dirty(False)
|
||||||
|
|
||||||
|
for spin in spin_boxes:
|
||||||
|
local_position = spin.rect().center()
|
||||||
|
global_position = spin.mapToGlobal(local_position)
|
||||||
|
event = QWheelEvent(
|
||||||
|
QPointF(local_position),
|
||||||
|
QPointF(global_position),
|
||||||
|
QPoint(),
|
||||||
|
QPoint(0, -120),
|
||||||
|
Qt.NoButton,
|
||||||
|
Qt.NoModifier,
|
||||||
|
Qt.ScrollUpdate,
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
QApplication.sendEvent(spin, event)
|
||||||
|
self.assertEqual(values[spin], spin.value())
|
||||||
|
|
||||||
|
self.assertFalse(tab.is_dirty())
|
||||||
|
scroll_bar = tab.settings_scroll_area.verticalScrollBar()
|
||||||
|
self.assertGreater(scroll_bar.maximum(), 0)
|
||||||
|
scroll_bar.setValue(0)
|
||||||
|
spin = tab.title_concurrency_spin
|
||||||
|
local_position = spin.rect().center()
|
||||||
|
global_position = spin.mapToGlobal(local_position)
|
||||||
|
QApplication.sendEvent(
|
||||||
|
spin,
|
||||||
|
QWheelEvent(
|
||||||
|
QPointF(local_position),
|
||||||
|
QPointF(global_position),
|
||||||
|
QPoint(),
|
||||||
|
QPoint(0, -120),
|
||||||
|
Qt.NoButton,
|
||||||
|
Qt.NoModifier,
|
||||||
|
Qt.ScrollUpdate,
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.app.processEvents()
|
||||||
|
self.assertGreater(scroll_bar.value(), 0)
|
||||||
|
|
||||||
|
before = tab.title_concurrency_spin.value()
|
||||||
|
tab.title_concurrency_spin.setFocus()
|
||||||
|
QTest.keyClick(tab.title_concurrency_spin, Qt.Key_Up)
|
||||||
|
self.assertEqual(before + 1, tab.title_concurrency_spin.value())
|
||||||
|
self.assertTrue(tab.is_dirty())
|
||||||
|
|
||||||
|
self.assert_removed(temp_dir)
|
||||||
|
|
||||||
def test_settings_tab_cmhub_alias_refresh_filters_unpriced_and_keeps_saved(self):
|
def test_settings_tab_cmhub_alias_refresh_filters_unpriced_and_keeps_saved(self):
|
||||||
with self.make_temp_dir() as temp_dir:
|
with self.make_temp_dir() as temp_dir:
|
||||||
cfg = self.make_config(temp_dir)
|
cfg = self.make_config(temp_dir)
|
||||||
|
|||||||
Reference in New Issue
Block a user