Coverage for graphica/gui/shell/dataset_page.py: 100.0%

198 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-12 16:38 +0000

1"""左パネルの「データセット」のページ。今の一覧・検索欄・操作のボタンを、中央の下から移して並べ直す。 

2 

3見本(docs/dev/mockups/gui_redesign/)の並び: 見出し(件数・フォルダ・+追加)、検索欄、一覧、選んでいる 

4データセットの統計カード(2段目はアドバンスドだけ)、下に操作のボタンの行。 

5""" 

6import numpy as np 

7from PySide6.QtCore import QSize, Qt, QTimer 

8from PySide6.QtGui import QAction 

9from PySide6.QtWidgets import (QFrame, QGridLayout, QHBoxLayout, QLabel, QSizePolicy, QTreeWidgetItemIterator, 

10 QVBoxLayout, QWidget) 

11 

12from graphica.core.analysis import sample_standard_deviation 

13from graphica.core.i18n import tr 

14from graphica.gui.dataset_style_icon import make_dataset_style_icon 

15from graphica.gui.shell.icons import themed_icon 

16from graphica.gui.widgets import StatCard 

17 

18LEFT_PANEL_WIDTH = 280 

19# ボタンの行に残すもの(左から)。曲線フィット・ピーク検出・多峰フィット・新規作成は「…」のメニューへ 

20FOOT_BUTTONS = ( 

21 ("view_edit_data_button", "table"), 

22 ("duplicate_dataset_button", "copy"), 

23 ("auto_color_button", "palette"), 

24) 

25OVERFLOW_BUTTON_ACTIONS = ( 

26 ("new_dataset_button", "新規作成..."), 

27 None, 

28 ("fit_curve_button", "曲線フィット..."), 

29 ("find_peaks_button", "ピーク検出..."), 

30 ("multi_peak_fit_button", "多峰フィット..."), 

31) 

32 

33 

34def _ghost(button, icon_name, size=30): 

35 """枠の無いアイコンのボタンにする(今のボタンをそのまま使う)。""" 

36 button.setProperty("iconOnly", True) 

37 button.setProperty("ghostButton", True) 

38 button.setText("") 

39 button.setFixedSize(size, size) 

40 button.setIconSize(QSize(18, 18)) 

41 button.setIcon(themed_icon(icon_name)) 

42 button.setProperty("iconName", icon_name) 

43 button.style().unpolish(button) 

44 button.style().polish(button) 

45 return button 

46 

47 

48def _take_all(layout): 

49 """レイアウトから部品を全部外す(部品は消さない。区切りの線は消す)。""" 

50 while layout.count(): 

51 item = layout.takeAt(0) 

52 widget = item.widget() 

53 if widget is not None and widget.objectName() == "button_row_separator": 

54 widget.deleteLater() 

55 

56 

57def _dataset_items(tree): 

58 iterator = QTreeWidgetItemIterator(tree) 

59 while iterator.value(): 

60 item = iterator.value() 

61 if item.data(0, Qt.ItemDataRole.UserRole) is not None: 

62 yield item 

63 iterator += 1 

64 

65 

66class DatasetPage(QWidget): 

67 def __init__(self, app, parent=None): 

68 super().__init__(parent) 

69 self.setObjectName("dataset_page") 

70 self._app = app 

71 tree = app.ui.dataset_list_widget 

72 old_container = tree.parentWidget() 

73 app.ui.gridLayout_2.removeWidget(old_container) 

74 _take_all(old_container.layout()) 

75 old_container.deleteLater() 

76 _take_all(app.ui.horizontalLayout_3) 

77 app.ui.gridLayout_2.removeItem(app.ui.horizontalLayout_3) 

78 

79 layout = QVBoxLayout(self) 

80 layout.setContentsMargins(0, 0, 0, 0) 

81 layout.setSpacing(0) 

82 

83 # 見出し 

84 head = QHBoxLayout() 

85 head.setContentsMargins(14, 0, 10, 0) 

86 head.setSpacing(6) 

87 title = QLabel(tr("データセット")) 

88 title.setObjectName("panel_title") 

89 self.count_label = QLabel("0") 

90 self.count_label.setObjectName("count_pill") 

