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

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 

9 

10 

11def build_dataset_property_sections(app): 

12 """データセットのプロパティ欄を DATASET_PROPERTY_SECTIONS の節に分け、節ごとに QFormLayout を持たせる。 

13 

14 列幅の広がりが節の中に閉じる。Designer の行は takeRow() で移す(removeRow() はウィジェットごと破棄する)。 

15 平滑化のチェックだけは「平滑化の手法」の直前に置くので、ここでは移さない。 

16 """ 

17 app._prop_sections = {} 

18 

19 container = QWidget() 

20 container_layout = QVBoxLayout(container) 

21 container_layout.setContentsMargins(0, 0, 0, 0) 

22 container_layout.setSpacing(0) 

23 

24 collapsed = app._load_collapsed_property_sections() 

25 

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) 

35 

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) 

51 

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

59 

60 toggle_button.toggled.connect( 

61 lambda checked, k=key: app._on_property_section_toggled(k, checked)) 

62 

63 container_layout.addWidget(section) 

64 app._prop_sections[key] = { 

65 'form': form, 'body': body, 'toggle': toggle_button, 'section': section, 

66 } 

67 

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 

76 

77 # 無効な親の下の子は有効にできないので、グループボックスは常に有効にして中身だけ無効にする 

78 # (でないと、データセットが無い間は節を開閉できない) 

79 app.ui.properties_groupbox.setEnabled(True) 

80 app._set_dataset_property_fields_enabled(False) 

81 

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) 

96 

97 cap_combo_width(app.ui.plot_type_combo) 

98 

99 

100def prop_form(app, section_key): 

101 return app._prop_sections[section_key]['form'] 

102 

103 

104def align_form_label_columns(app, forms, width=None): 

105 """ラベルの列幅を複数の QFormLayout で揃え、入力欄の左端を合わせる。決めた幅(px)を返す。 

106 

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

120 

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 

129 

130 

131def set_dataset_property_fields_enabled(app, enabled): 

132 """選択が無いときは無効にする。無効にするのは節の中身だけ(見出しまで無効にすると開閉できない)。""" 

133 for entry in getattr(app, '_prop_sections', {}).values(): 

134 entry['body'].setEnabled(enabled) 

135 

136 

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} 

145 

146 

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

155 

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

161 

162 

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 

177 

178 

179def update_property_section_visibility(app): 

180 """シンプルで隠す行を隠し、中の行が全部隠れた節は見出しごと隠す(シンプルで出さない節も)。 

181 

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