Skip to content

Commit e6d9442

Browse files
committed
ui: 优化平台收藏按钮的点击反馈
1 parent 90bb7ae commit e6d9442

4 files changed

Lines changed: 119 additions & 11 deletions

File tree

tests/test_provider_i18n.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ def test_provider_picker_text_and_numbered_count_are_localized(monkeypatch, lang
8181
"全部平台", "已配置", "常用平台", "未配置", "API 用量", "AI 平台列表", "删除", "已移除",
8282
"没有匹配的平台,试试其他名称或查看全部平台",
8383
"Enter 切换 · Ctrl+D 收藏 · Esc 关闭",
84+
"收藏", "取消收藏", "已收藏 {provider}", "已取消收藏 {provider}",
8485
)
8586
for source in sources:
8687
assert source in PROVIDER_MESSAGES

tests/test_provider_picker.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,57 @@ def test_star_click_toggles_favorite_without_activating_card(picker, picker_stat
238238
assert "elevenlabs" in state["pinned_providers"]
239239
assert pins_changed.count() == 1 and activated.count() == 0
240240
assert picker.currentData() == "codex" and picker.popup.isVisible()
241+
assert picker.hint.text() == "已收藏 ElevenLabs"
242+
QTest.mouseClick(picker.grid.viewport(), Qt.MouseButton.LeftButton, pos=star_center)
243+
assert picker.hint.text() == "已取消收藏 ElevenLabs"
244+
245+
246+
@pytest.mark.parametrize("theme", ["light", "dark"])
247+
def test_star_hover_press_and_drag_out_feedback(picker, picker_state, theme, tmp_path):
248+
configure_theme(APP, theme)
249+
open_all(picker)
250+
picker.search.setText("elevenlabs")
251+
APP.processEvents()
252+
viewport = picker.grid.viewport()
253+
rect = picker.grid.visualItemRect(picker.grid.item(0))
254+
star_center = QPoint(rect.right() - 23, rect.top() + 26)
255+
QTest.mouseMove(viewport, rect.center())
256+
normal = viewport.grab().toImage()
257+
QTest.mouseMove(viewport, star_center)
258+
hovered = viewport.grab().toImage()
259+
assert viewport.cursor().shape() == Qt.CursorShape.PointingHandCursor
260+
assert hovered != normal
261+
QTest.mousePress(viewport, Qt.MouseButton.LeftButton, pos=star_center)
262+
pressed = viewport.grab().toImage()
263+
assert pressed != hovered
264+
picker.popup.grab().save(str(tmp_path / f"favorite-{theme}-pressed.png"))
265+
QTest.mouseMove(viewport, rect.center())
266+
QTest.mouseRelease(viewport, Qt.MouseButton.LeftButton, pos=rect.center())
267+
assert "elevenlabs" not in provider_picker.pinned_provider_ids()
268+
assert picker.currentData() == "codex" and picker.popup.isVisible()
269+
assert viewport.grab().toImage() == normal
270+
271+
272+
def test_star_release_without_press_does_not_toggle(picker):
273+
open_all(picker)
274+
picker.search.setText("elevenlabs")
275+
rect = picker.grid.visualItemRect(picker.grid.item(0))
276+
QTest.mouseRelease(picker.grid.viewport(), Qt.MouseButton.LeftButton,
277+
pos=QPoint(rect.right() - 23, rect.top() + 26))
278+
assert "elevenlabs" not in provider_picker.pinned_provider_ids()
279+
280+
281+
def test_disabled_star_explains_why_it_cannot_be_pinned(picker, monkeypatch):
282+
monkeypatch.setattr(provider_picker.config_manager, "get",
283+
lambda key, default=None: ["elevenlabs"] if key == "DISABLED_PROVIDER_IDS" else default)
284+
open_all(picker)
285+
picker.search.setText("elevenlabs")
286+
rect = picker.grid.visualItemRect(picker.grid.item(0))
287+
QTest.mouseClick(picker.grid.viewport(), Qt.MouseButton.LeftButton,
288+
pos=QPoint(rect.right() - 23, rect.top() + 26))
289+
assert "elevenlabs" not in provider_picker.pinned_provider_ids()
290+
assert "重新启用" in picker.hint.text()
291+
assert picker.popup.isVisible()
241292

242293

243294
def test_shortcuts_show_three_pins_and_emit_correct_id(picker_state):

ui/provider_picker.py

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
QStyle,
2222
QStyledItemDelegate,
2323
QToolButton,
24+
QToolTip,
2425
QVBoxLayout,
2526
QWidget,
2627
)
@@ -48,6 +49,14 @@ def pinned_provider_ids() -> list[str]:
4849

