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

1"""左端の帯の ⚙ で開くポップアップ: テーマカラー(プリセット・カスタム・既定に戻す)と、環境設定への入口。 

2 

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) 

10 

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 

15 

16POPOVER_WIDTH = 318 

17SWATCH_SIZE = 34 

18 

19 

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 

31 

32 

33class _Swatch(QAbstractButton): 

34 """角丸の色の見本と、下に名前。選んでいる色は輪で囲む。""" 

35 

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)) 

44 

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() 

65 

66 

67class _ColorChip(QLabel): 

68 """小さな色の四角(プレビュー用)。""" 

69 

70 def __init__(self, parent=None): 

71 super().__init__(parent) 

72 self.setFixedSize(14, 14) 

73 

74 def set_color(self, color): 

75 self.setStyleSheet(f"background: {color}; border-radius: 4px;") 

76 

77 

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) 

93 

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) 

111 

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) 

122 

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) 

149 

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) 

169 

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) 

175 

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() 

188 

189 def current_color(self): 

190 return normalize_hex(theme.current_theme_color()) 

191 

192 def choose(self, color): 

193 self._app.set_theme_color(color) 

194 self.refresh() 

195 

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() 

213 

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()) 

220 

221 def _on_preferences(self): 

222 self.hide() 

223 self._app._on_show_preferences() 

224 

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() 

231 

232 

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) 

241