Coverage for graphica/gui/builders/property_sections.py: 95.8%
141 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"""データセットのプロパティ欄の節(折りたたみ・開閉の保存・見出しの出し入れ・ラベル列の幅)。"""
2import json
3from PySide6.QtCore import Qt
4from PySide6.QtWidgets import QFormLayout, QLabel, QSizePolicy, QVBoxLayout, QWidget
5from graphica.core.i18n import tr
6from graphica.gui import app_settings, ui_mode
7from graphica.gui.builders.common import DATASET_PROPERTY_SECTIONS, _svg_icon, cap_combo_width
8from graphica.gui.widgets import SectionHeader
11def build_dataset_property_sections(app):
12 """データセットのプロパティ欄を DATASET_PROPERTY_SECTIONS の節に分け、節ごとに QFormLayout を持たせる。
14 列幅の広がりが節の中に閉じる。Designer の行は takeRow() で移す(removeRow() はウィジェットごと破棄する)。
15 平滑化のチェックだけは「平滑化の手法」の直前に置くので、ここでは移さない。
16 """
17 app._prop_sections = {}
19 container = QWidget()
20 container_layout = QVBoxLayout(container)
21 container_layout.setContentsMargins(0, 0, 0, 0)
22 container_layout.setSpacing(0)
24 collapsed = app._load_collapsed_property_sections()
26 for key, title in DATASET_PROPERTY_SECTIONS:
27 body = QWidget()
28 body.setObjectName("property_section_body")
29 form = QFormLayout(body)
30 # 見本どおり、見出しより 20px 字下げし、名前と欄の間 10px、行の間 4px
31 form.setContentsMargins(20, 0, 0, 6)
32 form.setHorizontalSpacing(10)
33 form.setVerticalSpacing(4)
34 form.setRowWrapPolicy(QFormLayout.RowWrapPolicy.WrapLongRows)
36 # 閉じていても今の状態(「オフ」など)が右に出る見出し(GUI 刷新)
37 toggle_button = SectionHeader(tr(title))
38 toggle_button.setIcon(_svg_icon("chevron-down", size=13))
39 toggle_button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
40 # 上の2つの開閉ボタンとは別の名前(theme.py で控えめに描き、2つだけであることをテストが確かめる)
41 toggle_button.setObjectName("property_subsection_toggle")
42 toggle_button.setCursor(Qt.CursorShape.PointingHandCursor)
43 toggle_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
44 # QToolButton は文字幅なので、区切りの罫線が見出しの下にしか引かれない
45 toggle_button.setSizePolicy(
46 QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
47 toggle_button.setChecked(key not in collapsed)
48 # 1本目は親の見出しのすぐ下なので、罫線が二重に見える
49 if key == DATASET_PROPERTY_SECTIONS[0][0]:
50 toggle_button.setProperty("firstSection", True)
52 section = QWidget()
53 section_layout = QVBoxLayout(section)
54 section_layout.setContentsMargins(0, 0, 0, 0)
55 section_layout.setSpacing(0)
56 section_layout.addWidget(toggle_button)
57 section_layout.addWidget(body)
58 body.setVisible(toggle_button.isChecked())
60 toggle_button.toggled.connect(
61 lambda checked, k=key: app._on_property_section_toggled(k, checked))
63 container_layout.addWidget(section)
64 app._prop_sections[key] = {
65 'form': form, 'body': body, 'toggle': toggle_button, 'section': section,
66 }
68 # 空の formLayout_4 は生成物の構造なので残し、余白だけ潰す
69 for row in range(app.ui.formLayout_4.rowCount() - 1, -1, -1):
70 app.ui.formLayout_4.takeRow(row)
71 app.ui.formLayout_4.setContentsMargins(0, 0, 0, 0)
72 app.ui.formLayout_4.setSpacing(0)
73 app.ui.gridLayout_4.setContentsMargins(0, 0, 0, 0)
74 app.ui.gridLayout_4.addWidget(container, 1, 0, 1, 1)
75 app._dataset_property_sections_container = container
77 # 無効な親の下の子は有効にできないので、グループボックスは常に有効にして中身だけ無効にする
78 # (でないと、データセットが無い間は節を開閉できない)
79 app.ui.properties_groupbox.setEnabled(True)
80 app._set_dataset_property_fields_enabled(False)
82 style_form = app._prop_form('style')
83 # シンプルの種類のタイル(中身は property_widgets.build_property_widgets)。節の先頭の行
84 app._plot_type_tiles_row = QWidget()
85 tiles_layout = QVBoxLayout(app._plot_type_tiles_row)
86 tiles_layout.setContentsMargins(0, 0, 0, 2)
87 style_form.addRow(app._plot_type_tiles_row)
88 style_form.addRow(app.ui.legend_name_label, app.ui.legend_name_edit)
89 style_form.addRow(app.ui.plot_type_label, app.ui.plot_type_combo)
90 style_form.addRow(app.ui.color_label, app.color_picker_widget)
91 style_form.addRow(app.ui.linestyle_label, app.ui.linestyle_combo)
92 style_form.addRow(app.ui.linewidth_label, app.ui.linewidth_spinbox)
93 marker_form = app._prop_form('marker')
94 marker_form.addRow(app.ui.marker_label, app.ui.marker_combo)
95 marker_form.addRow(app.ui.makersize_label, app.ui.markersize_spinbox)
97 cap_combo_width(app.ui.plot_type_combo)
100def prop_form(app, section_key):
101 return app._prop_sections[section_key]['form']
104def align_form_label_columns(app, forms, width=None):
105 """ラベルの列幅を複数の QFormLayout で揃え、入力欄の左端を合わせる。決めた幅(px)を返す。
107 隠れているラベルも数える(QFormLayout は隠れたものを外すので、出た瞬間に列幅が変わって欄がずれる)。
108 width を渡すとその幅に固定し、入らない名前は折り返す(インスペクタのプロパティは見本どおり 92px)。
109 """
110 labels = []
111 widest = 0
112 for form in forms:
113 for row in range(form.rowCount()):
114 item = form.itemAt(row, QFormLayout.ItemRole.LabelRole)
115 if item is None or not isinstance(item.widget(), QLabel):
116 continue
117 label = item.widget()
118 labels.append(label)
119 widest = max(widest, label.sizeHint().width())
121 if width is not None:
122 for label in labels:
123 label.setFixedWidth(width)
124 label.setWordWrap(True)
125 return width
126 for label in labels:
127 label.setMinimumWidth(widest)
128 return widest
131def set_dataset_property_fields_enabled(app, enabled):
132 """選択が無いときは無効にする。無効にするのは節の中身だけ(見出しまで無効にすると開閉できない)。"""
133 for entry in getattr(app, '_prop_sections', {}).values():
134 entry['body'].setEnabled(enabled)
137def load_collapsed_property_sections(app):
138 raw = app_settings.DATASET_PROPERTY_COLLAPSED_SECTIONS.read(app.settings)
139 try:
140 keys = json.loads(raw) if isinstance(raw, str) else list(raw)
141 except (ValueError, TypeError):
142 return set()
143 valid = {key for key, _ in DATASET_PROPERTY_SECTIONS}
144 return {k for k in keys if k in valid}
147def on_property_section_toggled(app, section_key, checked):
148 """開閉し、閉じている節を QSettings に書き戻す(一度閉じたものは次の起動でも閉じたまま)。"""
149 entry = app._prop_sections.get(section_key)
150 if entry is None: 150 ↛ 151line 150 didn't jump to line 151 because the condition on line 150 was never true
151 return
152 entry['body'].setVisible(checked)
153 entry['toggle'].setIcon(
154 _svg_icon("chevron-down" if checked else "chevron-right", size=13))
156 collapsed = sorted(
157 key for key, _ in DATASET_PROPERTY_SECTIONS
158 if not app._prop_sections[key]['toggle'].isChecked()
159 )
160 app_settings.DATASET_PROPERTY_COLLAPSED_SECTIONS.write(app.settings, json.dumps(collapsed))
163def lite_hidden_property_widgets(app):
164 """シンプルで隠す行の欄と名前。"""
165 widgets = []
166 for dotted_name in ui_mode.LITE_HIDDEN_PROPERTY_FIELDS:
167 field = ui_mode.resolve(app, dotted_name)
168 if field is None: 168 ↛ 169line 168 didn't jump to line 169 because the condition on line 168 was never true
169 continue
170 widgets.append(field)
171 for entry in app._prop_sections.values():
172 label = entry['form'].labelForField(field)
173 if label is not None:
174 widgets.append(label)
175 break
176 return widgets
179def update_property_section_visibility(app):
180 """シンプルで隠す行を隠し、中の行が全部隠れた節は見出しごと隠す(シンプルで出さない節も)。
182 行の出し入れ(property_panel の update_*_visibility)の最後に呼ばれるので、モードの条件はここで当てる。
183 """
184 sections = getattr(app, '_prop_sections', None)
185 if not sections: 185 ↛ 186line 185 didn't jump to line 186 because the condition on line 185 was never true
186 return
187 lite = getattr(app, 'ui_mode', None) == ui_mode.LITE
188 if lite:
189 for widget in lite_hidden_property_widgets(app):
190 widget.setVisible(False)
191 for key, _ in DATASET_PROPERTY_SECTIONS:
192 entry = sections.get(key)
193 if entry is None: 193 ↛ 194line 193 didn't jump to line 194 because the condition on line 193 was never true
194 continue
195 form = entry['form']
196 has_visible = False
197 for row in range(form.rowCount()):
198 for role in (QFormLayout.ItemRole.LabelRole,
199 QFormLayout.ItemRole.FieldRole,
200 QFormLayout.ItemRole.SpanningRole):
201 item = form.itemAt(row, role)
202 if item is not None and item.widget() is not None \
203 and not item.widget().isHidden():
204 has_visible = True
205 break
206 if has_visible:
207 break
208 entry['section'].setVisible(has_visible and (not lite or key in ui_mode.LITE_PROPERTY_SECTIONS))