4950

5051
class _ProviderCardDelegate(QStyledItemDelegate):
52+
hovered_pin = None
53+
pressed_pin = None
54+
55+
@staticmethod
56+
def pin_rect(rect):
57+
# 绘制和命中检测共用 40px 热区,避免只有星形细线附近才能明确操作。
58+
return QRectF(rect).adjusted(rect.width() - 44, 6, -4, -rect.height() + 46)
59+
5160
def paint(self, painter, option, index) -> None:
5261
provider_id, connected, pinned = index.data(Qt.ItemDataRole.UserRole)
5362
tokens = current_theme()
@@ -66,14 +75,22 @@ def paint(self, painter, option, index) -> None:
6675
icon_rect.setSize(QSize(26, 26))
6776
provider_icon(provider_id, 26).paint(painter, icon_rect)
6877
# 收藏采用矢量星形;品牌标记始终来自独立的品牌 SVG 资产。
69-
center = QPointF(rect.right() - 19, rect.top() + 22)
78+
star_rect = self.pin_rect(option.rect)
79+
hovered_pin = self.hovered_pin == provider_id
80+
pressed_pin = hovered_pin and self.pressed_pin == provider_id
81+
if pinned or hovered_pin:
82+
painter.setPen(QPen(QColor(tokens.accent), 1) if hovered_pin else Qt.PenStyle.NoPen)
83+
painter.setBrush(QColor(tokens.accent if pressed_pin else tokens.accent_soft))
84+
painter.drawRoundedRect(star_rect.adjusted(3, 3, -3, -3), 7, 7)
85+
center = star_rect.center()
7086
points = QPolygonF([
7187
QPointF(center.x() + (7 if n % 2 == 0 else 3.1) * cos(-pi / 2 + n * pi / 5),
7288
center.y() + (7 if n % 2 == 0 else 3.1) * sin(-pi / 2 + n * pi / 5))
7389
for n in range(10)
7490
])
75-
painter.setPen(QPen(QColor(tokens.accent if pinned else tokens.subtext), 1.1))
76-
painter.setBrush(QColor(tokens.accent) if pinned else Qt.BrushStyle.NoBrush)
91+
star_color = QColor(tokens.window if pressed_pin else tokens.accent if pinned or hovered_pin else tokens.subtext)
92+
painter.setPen(QPen(star_color, 1.4))
93+
painter.setBrush(star_color if pinned else Qt.BrushStyle.NoBrush)
7794
painter.drawPolygon(points)
7895
painter.setFont(option.font)
7996
painter.setPen(QColor(tokens.value))
@@ -196,6 +213,9 @@ def _set_filter(self, selected: str) -> None:
196213
self._populate()
197214

198215
def _populate(self, *_args) -> None:
216+
self.grid.itemDelegate().hovered_pin = None
217+
self.grid.itemDelegate().pressed_pin = None
218+
self._grid_viewport.unsetCursor()
199219
query = self.search.text().strip().casefold()
200220
if query and self._filter != "all":
201221
# 搜索面向所有可接入平台,不能因默认的已配置筛选而隐藏用户正在找的品牌。
@@ -352,7 +372,10 @@ def _toggle_pin(self, item: QListWidgetItem) -> None:
352372
if self._pins != pins:
353373
bind_text(self.hint, "常用平台保存失败,请检查数据目录")
354374
return
355-
bind_text(self.hint, "Enter 切换 · Ctrl+D 收藏 · Esc 关闭")
375+
bind_text(self.hint, lambda: tr(
376+
"已收藏 {provider}" if provider_id in pins else "已取消收藏 {provider}",
377+
provider=provider_short_name(provider_id),
378+
))
356379
self.pins_changed.emit()
357380

