diff --git a/presentation/settings/about_widget.py b/presentation/settings/about_widget.py new file mode 100644 index 0000000..31be7d7 --- /dev/null +++ b/presentation/settings/about_widget.py @@ -0,0 +1,60 @@ +"""Mục "Giới thiệu" trong Cài đặt. + +Dòng ghi công tác giả trước đây nằm cố định ở góc dưới phải cửa sổ, dưới dạng +widget thường trực của thanh trạng thái. Chỗ đó chiếm một góc màn hình trên MỌI +màn hình, suốt cả phiên làm việc, cho một thông tin người dùng chỉ cần đọc một +lần. Chuyển vào Cài đặt: vẫn tra được bất cứ lúc nào, nhưng không còn đứng +thường trực trong tầm mắt. + +Cùng nhóm widget với bốn mục kia của Cài đặt (``general``/``provider``/ +``parameter``/``routing``) nên nó tự là một trang, không cần lắp ráp riêng. +""" +from __future__ import annotations + +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QLabel, QVBoxLayout, QWidget + +from ... import DISPLAY_NAME, __version__ +from ...i18n import tr + + +class AboutSettingsWidget(QWidget): + """Nhóm "Giới thiệu": tên ứng dụng và dòng ghi công tác giả.""" + + def __init__(self, ctx=None, parent: QWidget | None = None) -> None: + """Trang Giới thiệu. ``ctx`` không dùng tới, giữ cho khớp chữ ký của + bốn widget Cài đặt còn lại.""" + super().__init__(parent) + self.ctx = ctx + + layout = QVBoxLayout(self) + layout.setContentsMargins(4, 4, 4, 4) + layout.setSpacing(8) + + # Tên sản phẩm là danh từ riêng, không dịch — lấy thẳng từ gốc gói, + # cùng nguồn với tiêu đề cửa sổ (main_window.py:90) nên hai chỗ không lệch. + self.app_label = QLabel(f"{DISPLAY_NAME} v{__version__}") + # Không đặt màu ở đây: cỡ chữ là khác biệt duy nhất cần thiết, còn màu + # do theme quyết định (xem theme/__init__.py — ngoài theme/ không file + # nào được đặt tên một màu). + font = self.app_label.font() + font.setPointSize(font.pointSize() + 4) + font.setBold(True) + self.app_label.setFont(font) + layout.addWidget(self.app_label) + + self.credit_label = QLabel(tr("app.credit")) + self.credit_label.setObjectName("faint") + self.credit_label.setTextInteractionFlags(Qt.TextSelectableByMouse) + layout.addWidget(self.credit_label) + + layout.addStretch(1) + + def apply_to(self, data: dict) -> None: + """Không có thiết lập nào để ghi — mục này chỉ hiển thị. + + Vẫn khai để khớp giao diện chung của các trang Cài đặt: ``_save`` gọi + ``apply_to`` trên từng trang, nên một trang thiếu hàm này sẽ là + ``AttributeError`` ngay lần đầu ai đó thêm nó vào vòng lặp. + """ + return diff --git a/presentation/shell/main_window.py b/presentation/shell/main_window.py index 6397379..d0059df 100644 --- a/presentation/shell/main_window.py +++ b/presentation/shell/main_window.py @@ -163,9 +163,10 @@ class MainWindow(NavRailMixin, RailProjectMixin, TopBarMixin, self._built.append(widget is not None) self._build_nav_rail(right, rlay) - # Landing stays Workspace ▸ Project, exactly as before. Go through _goto - # so the page is actually shown — selecting the row alone only moves the - # highlight (its signals are blocked to avoid rebuild loops). + # Land on the Workspace screen. Go through _goto so the page is actually + # shown — selecting the row alone only moves the highlight (its signals + # are blocked to avoid rebuild loops). Which Workspace sub-view the user + # ends up looking at is settled after _restore_sessions(), below. self._goto(self._ROW_WORKSPACE, self.workspace.current_subtab()) self.toast = Toast(self) # top-left "task done" popup # Floating in-app Help assistant — a robot icon pinned bottom-right on @@ -176,14 +177,19 @@ class MainWindow(NavRailMixin, RailProjectMixin, TopBarMixin, self.help_agent.status_message.connect(self.statusBar().showMessage) self.statusBar().showMessage(tr("app.status.ready")) - # Author credit, pinned to the bottom-right corner. A permanent status-bar - # widget sits at the right end and is never cleared by showMessage (which - # writes on the left). - self._credit = QLabel(tr("app.credit")) - self._credit.setObjectName("faint") - self._credit.setStyleSheet("padding: 0 10px;") - self.statusBar().addPermanentWidget(self._credit) + # Dòng ghi công tác giả đã chuyển vào Cài đặt ▸ Giới thiệu + # (presentation/settings/about_widget.py). Nó từng là widget thường trực + # ở góc dưới phải: chiếm một góc màn hình trên MỌI màn hình, suốt cả + # phiên, cho một thông tin chỉ cần đọc một lần. self._restore_sessions() + # Open on "All projects…" — literally the same call the nav rail's link + # of that name makes, so the rail highlight and the content can never + # disagree. This runs AFTER the restore on purpose: _restore_sessions + # still reloads the last thread, because recovering it after a crash is + # the whole point of it, but the user should first see the list of + # everything rather than whichever conversation happened to be open when + # the app was last closed. + self.goto_all_projects() self._tray.setup() # Start the task scheduler last, once the whole window exists — it # catches up any overdue tasks right away (first tick runs inline). @@ -247,7 +253,6 @@ class MainWindow(NavRailMixin, RailProjectMixin, TopBarMixin, self._nav_toggle_btn.setText("" if self._nav_collapsed else tr("app.nav.menu_label")) self._nav_toggle_btn.setToolTip( tr("app.nav.expand_tooltip") if self._nav_collapsed else tr("app.nav.collapse_tooltip")) - self._credit.setText(tr("app.credit")) if hasattr(self, "provider_lbl"): self.provider_lbl.setText(tr("app.provider")) if hasattr(self, "settings_btn"): diff --git a/tests/ui/test_about_section.py b/tests/ui/test_about_section.py new file mode 100644 index 0000000..b96c03e --- /dev/null +++ b/tests/ui/test_about_section.py @@ -0,0 +1,75 @@ +"""Mục "Giới thiệu" trong Cài đặt, và việc gỡ dòng ghi công khỏi thanh trạng thái. + +Dòng "Made by QuanDH14" từng là widget thường trực ở góc dưới phải: chiếm một góc +màn hình trên MỌI màn hình, suốt cả phiên, cho một thông tin chỉ cần đọc một lần. +Nó chuyển vào Cài đặt ▸ Giới thiệu — vẫn tra được, không còn đứng thường trực. + +Hai đầu của việc chuyển này phải được chốt cùng nhau: chỉ thêm mục mới mà quên gỡ +chỗ cũ thì thành hiện hai lần; chỉ gỡ mà quên thêm thì mất hẳn. +""" +from __future__ import annotations + +import pytest + +from .test_settings_dialog_dac_ta import _Ctx + + +@pytest.fixture +def dialog(qapp): + from cowork_local.ui.settings_dialog import SettingsDialog + dlg = SettingsDialog(_Ctx()) + yield dlg + dlg.deleteLater() + + +# ---- đầu MỚI: mục Giới thiệu trong Cài đặt ------------------------------- + +def test_cai_dat_co_muc_gioi_thieu(dialog): + from cowork_local.i18n import tr + + labels = [dialog.section_list.item(i).text() + for i in range(dialog.section_list.count())] + assert tr("settings.group.about") in labels + + +def test_muc_gioi_thieu_dung_cuoi(dialog): + """Nó không có gì để đổi, nên đặt trước các mục thao tác được là đẩy chúng + xuống mà không được gì.""" + from cowork_local.i18n import tr + + last = dialog.section_list.item(dialog.section_list.count() - 1).text() + assert last == tr("settings.group.about") + + +def test_muc_gioi_thieu_hien_dong_ghi_cong(dialog): + from cowork_local.i18n import tr + + assert dialog._about_page.credit_label.text() == tr("app.credit") + assert "QuanDH14" in dialog._about_page.credit_label.text() + + +def test_muc_gioi_thieu_hien_ten_va_phien_ban(dialog): + from cowork_local import DISPLAY_NAME, __version__ + + text = dialog._about_page.app_label.text() + assert DISPLAY_NAME in text + assert __version__ in text + + +# ---- đầu CŨ: không còn ở thanh trạng thái -------------------------------- + +def test_khong_con_dong_ghi_cong_trong_ma_nguon_cua_so_chinh(): + """Chốt cả hai đầu: cửa sổ chính không được dựng lại widget ghi công. + + Kiểm trên mã nguồn thay vì dựng ``MainWindow`` — dựng cửa sổ thật mất ~35 + giây cho một khẳng định mà đọc file trả lời được ngay. + """ + from pathlib import Path + + src = (Path(__file__).resolve().parents[2] + / "presentation" / "shell" / "main_window.py").read_text(encoding="utf-8") + + assert "_credit" not in src, "vẫn còn widget ghi công ở cửa sổ chính" + assert "addPermanentWidget" not in src, ( + "thanh trạng thái lại có widget thường trực — nếu là thứ khác thì sửa " + "khẳng định này, nhưng phải cân nhắc: nó chiếm một góc trên mọi màn hình") diff --git a/tests/ui/test_settings_dialog_dac_ta.py b/tests/ui/test_settings_dialog_dac_ta.py index b00d9da..0c4f853 100644 --- a/tests/ui/test_settings_dialog_dac_ta.py +++ b/tests/ui/test_settings_dialog_dac_ta.py @@ -70,15 +70,21 @@ def dialog(qapp): # ---- dialog gồm những mục nào ------------------------------------------- -def test_co_dung_nam_muc(dialog): - """Năm mục thật trên màn hình. Plan R08-T07 ghi bốn widget và có một cái - tên `connector`, nhưng UI connector đã dời khỏi Settings từ trước (xem - ghi chú ở settings_dialog.py:180) — nên con số thật là năm, không bốn.""" +def test_co_dung_sau_muc(dialog): + """Sáu mục thật trên màn hình. + + Plan R08-T07 ghi bốn widget và có một cái tên `connector`, nhưng UI + connector đã dời khỏi Settings từ trước (xem ghi chú ở + settings_dialog.py:180) — nên con số thật là năm, không bốn. + + Mục thứ sáu là "Giới thiệu", thêm ngày 2026-09-07 cùng lúc với việc gỡ dòng + ghi công tác giả khỏi góc dưới phải cửa sổ. Đây là thay đổi CÓ CHỦ Ý, không + phải tách nhầm.""" dlg, _ = dialog labels = [dlg.section_list.item(i).text() for i in range(dlg.section_list.count())] - assert len(labels) == 5, labels - assert dlg.section_stack.count() == 5 + assert len(labels) == 6, labels + assert dlg.section_stack.count() == 6 def test_moi_muc_deu_bam_duoc(dialog):