Coverage for graphica/gui/shell/side_pages.py: 96.8%

173 statements  

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

1"""左パネルの「解析」「注釈」「レイアウト」「プラグイン」のページ。 

2 

3まずは今の操作を節ごとのボタンの並びで出す(GUI 刷新の決定 6。中身の作り込みは刷新の後)。押したときの動きは 

4メニューやボタンと同じ(同じ QAction か、同じメソッドを呼ぶ)。 

5""" 

6from PySide6.QtCore import QSize, Qt 

7from PySide6.QtWidgets import (QComboBox, QLabel, QPushButton, QScrollArea, QSizePolicy, QStackedWidget, 

8 QVBoxLayout, QWidget) 

9 

10from graphica.core.i18n import tr 

11from graphica.gui.datasets.operations.processing import PROCESSING_OPERATIONS 

12from graphica.gui.shell.dataset_page import LEFT_PANEL_WIDTH 

13 

14 

15class ActionListPage(QWidget): 

16 """見出しと、節ごとのボタンの並び。""" 

17 

18 def __init__(self, title, parent=None): 

19 super().__init__(parent) 

20 self.setObjectName("side_page") 

21 self.setFixedWidth(LEFT_PANEL_WIDTH) 

22 outer = QVBoxLayout(self) 

23 outer.setContentsMargins(0, 0, 0, 0) 

24 outer.setSpacing(0) 

25 heading = QLabel(tr(title)) 

26 heading.setObjectName("panel_title") 

27 heading.setFixedHeight(52) 

28 heading.setContentsMargins(14, 0, 0, 0) 

29 outer.addWidget(heading) 

30 scroll = QScrollArea() 

31 scroll.setObjectName("side_page_scroll") 

32 scroll.setWidgetResizable(True) 

33 scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) 

34 body = QWidget() 

35 body.setObjectName("side_page_body") 

36 self._body = QVBoxLayout(body) 

37 self._body.setContentsMargins(8, 0, 8, 12) 

38 self._body.setSpacing(2) 

39 self._body.addStretch(1) 

40 scroll.setWidget(body) 

41 outer.addWidget(scroll, 1) 

42 self.rows = [] 

43 

44 def _insert(self, widget): 

45 self._body.insertWidget(self._body.count() - 1, widget) 

46 

47 def add_section(self, title): 

48 label = QLabel(tr(title)) 

49 label.setObjectName("side_page_section") 

50 self._insert(label) 

51 return label 

52 

53 def _row_button(self): 

54 # QToolButton は文字を左に寄せられない(QSS の text-align が効かない)ので QPushButton にする 

55 button = QPushButton() 

56 button.setObjectName("side_page_row") 

57 button.setIconSize(QSize(16, 16)) 

58 button.setCursor(Qt.CursorShape.PointingHandCursor) 

59 button.setFocusPolicy(Qt.FocusPolicy.NoFocus) 

60 button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) 

61 button.setFixedHeight(32) 

62 return button 

63 

64 def add_action(self, action, text): 

65 """メニューと同じ QAction の行。チェックの状態と有効・無効はメニューと一緒に変わる。 

66 

67 行の文字は短い名前にする(ツールバーの QAction の文字は説明を兼ねて長いので、それはツールチップに出す)。 

68 """ 

69 button = self._row_button() 

70 button.setText(tr(text)) 

71 button.setCheckable(action.isCheckable()) 

72 button.action = action 

73 

74 def sync(): 

75 button.setEnabled(action.isEnabled()) 

76 button.setChecked(action.isChecked()) 

77 button.setToolTip(action.text().replace("&", "")) 

78 button.setIcon(action.icon()) 

79 

80 action.changed.connect(sync) 

81 action.toggled.connect(lambda _checked: sync()) 

82 # 押すとボタンのチェックが先に変わるので、QAction の側で決め直す(trigger が反転させる) 

83 button.clicked.connect(lambda _checked=False: (action.trigger(), sync())) 

84 sync() 

85 self._insert(button) 

86 self.rows.append(button) 

87 return button 

88 

89 def add_command(self, text, slot): 

90 button = self._row_button() 

91 button.setText(tr(text)) 

92 button.clicked.connect(lambda _checked=False: slot()) 

93 self._insert(button) 

94 self.rows.append(button) 

95 return button 

96 

97 def row_texts(self): 

98 return [button.text().replace("&", "") for button in self.rows] 

99 

100 

101# 解析のページのデータ処理(PROCESSING_OPERATIONS のキー。よく使うものだけ。全部はデータセットのメニューにある) 

102ANALYSIS_PROCESSING_KEYS = ("normalize", "savgol_smooth", "baseline_correction", "interval_integral", 

103 "cumulative_integral", "resample", "detect_outliers") 

104 

105 

106def build_analysis_page(app): 

107 page = ActionListPage("解析") 

108 page.add_section("フィット") 

109 page.add_command("曲線フィット...", app.fitting.fit_current_dataset) 

110 page.add_command("一括フィット...", app.fitting.batch_fit_selected) 

111 page.add_command("多峰フィット...", app.fitting.multi_peak_fit_current_dataset) 

112 page.add_command("フィット結果を表示", app.fitting.show_fit_result) 

113 page.add_section("ピーク") 

114 page.add_command("ピーク検出...", app.peaks.find_peaks) 

115 page.add_command("ピークにラベルを付ける", app.peaks.add_peak_labels) 

