Files
cowork-local/ui/segmented_control.py
T
f9f6bc01fd
CI / test (push) Canceled after 0s
Feature/delta team/epic r04 (#7)
## Summary

epic r04 - begin refactor

## Change Type

- [x] Cowork feature
- [ ] Bug fix
- [ ] Core AI contribution
- [ ] Test / hardening
- [ ] Performance
- [ ] Documentation

## Related Work

Cowork Task:

Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets

Core AI Issue:

Core Task:

Related PR:

## Scope

What is intentionally included?

What is intentionally NOT included?

## Validation

- [ ] Unit tests
- [ ] Integration tests
- [ ] Manual verification
- [ ] Regression check

Commands / evidence:

## Security Impact

Permission / credential / network / customer data impact:

## Compatibility

- [ ] No breaking change
- [ ] Breaking change documented

## Reviewer Notes

Anything Cowork reviewers should pay attention to.

---------

Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com>
Co-authored-by: Huong Le Thi Thien <huongltt35@fpt.com>
Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com>
Co-authored-by: Vu Dam Tuan <vudt15@fpt.com>
Co-authored-by: Hiep Ha Van <hiephv3@fpt.com>
Co-authored-by: Lam Hoang Van <lamhv7@fpt.com>
Reviewed-on: #7
Co-authored-by: Duy Le Huu <duylh19@fpt.com>
2026-08-31 05:15:13 +00:00

135 lines
6.0 KiB
Python

"""Dải nút chọn một trong nhiều — tách khỏi ``ui/widgets.py``.
Thay ``QComboBox`` ở những chỗ chỉ có hai đến bốn lựa chọn và người dùng nên
thấy hết cùng lúc: ngôn ngữ và giao diện trong Cài đặt. Mở một danh sách xổ
xuống chỉ để biết trong đó có gì là một cú bấm thừa.
Tách ra vì hai lẽ. Một, ``ui/widgets.py`` đã chạm đúng trần nợ cũ của cổng LOC
nên không nhận thêm được dòng nào. Hai, chỗ này có một luật riêng đáng đứng
một mình: bề rộng nút phải chừa sẵn cho chữ IN ĐẬM — xem
:meth:`SegmentedControl._reserve_bold_width`.
"""
from __future__ import annotations
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QFont, QFontMetrics
from PySide6.QtWidgets import QHBoxLayout, QPushButton, QWidget
class SegmentedControl(QWidget):
"""Two-to-four choices shown side by side instead of hidden in a drop-list.
Exposes the slice of the QComboBox API this app's settings code uses
(addItem / findData / currentData / setCurrentIndex / currentIndexChanged),
so it drops into an existing form without touching the save/load paths.
"""
currentIndexChanged = Signal(int)
#: Độ đậm mà ``theme/qss.py`` áp cho nút đang chọn
#: (``QPushButton#segItem:checked { font-weight: 600 }``). Đổi ở QSS thì
#: phải đổi cả ở đây, nếu không chữ lại bị cắt.
_CHECKED_WEIGHT = QFont.DemiBold
def __init__(self, parent=None):
"""Dải nút chọn một trong nhiều — thay ``QComboBox`` khi chỉ có vài lựa chọn và
nên thấy hết cùng lúc.
"""
super().__init__(parent)
self._data: list = []
self._buttons: list = []
self._current = -1
lay = QHBoxLayout(self)
lay.setContentsMargins(0, 0, 0, 0)
lay.setSpacing(0)
self._lay = lay
lay.addStretch(1)
def addItem(self, text: str, data=None) -> None: # noqa: N802 - Qt-style name
"""Thêm một lựa chọn kèm dữ liệu đi kèm."""
btn = QPushButton(text)
btn.setObjectName("segItem")
btn.setCheckable(True)
btn.setCursor(Qt.PointingHandCursor)
index = len(self._buttons)
btn.clicked.connect(lambda _c=False, i=index: self.setCurrentIndex(i))
self._lay.insertWidget(index, btn)
self._buttons.append(btn)
self._data.append(data)
self._reserve_bold_width(btn)
if self._current < 0:
self.setCurrentIndex(0)
@staticmethod
def _reserve_bold_width(btn: QPushButton) -> None:
"""Chừa sẵn bề rộng cho chữ khi nút được chọn và bị in đậm.
``QPushButton`` tính ``sizeHint()`` theo phông ĐANG dùng, tức phông
thường. Nhưng QSS lại đặt ``font-weight: 600`` cho nút đang chọn, và
chữ đậm rộng hơn chữ thường — nên đúng lúc một mục được chọn thì nó
không còn đủ chỗ và Qt cắt bớt chữ đi.
Nhãn càng dài, thiếu càng nhiều: đo trên bản 30/08 thì "Tiếng Việt"
thiếu 2px, "English" 2px, còn "Tự động (theo hệ thống)" thiếu tới 7px.
Tiếng Việt lộ rõ nhất vì nó vừa là nhãn dài vừa có dấu, và với người
dùng tiếng Việt thì nó luôn là mục ĐANG được chọn.
Cách đo: lấy phần khung (viền + padding do QSS quy định) bằng cách trừ
bề rộng chữ khỏi ``sizeHint()``, rồi cộng lại bề rộng của chính chữ ấy
ở độ đậm khi được chọn. Không viết cứng con số padding nào — QSS đổi
thì phép đo tự theo.
"""
btn.ensurePolished()
text = btn.text()
normal = btn.font()
chrome = btn.sizeHint().width() - QFontMetrics(normal).horizontalAdvance(text)
bold = QFont(normal)
bold.setWeight(SegmentedControl._CHECKED_WEIGHT)
btn.setMinimumWidth(chrome + QFontMetrics(bold).horizontalAdvance(text))
def findData(self, value) -> int: # noqa: N802
"""Chỉ số của lựa chọn mang dữ liệu ``value``; -1 nếu không có."""
return self._data.index(value) if value in self._data else -1
def currentData(self): # noqa: N802
"""Dữ liệu của lựa chọn đang chọn; ``None`` nếu chưa chọn gì."""
return self._data[self._current] if 0 <= self._current < len(self._data) else None
def currentIndex(self) -> int: # noqa: N802
"""Chỉ số lựa chọn đang chọn; -1 nếu chưa chọn gì."""
return self._current
def count(self) -> int:
"""Số lựa chọn đang có."""
return len(self._buttons)
def setItemText(self, index: int, text: str) -> None: # noqa: N802
"""Đổi nhãn một lựa chọn (dùng khi đổi ngôn ngữ).
Tính lại bề rộng tối thiểu: nhãn mới dài ngắn khác nhau, giữ nguyên số
cũ thì hoặc cắt chữ hoặc chừa một khoảng trống vô cớ.
"""
if 0 <= index < len(self._buttons):
btn = self._buttons[index]
btn.setText(text)
btn.setMinimumWidth(0)
self._reserve_bold_width(btn)
def setCurrentIndex(self, index: int) -> None: # noqa: N802
"""Chọn một mục và phát tín hiệu đổi.
Chỉ số không hợp lệ hoặc trùng mục đang chọn thì chỉ đồng bộ lại trạng thái
nút, không phát tín hiệu — tránh vòng lặp khi chỗ gọi lại đặt lại chỉ số.
"""
if not (0 <= index < len(self._buttons)) or index == self._current:
for i, b in enumerate(self._buttons):
b.setChecked(i == self._current)
return
self._current = index
for i, b in enumerate(self._buttons):
b.setChecked(i == index)
self.currentIndexChanged.emit(index)
__all__ = ["SegmentedControl"]