91 self.count_label.setFixedHeight(18) 

92 self.count_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 

93 head.addWidget(title) 

94 head.addWidget(self.count_label) 

95 head.addStretch(1) 

96 head.addWidget(_ghost(app.new_folder_button, "folder-plus", size=28)) 

97 add_button = app.ui.add_dataset_button 

98 add_button.setProperty("iconOnly", False) 

99 add_button.setProperty("primaryButton", True) 

100 add_button.setText(tr("追加")) 

101 add_button.setMinimumSize(0, 0) 

102 add_button.setMaximumSize(16777215, 16777215) 

103 add_button.setIconSize(QSize(14, 14)) 

104 add_button.setIcon(themed_icon("plus", size=14, token="on_accent")) 

105 add_button.style().unpolish(add_button) 

106 add_button.style().polish(add_button) 

107 head.addWidget(add_button) 

108 head_box = QWidget() 

109 head_box.setFixedHeight(52) 

110 head_box.setLayout(head) 

111 layout.addWidget(head_box) 

112 

113 search_row = QHBoxLayout() 

114 search_row.setContentsMargins(12, 0, 12, 8) 

115 search_row.addWidget(app.dataset_search_edit) 

116 layout.addLayout(search_row) 

117 

118 tree_row = QHBoxLayout() 

119 tree_row.setContentsMargins(8, 0, 8, 4) 

120 # 余った高さは一覧に回す(カードや見出しは中身の高さのまま) 

121 tree.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) 

122 tree_row.addWidget(tree) 

123 layout.addLayout(tree_row, 1) 

124 

125 # 選んでいるデータセットの統計 

126 stats_head = QHBoxLayout() 

127 stats_head.setContentsMargins(14, 6, 14, 6) 

128 stats_head.setSpacing(6) 

129 self.stats_swatch = QLabel() 

130 self.stats_name = QLabel("-") 

131 self.stats_name.setObjectName("stats_target_label") 

132 stats_head.addWidget(self.stats_swatch) 

133 stats_head.addWidget(self.stats_name, 1) 

134 layout.addLayout(stats_head) 

135 self.cards = {} 

136 layout.addWidget(self._card_row((("n", "点の数"), ("mean", "平均"), ("sd", "標準偏差")))) 

137 app.dataset_stats_advanced_row = self._card_row((("min", "最小"), ("max", "最大"), ("missing", "欠損"))) 

138 layout.addWidget(app.dataset_stats_advanced_row) 

139 layout.addSpacing(6) 

140 

141 # 操作のボタンの行 

142 foot = QFrame() 

143 foot.setObjectName("panel_foot") 

144 foot_layout = QHBoxLayout(foot) 

145 foot_layout.setContentsMargins(8, 6, 8, 6) 

146 foot_layout.setSpacing(2) 

147 for attr, icon_name in FOOT_BUTTONS: 

148 foot_layout.addWidget(_ghost(getattr(app, attr), icon_name)) 

149 foot_layout.addStretch(1) 

150 overflow = app.dataset_overflow_button 

151 overflow.setText("") 

152 overflow.setProperty("ghostButton", True) 

153 overflow.setFixedSize(30, 30) 

154 overflow.setIconSize(QSize(18, 18)) 

155 overflow.setIcon(themed_icon("dots")) 

156 overflow.setProperty("iconName", "dots") 

157 overflow.setStyleSheet("QToolButton::menu-indicator { image: none; width: 0; }") 

158 self._extend_overflow_menu(overflow.menu()) 

159 foot_layout.addWidget(overflow) 

160 foot_layout.addWidget(_ghost(app.ui.remove_dataset_button, "trash")) 

161 layout.addWidget(foot) 

162 

163 # 移さなかったボタン(…のメニューから押す)は見せないだけで残す(シグナルとテストはそのまま) 

164 for entry in OVERFLOW_BUTTON_ACTIONS: 

165 if entry is not None: 

166 button = getattr(app, entry[0]) 

167 button.setParent(self) 

168 button.hide() 

169 

170 # ダークモードの切り替えでアイコンを作り直す表(_refresh_custom_svg_icons)を、移したボタンの形に合わせる 

171 icons = app._dataset_action_button_icons 

