Coverage for graphica/gui/shell/settings_popover.py: 89.3%
196 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
1"""左端の帯の ⚙ で開くポップアップ: テーマカラー(プリセット・カスタム・既定に戻す)と、環境設定への入口。
3色を選ぶとすぐ app.set_theme_color() で全部のタブに当てる。読みにくい色は theme_color.derive_accent() が
4明るさを動かして使うので、ライト・ダークで実際に使う色と、調整したかどうかを並べて見せる。
5"""
6from PySide6.QtCore import QPoint, QRectF, QSize, Qt
7from PySide6.QtGui import QColor, QPainter, QPen
8from PySide6.QtWidgets import (
9 QAbstractButton, QColorDialog, QFrame, QGridLayout, QHBoxLayout, QLabel, QPushButton, QToolButton, QVBoxLayout)
11from graphica.core.i18n import tr
12from graphica.gui import theme
13from graphica.gui.builders.common import _svg_icon
14from graphica.gui.theme_color import DEFAULT_THEME_COLOR, THEME_COLOR_PRESETS, derive_accent, normalize_hex
16POPOVER_WIDTH = 318
17SWATCH_SIZE = 34
20def theme_color_preview(color):
21 """(ライトで使う色, ダークで使う色, 調整の説明 or "")。"""
22 light = derive_accent(color, theme.LIGHT_TOKENS["surface"], dark=False)
23 dark = derive_accent(color, theme.DARK_TOKENS["surface"], dark=True)
24 notes = []
25 if light["adjusted"]:
26 notes.append(tr("ライトでは白い背景で見えるよう濃く"))
27 if dark["adjusted"]:
28 notes.append(tr("ダークでは暗い背景で見えるよう明るく"))
29 note = tr("、").join(notes) + tr("しています(読みやすさの自動調整)。") if notes else ""
30 return light["accent"], dark["accent"], note
33class _Swatch(QAbstractButton):
34 """角丸の色の見本と、下に名前。選んでいる色は輪で囲む。"""
36 def __init__(self, name, color, parent=None):
37 super().__init__(parent)
38 self.color = normalize_hex(color)
39 self.name = name
40 self.setCheckable(True)
41 self.setCursor(Qt.CursorShape.PointingHandCursor)
42 self.setToolTip(f"{name} {self.color}")
43 self.setFixedSize(QSize(62, SWATCH_SIZE + 26))
45 def paintEvent(self, event):
46 tokens = theme.current_tokens()
47 painter = QPainter(self)
48 painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
49 left = (self.width() - SWATCH_SIZE) / 2
50 rect = QRectF(left, 4, SWATCH_SIZE, SWATCH_SIZE)
51 if self.isChecked():
52 painter.setPen(QPen(QColor(tokens["text_primary"]), 2))
53 painter.setBrush(Qt.BrushStyle.NoBrush)
54 painter.drawRoundedRect(rect.adjusted(-3, -3, 3, 3), 11, 11)
55 painter.setPen(Qt.PenStyle.NoPen)
56 painter.setBrush(QColor(self.color))
57 painter.drawRoundedRect(rect, 9, 9)
58 painter.setPen(QColor(tokens["text_secondary"]))
59 font = painter.font()
60 font.setPointSizeF(max(1.0, font.pointSizeF() - 1))
61 painter.setFont(font)
62 painter.drawText(QRectF(0, SWATCH_SIZE + 8, self.width(), 18),
63 int(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop), self.name)
64 painter.end()
67class _ColorChip(QLabel):
68 """小さな色の四角(プレビュー用)。"""
70 def __init__(self, parent=None):
71 super().__init__(parent)
72 self.setFixedSize(14, 14)
74 def set_color(self, color):
75 self.setStyleSheet(f"background: {color}; border-radius: 4px;")
78class SettingsPopover(QFrame):
79 def __init__(self, app, parent=None):
80 super().__init__(parent, Qt.WindowType.Popup | Qt.WindowType.FramelessWindowHint)
81 self._app = app
82 # 窓の四隅は透かし、中の枠を角丸にする(窓のままだと角が四角く残る)
83 self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
84 self.setFixedWidth(POPOVER_WIDTH)
85 outer = QVBoxLayout(self)
86 outer.setContentsMargins(0, 0, 0, 0)
87 body = QFrame()
88 body.setObjectName("settings_popover")
89 outer.addWidget(body)
90 layout = QVBoxLayout(body)
91 layout.setContentsMargins(16, 14, 16, 14)
92 layout.setSpacing(0)
94 title_row = QHBoxLayout()
95 title = QLabel(tr("テーマカラー"))
96 title.setObjectName("settings_popover_title")
97 self.close_button = QToolButton()
98 self.close_button.setObjectName("card_button")
99 self.close_button.setIcon(_svg_icon("x", size=14))
100 self.close_button.setFixedSize(24, 24)
101 self.close_button.clicked.connect(self.close)
102 title_row.addWidget(title)
103 title_row.addStretch(1)
104 title_row.addWidget(self.close_button)
105 layout.addLayout(title_row)
106 subtitle = QLabel(tr("ボタン・選択・枠などに使う色です。グラフの線の色は変わりません。"))
107 subtitle.setObjectName("settings_popover_sub")
108 subtitle.setWordWrap(True)
109 layout.addWidget(subtitle)
110 layout.addSpacing(10)
112 grid = QGridLayout()
113 grid.setHorizontalSpacing(8)
114 grid.setVerticalSpacing(6)
115 self.swatches = []
116 for index, (name, color) in enumerate(THEME_COLOR_PRESETS):
117 swatch = _Swatch(tr(name), color)
118 swatch.clicked.connect(lambda _checked=False, c=swatch.color: self.choose(c))
119 grid.addWidget(swatch, index // 4, index % 4, Qt.AlignmentFlag.AlignHCenter)
120 self.swatches.append(swatch)
121 layout.addLayout(grid)
123 custom_box = QFrame()
124 custom_box.setObjectName("settings_popover_row")
125 custom_row = QHBoxLayout(custom_box)
126 custom_row.setContentsMargins(0, 12, 0, 0)
127 custom_row.setSpacing(8)
128 self.custom_button = QPushButton()
129 self.custom_button.setObjectName("settings_custom_color")
130 self.custom_button.setFixedSize(34, 30)
131 self.custom_button.setCursor(Qt.CursorShape.PointingHandCursor)
132 self.custom_button.clicked.connect(self._on_custom)
133 custom_text = QVBoxLayout()
134 custom_text.setSpacing(0)
135 custom_title = QLabel(tr("カスタム"))
136 custom_title.setObjectName("settings_popover_strong")
137 custom_sub = QLabel(tr("好きな色を選ぶ"))
138 custom_sub.setObjectName("settings_popover_sub")
139 custom_text.addWidget(custom_title)
140 custom_text.addWidget(custom_sub)
141 self.hex_label = QLabel()
142 self.hex_label.setObjectName("settings_popover_mono")
143 custom_row.addWidget(self.custom_button)
144 custom_row.addLayout(custom_text)
145 custom_row.addStretch(1)
146 custom_row.addWidget(self.hex_label)
147 layout.addSpacing(12)
148 layout.addWidget(custom_box)
150 preview = QGridLayout()
151 preview.setContentsMargins(0, 10, 0, 0)
152 preview.setHorizontalSpacing(10)
153 preview.setVerticalSpacing(6)
154 self.light_chip, self.dark_chip = _ColorChip(), _ColorChip()
155 self.light_hex, self.dark_hex = QLabel(), QLabel()
156 for row, (text, chip, hex_label) in enumerate(((tr("ライトで使う色"), self.light_chip, self.light_hex),
157 (tr("ダークで使う色"), self.dark_chip, self.dark_hex))):
158 label = QLabel(text)
159 label.setObjectName("settings_popover_label")
160 hex_label.setObjectName("settings_popover_mono")
161 value = QHBoxLayout()
162 value.setSpacing(6)
163 value.addWidget(chip)
164 value.addWidget(hex_label)
165 value.addStretch(1)
166 preview.addWidget(label, row, 0)
167 preview.addLayout(value, row, 1)
168 layout.addLayout(preview)
170 self.note_label = QLabel()
171 self.note_label.setObjectName("settings_popover_note")
172 self.note_label.setWordWrap(True)
173 layout.addSpacing(10)
174 layout.addWidget(self.note_label)
176 buttons = QHBoxLayout()
177 buttons.setContentsMargins(0, 12, 0, 0)
178 buttons.setSpacing(8)
179 self.reset_button = QPushButton(tr("既定に戻す(アイコンの色)"))
180 self.reset_button.clicked.connect(lambda: self.choose(DEFAULT_THEME_COLOR))
181 self.preferences_button = QPushButton(tr("環境設定..."))
182 self.preferences_button.clicked.connect(self._on_preferences)
183 buttons.addWidget(self.reset_button)
184 buttons.addStretch(1)
185 buttons.addWidget(self.preferences_button)
186 layout.addLayout(buttons)
187 self.refresh()
189 def current_color(self):
190 return normalize_hex(theme.current_theme_color())
192 def choose(self, color):
193 self._app.set_theme_color(color)
194 self.refresh()
196 def refresh(self):
197 color = self.current_color()
198 for swatch in self.swatches:
199 swatch.setChecked(swatch.color == color)
200 swatch.update()
201 self.custom_button.setStyleSheet(
202 f"background: {color}; border: 1px solid {theme.current_tokens()['border_strong']}; border-radius: 7px;")
203 self.hex_label.setText(color)
204 light, dark, note = theme_color_preview(color)
205 self.light_chip.set_color(light)
206 self.dark_chip.set_color(dark)
207 self.light_hex.setText(light)
208 self.dark_hex.setText(dark)
209 self.note_label.setText(note)
210 self.note_label.setVisible(bool(note))
211 self.close_button.setIcon(_svg_icon("x", size=14))
212 self.adjustSize()
214 def _on_custom(self):
215 # 色の画面を開くとポップアップは閉じる(フォーカスが外れる)ので、選び終えてから当てる
216 self.hide()
217 chosen = QColorDialog.getColor(QColor(self.current_color()), self._app, tr("テーマカラー"))
218 if chosen.isValid(): 218 ↛ exitline 218 didn't return from function '_on_custom' because the condition on line 218 was always true
219 self._app.set_theme_color(chosen.name())
221 def _on_preferences(self):
222 self.hide()
223 self._app._on_show_preferences()
225 def open_beside(self, anchor):
226 """anchor(帯の ⚙)の右、下端をそろえて開く。"""
227 self.refresh()
228 bottom_right = anchor.mapToGlobal(QPoint(anchor.width() + 8, anchor.height()))
229 self.move(bottom_right.x(), bottom_right.y() - self.sizeHint().height())
230 self.show()
233def toggle_settings_popover(app, anchor):
234 popover = getattr(app, 'settings_popover', None)
235 if popover is None:
236 popover = app.settings_popover = SettingsPopover(app, app)
237 if popover.isVisible():
238 popover.hide()
239 else:
240 popover.open_beside(anchor)