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
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
1"""左パネルの「解析」「注釈」「レイアウト」「プラグイン」のページ。
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)
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
15class ActionListPage(QWidget):
16 """見出しと、節ごとのボタンの並び。"""
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 = []
44 def _insert(self, widget):
45 self._body.insertWidget(self._body.count() - 1, widget)
47 def add_section(self, title):
48 label = QLabel(tr(title))
49 label.setObjectName("side_page_section")
50 self._insert(label)
51 return label
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
64 def add_action(self, action, text):
65 """メニューと同じ QAction の行。チェックの状態と有効・無効はメニューと一緒に変わる。
67 行の文字は短い名前にする(ツールバーの QAction の文字は説明を兼ねて長いので、それはツールチップに出す)。
68 """
69 button = self._row_button()
70 button.setText(tr(text))
71 button.setCheckable(action.isCheckable())
72 button.action = action
74 def sync():
75 button.setEnabled(action.isEnabled())
76 button.setChecked(action.isChecked())
77 button.setToolTip(action.text().replace("&", ""))
78 button.setIcon(action.icon())
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
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
97 def row_texts(self):
98 return [button.text().replace("&", "") for button in self.rows]
101# 解析のページのデータ処理(PROCESSING_OPERATIONS のキー。よく使うものだけ。全部はデータセットのメニューにある)
102ANALYSIS_PROCESSING_KEYS = ("normalize", "savgol_smooth", "baseline_correction", "interval_integral",
103 "cumulative_integral", "resample", "detect_outliers")
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
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
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
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
154def _open_axis_settings(app):
155 """サブプロットの行と列は、右のプロパティの「プロットのプロパティ」にある。そこを見せる。"""
156 app.properties_dock_widget.show()
157 app.properties_dock_widget.raise_()
160class PluginPage(QWidget):
161 """プラグインが register_panel で登録したパネル。上で選び、下に1つずつ出す。"""
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)
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()
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()
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)
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())
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)