116 page.add_section("データ処理") 

117 for key in ANALYSIS_PROCESSING_KEYS: 

118 # ProcessingController のメソッドは、ここに挙げたキーと同じ名前 

119 page.add_command(tr(PROCESSING_OPERATIONS[key].title) + "...", getattr(app.processing, key)) 

120 return page 

121 

122 

123def populate_side_pages(app): 

124 """解析・注釈・レイアウトのページを作って左パネルに足す。メニューの QAction を使うので、メニューを作った後に呼ぶ。""" 

125 for key, builder in (("analysis", build_analysis_page), ("notes", build_notes_page), 

126 ("layout", build_layout_page)): 

127 page = builder(app) 

128 app.left_panel.addWidget(page) 

129 app._left_panel_pages[key] = page 

130 

131 

132def build_notes_page(app): 

133 page = ActionListPage("注釈") 

134 page.add_section("グラフに描く") 

135 page.add_action(app.annotation_action, "注釈") 

136 page.add_action(app.region_highlight_action, "領域ハイライト") 

137 page.add_action(app.range_select_action, "範囲選択") 

138 page.add_section("表示") 

139 page.add_action(app.panel_labels_action, "パネルラベルを自動表示") 

140 return page 

141 

142 

143def build_layout_page(app): 

144 page = ActionListPage("レイアウト") 

145 page.add_section("配置") 

146 page.add_action(app.layout_edit_action, "レイアウト編集(自由配置のとき)") 

147 page.add_command("サブプロットの数・並び(軸の設定)", lambda: _open_axis_settings(app)) 

148 page.add_section("表示") 

149 page.add_action(app.minimap_action, "ミニマップ") 

150 page.add_action(app.canvas_detach_action, "キャンバスを別ウィンドウに切り離す") 

151 return page 

152 

153 

154def _open_axis_settings(app): 

155 """サブプロットの行と列は、右のプロパティの「プロットのプロパティ」にある。そこを見せる。""" 

156 app.properties_dock_widget.show() 

157 app.properties_dock_widget.raise_() 

158 

159 

160class PluginPage(QWidget): 

161 """プラグインが register_panel で登録したパネル。上で選び、下に1つずつ出す。""" 

162 

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

164 super().__init__(parent) 

165 self.setObjectName("side_page") 

166 self.setFixedWidth(LEFT_PANEL_WIDTH) 

167 self._app = app 

168 layout = QVBoxLayout(self) 

169 layout.setContentsMargins(0, 0, 0, 0) 

170 layout.setSpacing(0) 

171 heading = QLabel(tr("プラグイン")) 

172 heading.setObjectName("panel_title") 

173 heading.setFixedHeight(52) 

174 heading.setContentsMargins(14, 0, 0, 0) 

175 layout.addWidget(heading) 

176 self.selector = QComboBox() 

177 self.selector.setObjectName("plugin_panel_selector") 

178 self.selector.currentIndexChanged.connect(self._on_selected) 

179 selector_row = QVBoxLayout() 

180 selector_row.setContentsMargins(12, 0, 12, 8) 

181 selector_row.addWidget(self.selector) 

182 layout.addLayout(selector_row) 

183 self.stack = QStackedWidget() 

184 layout.addWidget(self.stack, 1) 

185 

186 self.empty = QWidget() 

187 empty_layout = QVBoxLayout(self.empty) 

188 empty_layout.setContentsMargins(14, 8, 14, 8) 

189 message = QLabel(tr("プラグインのパネルはありません。プラグインは環境設定から追加できます。")) 

190 message.setObjectName("side_page_note") 

191 message.setWordWrap(True) 

192 manage = QPushButton(tr("環境設定を開く...")) 

193 manage.clicked.connect(lambda _checked=False: app._on_show_preferences()) 

194 empty_layout.addWidget(message) 

195 empty_layout.addWidget(manage, 0, Qt.AlignmentFlag.AlignLeft) 

196 empty_layout.addStretch(1) 

197 self.stack.addWidget(self.empty) 

198 self.panels = {} 

199 self._refresh_selector() 

200 

201 def add_panel(self, name, widget): 

202 self.panels[name] = widget 

203 self.stack.addWidget(widget) 

204 self.selector.addItem(name) 

205 self._refresh_selector() 

206 

207 def show_panel(self, name): 

208 index = self.selector.findText(name) 

209 if index >= 0: 209 ↛ exitline 209 didn't return from function 'show_panel' because the condition on line 209 was always true

210 self.selector.setCurrentIndex(index) 

211 

212 def _refresh_selector(self): 

213 # 1つだけなら選ぶ欄は要らない 

214 self.selector.setVisible(len(self.panels) > 1) 

215 if not self.panels: 

216 self.stack.setCurrentWidget(self.empty) 

217 elif self.selector.currentIndex() < 0: 217 ↛ 218line 217 didn't jump to line 218 because the condition on line 217 was never true

218 self.selector.setCurrentIndex(0) 

219 else: 

220 self._on_selected(self.selector.currentIndex()) 

221 

222 def _on_selected(self, index): 

223 if index < 0: 223 ↛ 224line 223 didn't jump to line 224 because the condition on line 223 was never true

224 return 

225 widget = self.panels.get(self.selector.itemText(index)) 

226 if widget is not None: 226 ↛ exitline 226 didn't return from function '_on_selected' because the condition on line 226 was always true

227 self.stack.setCurrentWidget(widget)