172 for attr, icon_name in FOOT_BUTTONS: 

173 button = getattr(app, attr) 

174 icons[button] = (icon_name, icons.get(button, (icon_name, ""))[1]) 

175 icons.pop(add_button, None) 

176 

177 self.setFixedWidth(LEFT_PANEL_WIDTH) 

178 self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding) 

179 

180 self._count_timer = QTimer(self) 

181 self._count_timer.setSingleShot(True) 

182 self._count_timer.timeout.connect(self.refresh_count) 

183 model = tree.model() 

184 for signal in (model.rowsInserted, model.rowsRemoved, model.modelReset): 

185 signal.connect(lambda *args: self._count_timer.start(0)) 

186 self.refresh_count() 

187 self.set_stats(None) 

188 

189 def _card_row(self, entries): 

190 row = QWidget() 

191 row.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) 

192 grid = QGridLayout(row) 

193 grid.setContentsMargins(12, 0, 12, 6) 

194 grid.setSpacing(6) 

195 for column, (key, label) in enumerate(entries): 

196 card = StatCard(tr(label)) 

197 self.cards[key] = card 

198 grid.addWidget(card, 0, column) 

199 return row 

200 

201 def _extend_overflow_menu(self, menu): 

202 """今の「…」のメニューの頭に、ボタンの行から外した操作を足す(押すと元のボタンを押したのと同じ)。""" 

203 first = menu.actions()[0] if menu.actions() else None 

204 self._overflow_actions = [] 

205 for entry in OVERFLOW_BUTTON_ACTIONS: 

206 if entry is None: 

207 action = menu.insertSeparator(first) 

208 else: 

209 attr, text = entry 

210 button = getattr(self._app, attr) 

211 action = QAction(tr(text), menu) 

212 menu.insertAction(first, action) 

213 action.triggered.connect(button.click) 

214 self._overflow_actions.append((action, button)) 

215 menu.insertSeparator(first) 

216 # ボタンの有効・無効は選択で変わるので、開くたびに写す 

217 menu.aboutToShow.connect(self._sync_overflow_enabled) 

218 

219 def _sync_overflow_enabled(self): 

220 for action, button in self._overflow_actions: 

221 action.setEnabled(button.isEnabled()) 

222 

223 def refresh_count(self): 

224 self.count_label.setText(str(sum(1 for _ in _dataset_items(self._app.ui.dataset_list_widget)))) 

225 

226 def set_stats(self, dataset): 

227 """選んでいるデータセットの Y の統計(NaN は数えない)。None なら全部「-」。""" 

228 values = dict.fromkeys(("n", "mean", "sd", "min", "max", "missing"), "-") 

229 if dataset is None: 

230 self.stats_name.setText("-") 

231 self.stats_swatch.clear() 

232 else: 

233 self.stats_name.setText(dataset.name) 

234 self.stats_swatch.setPixmap(make_dataset_style_icon(dataset).pixmap(QSize(16, 10))) 

235 try: 

236 y = np.asarray(dataset.y_data, dtype=float) 

237 valid = y[~np.isnan(y)] 

238 values["missing"] = str(int(len(y) - len(valid))) 

239 if len(valid): 

240 std = sample_standard_deviation(valid) 

241 values.update(n=str(len(valid)), mean=f"{np.mean(valid):.4g}", 

242 sd="-" if np.isnan(std) else f"{std:.4g}", 

243 min=f"{np.min(valid):.4g}", max=f"{np.max(valid):.4g}") 

244 except (TypeError, ValueError): 

245 pass 

246 for key, card in self.cards.items(): 

247 card.set_value(values[key]) 

248 

249 def refresh_icons(self): 

250 app = self._app 

251 for attr, icon_name in FOOT_BUTTONS: 

252 getattr(app, attr).setIcon(themed_icon(icon_name)) 

253 app.new_folder_button.setIcon(themed_icon("folder-plus")) 

254 app.ui.remove_dataset_button.setIcon(themed_icon("trash")) 

255 app.dataset_overflow_button.setIcon(themed_icon("dots")) 

256 app.ui.add_dataset_button.setIcon(themed_icon("plus", size=14, token="on_accent"))