358381
def eventFilter(self, watched, event) -> bool:
@@ -370,17 +393,40 @@ def eventFilter(self, watched, event) -> bool:
370393
if watched is self._grid_viewport and event.type() == QEvent.Type.Resize:
371394
self.grid.doItemsLayout()
372395
self._position_remove_buttons()
396+
if watched is self._grid_viewport and event.type() == QEvent.Type.Leave:
397+
self.grid.itemDelegate().hovered_pin = None
398+
self._grid_viewport.unsetCursor()
399+
self._grid_viewport.update()
373400
if watched is self._grid_viewport and event.type() in (
401+
QEvent.Type.MouseMove, QEvent.Type.ToolTip,
374402
QEvent.Type.MouseButtonPress, QEvent.Type.MouseButtonRelease,
375-
) and event.button() == Qt.MouseButton.LeftButton:
376-
item = self.grid.itemAt(event.position().toPoint())
377-
if item is not None:
378-
rect = self.grid.visualItemRect(item)
379-
star = rect.adjusted(rect.width() - 44, 5, -4, -rect.height() + 44)
380-
if star.contains(event.position().toPoint()):
381-
if event.type() == QEvent.Type.MouseButtonRelease:
403+
):
404+
position = event.pos() if event.type() == QEvent.Type.ToolTip else event.position().toPoint()
405+
item = self.grid.itemAt(position)
406+
delegate = self.grid.itemDelegate()
407+
over_pin = item is not None and delegate.pin_rect(self.grid.visualItemRect(item)).contains(position)
408+
provider_id = item.data(Qt.ItemDataRole.UserRole)[0] if over_pin else None
409+
delegate.hovered_pin = provider_id
410+
self._grid_viewport.setCursor(Qt.CursorShape.PointingHandCursor if over_pin else Qt.CursorShape.ArrowCursor)
411+
self._grid_viewport.update()
412+
if event.type() == QEvent.Type.ToolTip and over_pin:
413+
label = "请先在全部平台中重新启用此平台。" if provider_id in self._disabled else "取消收藏" if provider_id in self._pins else "收藏"
414+
QToolTip.showText(event.globalPos(), tr(label), self._grid_viewport)
415+
return True
416+
if event.type() == QEvent.Type.MouseButtonPress and event.button() == Qt.MouseButton.LeftButton:
417+
delegate.pressed_pin = provider_id
418+
if over_pin:
419+
return True
420+
if event.type() == QEvent.Type.MouseButtonRelease and event.button() == Qt.MouseButton.LeftButton:
421+
pressed_pin = delegate.pressed_pin
422+
delegate.pressed_pin = None
423+
# 必须在同一收藏热区按下并松开;拖出按钮时取消,不能误收藏或切换平台。
424+
if pressed_pin is not None:
425+
if pressed_pin == provider_id:
382426
self._toggle_pin(item)
383427
return True
428+
if over_pin:
429+
return True
384430
if event.type() == QEvent.Type.KeyPress:
385431
if (watched is self.grid or watched in self.remove_buttons.values()) and event.key() == Qt.Key.Key_Delete:
386432
self._remove_selected()

ui/provider_translations.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,16 @@
121121
"已连接": ("Connected", "已連線", "接続済み", "연결됨"),
122122
"已配置": ("Configured", "已設定", "設定済み", "설정됨"),
123123
"常用平台": ("Favorites", "常用平台", "お気に入り", "즐겨찾기"),
124+
"收藏": ("Add to favorites", "收藏", "お気に入りに追加", "즐겨찾기에 추가"),
125+
"取消收藏": ("Remove from favorites", "取消收藏", "お気に入りから削除", "즐겨찾기에서 제거"),
126+
"已收藏 {provider}": (
127+
"Added {provider} to favorites", "已收藏 {provider}",
128+
"{provider} をお気に入りに追加しました", "{provider} 즐겨찾기에 추가됨",
129+
),
130+
"已取消收藏 {provider}": (
131+
"Removed {provider} from favorites", "已取消收藏 {provider}",
132+
"{provider} をお気に入りから削除しました", "{provider} 즐겨찾기에서 제거됨",
133+
),
124134
"未配置": ("Not configured", "未設定", "未設定", "설정되지 않음"),
125135
"API 用量": ("API usage", "API 用量", "API 使用量", "API 사용량"),
126136
"AI 平台列表": ("AI provider list", "AI 平台清單", "AI プロバイダー一覧", "AI 공급업체 목록"),

0 commit comments

Comments
 (0)