graphica-plot 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. graphica/Graphica.ico +0 -0
  2. graphica/__init__.py +1 -0
  3. graphica/__main__.py +111 -0
  4. graphica/assets/__init__.py +0 -0
  5. graphica/assets/icons/__init__.py +0 -0
  6. graphica/assets/icons/arrow-right.svg +21 -0
  7. graphica/assets/icons/bold.svg +20 -0
  8. graphica/assets/icons/calculator.svg +26 -0
  9. graphica/assets/icons/chart-histogram.svg +24 -0
  10. graphica/assets/icons/chart-line.svg +20 -0
  11. graphica/assets/icons/chevron-down.svg +19 -0
  12. graphica/assets/icons/chevron-right.svg +19 -0
  13. graphica/assets/icons/color-swatch.svg +22 -0
  14. graphica/assets/icons/column-insert-right.svg +21 -0
  15. graphica/assets/icons/column-remove.svg +21 -0
  16. graphica/assets/icons/copy.svg +20 -0
  17. graphica/assets/icons/download.svg +21 -0
  18. graphica/assets/icons/edit.svg +21 -0
  19. graphica/assets/icons/eye-off.svg +21 -0
  20. graphica/assets/icons/eye.svg +20 -0
  21. graphica/assets/icons/file-plus.svg +22 -0
  22. graphica/assets/icons/folder-plus.svg +21 -0
  23. graphica/assets/icons/folder.svg +19 -0
  24. graphica/assets/icons/highlight.svg +22 -0
  25. graphica/assets/icons/history.svg +20 -0
  26. graphica/assets/icons/italic.svg +21 -0
  27. graphica/assets/icons/layout-grid.svg +22 -0
  28. graphica/assets/icons/math-function.svg +22 -0
  29. graphica/assets/icons/message-2.svg +21 -0
  30. graphica/assets/icons/mountain.svg +20 -0
  31. graphica/assets/icons/palette.svg +22 -0
  32. graphica/assets/icons/pointer.svg +19 -0
  33. graphica/assets/icons/refresh.svg +20 -0
  34. graphica/assets/icons/row-insert-bottom.svg +21 -0
  35. graphica/assets/icons/row-remove.svg +21 -0
  36. graphica/assets/icons/search.svg +20 -0
  37. graphica/assets/icons/select-all.svg +35 -0
  38. graphica/assets/icons/subscript.svg +20 -0
  39. graphica/assets/icons/superscript.svg +20 -0
  40. graphica/assets/icons/table.svg +21 -0
  41. graphica/assets/icons/trash.svg +23 -0
  42. graphica/assets/icons/typography.svg +23 -0
  43. graphica/assets/icons/x.svg +20 -0
  44. graphica/core/__init__.py +0 -0
  45. graphica/core/analysis.py +1278 -0
  46. graphica/core/app_paths.py +35 -0
  47. graphica/core/axis_settings.py +128 -0
  48. graphica/core/caption_export.py +47 -0
  49. graphica/core/color_palettes.py +56 -0
  50. graphica/core/commands.py +226 -0
  51. graphica/core/cvd_simulation.py +49 -0
  52. graphica/core/dataset.py +443 -0
  53. graphica/core/diagnostics.py +105 -0
  54. graphica/core/excel_utils.py +58 -0
  55. graphica/core/fit_models.py +379 -0
  56. graphica/core/grid_data.py +128 -0
  57. graphica/core/i18n.py +44 -0
  58. graphica/core/json_utils.py +27 -0
  59. graphica/core/label_utils.py +21 -0
  60. graphica/core/methods_text.py +115 -0
  61. graphica/core/named_colors.py +126 -0
  62. graphica/core/plugin_api.py +515 -0
  63. graphica/core/plugin_context.py +167 -0
  64. graphica/core/plugin_install.py +82 -0
  65. graphica/core/plugin_manifest.py +67 -0
  66. graphica/core/plugin_testing.py +213 -0
  67. graphica/core/plugin_types.py +118 -0
  68. graphica/core/provenance.py +19 -0
  69. graphica/core/report_export.py +51 -0
  70. graphica/core/safe_eval.py +194 -0
  71. graphica/core/script_export.py +272 -0
  72. graphica/core/translations_en.py +530 -0
  73. graphica/core/unit_conversion.py +51 -0
  74. graphica/core/update_check.py +43 -0
  75. graphica/core/version.py +14 -0
  76. graphica/gui/__init__.py +0 -0
  77. graphica/gui/app_settings.py +119 -0
  78. graphica/gui/axis_bindings.py +173 -0
  79. graphica/gui/binding.py +135 -0
  80. graphica/gui/builders/__init__.py +1 -0
  81. graphica/gui/builders/axis_panel.py +431 -0
  82. graphica/gui/builders/canvas_area.py +176 -0
  83. graphica/gui/builders/common.py +120 -0
  84. graphica/gui/builders/dataset_panel.py +433 -0
  85. graphica/gui/builders/property_sections.py +208 -0
  86. graphica/gui/canvas.py +519 -0
  87. graphica/gui/color_history.py +47 -0
  88. graphica/gui/color_picker_widget.py +198 -0
  89. graphica/gui/crash_handler.py +77 -0
  90. graphica/gui/cvd_preview.py +23 -0
  91. graphica/gui/data_editor.py +761 -0
  92. graphica/gui/data_import_flow.py +355 -0
  93. graphica/gui/dataset_bindings.py +107 -0
  94. graphica/gui/dataset_style_icon.py +80 -0
  95. graphica/gui/datasets/__init__.py +1 -0
  96. graphica/gui/datasets/actions_menu.py +114 -0
  97. graphica/gui/datasets/colors.py +218 -0
  98. graphica/gui/datasets/fitting.py +70 -0
  99. graphica/gui/datasets/host.py +162 -0
  100. graphica/gui/datasets/operations/__init__.py +1 -0
  101. graphica/gui/datasets/operations/fitting.py +468 -0
  102. graphica/gui/datasets/operations/peaks.py +109 -0
  103. graphica/gui/datasets/operations/processing.py +659 -0
  104. graphica/gui/datasets/operations/runner.py +148 -0
  105. graphica/gui/datasets/operations/transfer.py +227 -0
  106. graphica/gui/datasets/order.py +310 -0
  107. graphica/gui/datasets/overlays.py +74 -0
  108. graphica/gui/datasets/peaks.py +21 -0
  109. graphica/gui/datasets/plugin_runs.py +99 -0
  110. graphica/gui/datasets/processing.py +68 -0
  111. graphica/gui/datasets/property_panel.py +538 -0
  112. graphica/gui/datasets/transfer.py +36 -0
  113. graphica/gui/detached_canvas_window.py +22 -0
  114. graphica/gui/dialogs/__init__.py +137 -0
  115. graphica/gui/dialogs/analysis.py +1322 -0
  116. graphica/gui/dialogs/app.py +1015 -0
  117. graphica/gui/dialogs/appearance.py +851 -0
  118. graphica/gui/dialogs/data_edit.py +577 -0
  119. graphica/gui/dialogs/data_import.py +679 -0
  120. graphica/gui/dialogs/export.py +433 -0
  121. graphica/gui/dock_layout.py +176 -0
  122. graphica/gui/export_preview_panel.py +321 -0
  123. graphica/gui/export_settings.py +16 -0
  124. graphica/gui/file_association.py +97 -0
  125. graphica/gui/icon_utils.py +44 -0
  126. graphica/gui/main_app_window.py +210 -0
  127. graphica/gui/main_window.py +1151 -0
  128. graphica/gui/mathtext_preview.py +168 -0
  129. graphica/gui/menu_bar.py +320 -0
  130. graphica/gui/minimap_widget.py +151 -0
  131. graphica/gui/mixins/__init__.py +0 -0
  132. graphica/gui/mixins/export_mixin.py +520 -0
  133. graphica/gui/mixins/help_mixin.py +127 -0
  134. graphica/gui/mixins/project_io_mixin.py +289 -0
  135. graphica/gui/mixins/quick_access_mixin.py +194 -0
  136. graphica/gui/mixins/ui_setup_mixin.py +206 -0
  137. graphica/gui/notify.py +61 -0
  138. graphica/gui/panels/__init__.py +43 -0
  139. graphica/gui/panels/axis_settings.py +605 -0
  140. graphica/gui/panels/dataset_tree.py +424 -0
  141. graphica/gui/plot_type_drawers.py +131 -0
  142. graphica/gui/plugin_context.py +119 -0
  143. graphica/gui/project_files.py +361 -0
  144. graphica/gui/provenance_panel.py +65 -0
  145. graphica/gui/rendering/__init__.py +1 -0
  146. graphica/gui/rendering/annotations.py +177 -0
  147. graphica/gui/rendering/appearance.py +368 -0
  148. graphica/gui/rendering/common.py +316 -0
  149. graphica/gui/rendering/data_1d.py +376 -0
  150. graphica/gui/rendering/data_2d.py +66 -0
  151. graphica/gui/residual_panel.py +62 -0
  152. graphica/gui/resources.py +16 -0
  153. graphica/gui/single_instance.py +141 -0
  154. graphica/gui/splash.py +83 -0
  155. graphica/gui/task_runner.py +41 -0
  156. graphica/gui/theme.py +864 -0
  157. graphica/gui/tools/__init__.py +56 -0
  158. graphica/gui/tools/annotation.py +275 -0
  159. graphica/gui/tools/cursor.py +259 -0
  160. graphica/gui/tools/layout_edit.py +285 -0
  161. graphica/gui/tools/manager.py +78 -0
  162. graphica/gui/tools/peak_placement.py +132 -0
  163. graphica/gui/tools/range_select.py +191 -0
  164. graphica/gui/tools/region_highlight.py +222 -0
  165. graphica/gui/tools/slice_extraction.py +200 -0
  166. graphica/gui/widget_translation.py +46 -0
  167. graphica/gui/workers.py +280 -0
  168. graphica/models/__init__.py +0 -0
  169. graphica/models/project.py +197 -0
  170. graphica/plugin/__init__.py +20 -0
  171. graphica/plugin/testing.py +57 -0
  172. graphica/sample_data/__init__.py +0 -0
  173. graphica/sample_data/cooling_curve_sample.csv +42 -0
  174. graphica/ui_main_window.py +581 -0
  175. graphica_plot-2.0.0.dist-info/METADATA +317 -0
  176. graphica_plot-2.0.0.dist-info/RECORD +181 -0
  177. graphica_plot-2.0.0.dist-info/WHEEL +5 -0
  178. graphica_plot-2.0.0.dist-info/entry_points.txt +2 -0
  179. graphica_plot-2.0.0.dist-info/licenses/LICENSE +30 -0
  180. graphica_plot-2.0.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +91 -0
  181. graphica_plot-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,119 @@
1
+ """アプリの設定(QSettings)のキー・型・既定値の表と、設定を開く唯一の場所。
2
+
3
+ 読み方は今の挙動をそのまま写している。型を指定しないキー(value_type が None)は INI 形式では真偽や数が文字列で返り、
4
+ 要素が 1 つのリストは文字列で返ることがある。どちらも読む側の扱いまで含めて挙動なので、型の有無を変えないこと。
5
+ """
6
+ from dataclasses import dataclass
7
+ from typing import Any
8
+
9
+ from PySide6.QtCore import QSettings
10
+
11
+ from graphica.core.i18n import DEFAULT_LANGUAGE
12
+
13
+ ORGANIZATION_NAME = "Graphica"
14
+ APPLICATION_NAME = "Graphica"
15
+
16
+ DEFAULT_AUTOSAVE_INTERVAL_MIN = 5 # 0 で無効
17
+ # これより点が多いデータセットには点ラベルを描かない(annotate が点の数だけ呼ばれて固まる)。環境設定で変えられる
18
+ DEFAULT_POINT_LABEL_MAX_POINTS = 1000
19
+ DEFAULT_SNAP_TO_GRID_ENABLED = False
20
+ DEFAULT_SNAP_GRID_INTERVAL_PX = 10
21
+
22
+ CANVAS_DETACHED_GEOMETRY_KEY = "canvas_detached_geometry"
23
+ CANVAS_WAS_DETACHED_KEY = "canvas_was_detached"
24
+ # 名前を付けて保存するドック配置(起動時に戻す直近の window_state とは別)
25
+ DOCK_LAYOUT_PRESETS_SETTINGS_KEY = "dock_layout_presets"
26
+ # 閉じている節のキーの JSON 配列。無い・空なら全部開く(節を足しても既定は開いたまま)
27
+ DATASET_PROPERTY_COLLAPSED_SECTIONS_KEY = "dataset_property_collapsed_sections"
28
+ DISABLED_PLUGINS_SETTINGS_KEY = "disabled_plugins"
29
+ QUICK_ACCESS_SETTINGS_KEY = "quick_access_pinned_actions"
30
+ COLOR_PALETTES_SETTINGS_KEY = "custom_color_palettes_json"
31
+ ACTIVE_PALETTE_SETTINGS_KEY = "active_color_palette"
32
+ RECENT_COLORS_SETTINGS_KEY = "recent_colors"
33
+
34
+ _TABLE_DEFAULT = object()
35
+
36
+
37
+ def open_settings() -> QSettings:
38
+ """アプリの設定を開く。QSettings を作るのはここだけ(テストはこのモジュールの QSettings を差し替えて隔離する)。"""
39
+ return QSettings(ORGANIZATION_NAME, APPLICATION_NAME)
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class Setting:
44
+ key: str
45
+ default: Any = None
46
+ value_type: type | None = None # None なら型を指定せずに読む
47
+ export_as: type | None = None # 設定の書き出しに入れるときの型(list は要素 1 つを直して読む)
48
+ export_default: Any = _TABLE_DEFAULT # 書き出しのときだけ既定値が違うもの
49
+
50
+ def read(self, settings: QSettings, default: Any = _TABLE_DEFAULT) -> Any:
51
+ """default を渡すのは、読む場所によって既定値が違うところ(今の挙動)だけ。"""
52
+ fallback = self.default if default is _TABLE_DEFAULT else default
53
+ if self.value_type is None:
54
+ return settings.value(self.key, fallback)
55
+ return settings.value(self.key, fallback, type=self.value_type)
56
+
57
+ def write(self, settings: QSettings, value: Any) -> None:
58
+ settings.setValue(self.key, value)
59
+
60
+ def read_for_export(self, settings: QSettings) -> Any:
61
+ default = self.default if self.export_default is _TABLE_DEFAULT else self.export_default
62
+ if self.export_as is list:
63
+ return as_list(settings.value(self.key, default))
64
+ return settings.value(self.key, default, type=self.export_as)
65
+
66
+
67
+ def as_list(value: Any) -> Any:
68
+ """要素 1 つのリストが文字列で返ったのを直す。空の文字列は空のリスト(設定の書き出し・最近使った色の読み方)。"""
69
+ if isinstance(value, str):
70
+ return [value] if value else []
71
+ return value
72
+
73
+
74
+ def as_list_keeping_empty_string(value: Any) -> list[Any]:
75
+ """要素 1 つのリストが文字列で返ったのを直す(最近使ったファイル・クイックアクセスの読み方)。"""
76
+ if isinstance(value, str):
77
+ value = [value]
78
+ return list(value) if value else []
79
+
80
+
81
+ LANGUAGE = Setting("language", DEFAULT_LANGUAGE, export_as=str, export_default="")
82
+ DARK_MODE = Setting("dark_mode", False, bool, export_as=bool)
83
+ AUTOSAVE_INTERVAL_MIN = Setting("autosave_interval_min", DEFAULT_AUTOSAVE_INTERVAL_MIN, int, export_as=int)
84
+ AUTOSAVE_DIR = Setting("autosave_dir", "", str)
85
+ POINT_LABEL_MAX_POINTS = Setting("point_label_max_points", DEFAULT_POINT_LABEL_MAX_POINTS, int, export_as=int)
86
+ SNAP_TO_GRID_ENABLED = Setting("snap_to_grid_enabled", DEFAULT_SNAP_TO_GRID_ENABLED, bool, export_as=bool)
87
+ SNAP_GRID_INTERVAL_PX = Setting("snap_grid_interval_px", DEFAULT_SNAP_GRID_INTERVAL_PX, int, export_as=int)
88
+ CUSTOM_COLOR_PALETTES = Setting(COLOR_PALETTES_SETTINGS_KEY, "", export_as=str)
89
+ # 画面の既定は配色パレットのダイアログの既定の名前(読む側が渡す)。書き出しでは空
90
+ ACTIVE_COLOR_PALETTE = Setting(ACTIVE_PALETTE_SETTINGS_KEY, None, export_as=str, export_default="")
91
+ QUICK_ACCESS_PINNED_ACTIONS = Setting(QUICK_ACCESS_SETTINGS_KEY, [], export_as=list)
92
+ DISABLED_PLUGINS = Setting(DISABLED_PLUGINS_SETTINGS_KEY, [], export_as=list)
93
+
94
+ CLEAN_EXIT = Setting("clean_exit", True, bool)
95
+ HAS_SHOWN_WELCOME = Setting("has_shown_welcome", False, bool)
96
+ MINIMAP_VISIBLE = Setting("minimap_visible", True, bool)
97
+ RECENT_FILES = Setting("recent_files", [])
98
+ RECENT_COLORS = Setting(RECENT_COLORS_SETTINGS_KEY, [])
99
+ WINDOW_GEOMETRY = Setting("window_geometry")
100
+ WINDOW_STATE = Setting("window_state")
101
+ DOCK_LAYOUT_VERSION = Setting("dock_layout_version", 0, int)
102
+ DOCK_LAYOUT_PRESETS = Setting(DOCK_LAYOUT_PRESETS_SETTINGS_KEY, "{}")
103
+ DATASET_PROPERTY_COLLAPSED_SECTIONS = Setting(DATASET_PROPERTY_COLLAPSED_SECTIONS_KEY, "[]")
104
+ CANVAS_WAS_DETACHED = Setting(CANVAS_WAS_DETACHED_KEY, False, bool)
105
+ CANVAS_DETACHED_GEOMETRY = Setting(CANVAS_DETACHED_GEOMETRY_KEY)
106
+
107
+ # 設定の書き出し(別の PC や研究室での共有用)に入れるもの。JSON の並びはこの順
108
+ EXPORTED_SETTINGS: tuple[Setting, ...] = (
109
+ LANGUAGE, DARK_MODE, AUTOSAVE_INTERVAL_MIN, POINT_LABEL_MAX_POINTS, SNAP_TO_GRID_ENABLED, SNAP_GRID_INTERVAL_PX,
110
+ CUSTOM_COLOR_PALETTES, ACTIVE_COLOR_PALETTE, QUICK_ACCESS_PINNED_ACTIONS, DISABLED_PLUGINS,
111
+ )
112
+
113
+
114
+ def disabled_plugin_names(settings: QSettings) -> set[str]:
115
+ """QSettings は要素が1つのリストを文字列で返すことがあるので直す。"""
116
+ names = DISABLED_PLUGINS.read(settings)
117
+ if isinstance(names, str):
118
+ names = [names]
119
+ return set(names) if names else set()
@@ -0,0 +1,173 @@
1
+ """軸の設定(core/axis_settings.py のキー)と画面の欄の対応表。欄を 1 つ足すときは表に 1 行足す。
2
+
3
+ 表の順は戻す順。集めた辞書のキーの順は AXIS_SETTING_DEFAULTS の順(保存形式)にそろえる。
4
+ """
5
+ from PySide6.QtGui import QFont
6
+
7
+ from graphica.core.unit_conversion import X_AXIS_UNIT_CHOICES
8
+ from graphica.gui.binding import Binding, check, choice, index, item_data, number, shown_text, text
9
+ from graphica.gui.canvas import MINOR_TICK_LENGTH_AUTO
10
+
11
+ _CHANGED = ('_on_axis_setting_changed',)
12
+
13
+ # 欄を持たず操作から直接書き込まれるキー。集めるときは今の軸の設定から引き継ぐ(辞書は丸ごと入れ替わるので)
14
+ CARRIED_AXIS_KEYS = ('annotations', 'legend_order', 'free_rect', 'legend_position')
15
+
16
+ # 戻す間は信号を止めるが、値は持たない欄
17
+ AXIS_BUTTONS = (
18
+ 'ui.tick_font_button', 'ui.tick_color_button', 'ui.axis_label_font_button', 'ui.axis_label_color_button',
19
+ 'legend_font_button', 'legend_color_button', 'ui.spine_color_button',
20
+ )
21
+
22
+
23
+ def qfont_from_family_props(font_props: dict) -> QFont:
24
+ """'family' は候補のリストか、古いプロジェクトでは1つの名前。QFont(list) は無いので setFamilies() を使う。"""
25
+ family = font_props.get('family', 'Sans Serif')
26
+ if isinstance(family, (list, tuple)):
27
+ font = QFont()
28
+ if family:
29
+ font.setFamilies(list(family))
30
+ return font
31
+ return QFont(family)
32
+
33
+
34
+ def _font(key, attribute, size_only_when_given=False):
35
+ """フォントは持ち主の属性に QFont で持つ。凡例は保存にサイズが無ければ QFont の既定のサイズのまま。"""
36
+ def read(owner, _widget):
37
+ return owner._font_props_to_dict(getattr(owner, attribute))
38
+
39
+ def write(owner, _widget, props):
40
+ setattr(owner, attribute, qfont_from_family_props(props))
41
+ font = getattr(owner, attribute)
42
+ if not size_only_when_given or 'size' in props:
43
+ font.setPointSize(props.get('size', 10))
44
+ font.setBold(props.get('weight') == 'bold')
45
+ font.setItalic(props.get('style') == 'italic')
46
+ return Binding(key, None, read, write)
47
+
48
+
49
+ def _attribute(key, attribute):
50
+ return Binding(key, None, lambda owner, _w: getattr(owner, attribute),
51
+ lambda owner, _w, value: setattr(owner, attribute, value))
52
+
53
+
54
+ def _width(key, widget, attribute):
55
+ """線の太さは描画が持ち主の属性から読むので、欄と一緒に属性にも入れる。"""
56
+ def write(owner, spinbox, value):
57
+ setattr(owner, attribute, value)
58
+ spinbox.setValue(getattr(owner, attribute))
59
+ return Binding(key, widget, lambda _o, w: w.value(), write, 'valueChanged', _CHANGED)
60
+
61
+
62
+ def _grid_linestyle(key, widget):
63
+ return Binding(key, widget, lambda owner, w: owner._grid_linestyle_code(w.currentIndex()),
64
+ lambda owner, w, value: w.setCurrentIndex(owner._grid_linestyle_index(value)),
65
+ 'currentIndexChanged', _CHANGED)
66
+
67
+
68
+ def _minor_tick_length(key, widget):
69
+ """負値(スピンボックスの「自動」)は -1 に正規化して保存する。"""
70
+ def read(_owner, spinbox):
71
+ return spinbox.value() if spinbox.value() >= 0 else -1
72
+
73
+ def write(_owner, spinbox, value):
74
+ spinbox.setValue(MINOR_TICK_LENGTH_AUTO if value is None or value < 0 else value)
75
+ return Binding(key, widget, read, write, 'valueChanged', _CHANGED)
76
+
77
+
78
+ def _axis_rows(axis):
79
+ """X と Y で同じ並びの行。autoscale・目盛りの方式・補助目盛りの表示・対数は、見え方を切り替える処理もつなぐ。"""
80
+ ui = f'ui.{axis}'
81
+ return (
82
+ check(f'{axis}_autoscale', f'{ui}_autoscale_checkbox', _CHANGED + (f'_on_{axis}_autoscale_changed',)),
83
+ number(f'{axis}_min', f'{ui}_min_spinbox', _CHANGED),
84
+ number(f'{axis}_max', f'{ui}_max_spinbox', _CHANGED),
85
+ check(f'{axis}_log', f'{ui}_log_checkbox', _CHANGED + (f'_on_{axis}_minor_tick_visibility_changed',)),
86
+ check(f'{axis}_invert', f'{ui}_invert_checkbox', _CHANGED),
87
+ index(f'{axis}_major_tick_mode', f'{ui}_major_tick_mode_combo', _CHANGED + (f'_on_{axis}_tick_mode_changed',)),
88
+ number(f'{axis}_major_tick_interval', f'{ui}_major_tick_interval_spinbox', _CHANGED),
89
+ check(f'{axis}_minor_ticks_visible', f'{ui}_minor_ticks_visible_checkbox',
90
+ _CHANGED + (f'_on_{axis}_minor_tick_visibility_changed',)),
91
+ number(f'{axis}_minor_tick_interval', f'{ui}_minor_tick_interval_spinbox', _CHANGED),
92
+ # 対数軸のときだけ効く
93
+ item_data(f'{axis}_log_minor_subs', f'{axis}_log_minor_subs_combo', _CHANGED),
94
+ check(f'{axis}_log_minor_labels', f'{axis}_log_minor_labels_checkbox', _CHANGED),
95
+ index(f'{axis}_tick_format_mode', f'{axis}_tick_format_combo', _CHANGED),
96
+ number(f'{axis}_tick_decimals', f'{axis}_tick_decimals_spinbox', _CHANGED),
97
+ )
98
+
99
+
100
+ def _grid_rows(axis, which):
101
+ name = f'{axis}_{which}_grid'
102
+ return (
103
+ _grid_linestyle(f'{name}_linestyle', f'{name}_linestyle_combo'),
104
+ number(f'{name}_width', f'{name}_width_spinbox', _CHANGED),
105
+ number(f'{name}_alpha', f'{name}_alpha_spinbox', _CHANGED),
106
+ )
107
+
108
+
109
+ AXIS_BINDINGS = (
110
+ text('title', 'ui.title_text_edit', _CHANGED),
111
+ text('x_label', 'ui.x_label_text_edit', _CHANGED),
112
+ text('y_label', 'ui.y_label_text_edit', _CHANGED),
113
+ check('x_label_visible', 'x_label_visible_checkbox', _CHANGED),
114
+ check('y_label_visible', 'y_label_visible_checkbox', _CHANGED),
115
+ text('y2_label', 'y2_label_text_edit', _CHANGED),
116
+
117
+ *_axis_rows('x'),
118
+ choice('x_secondary_axis_source_unit', 'x_secondary_axis_source_unit_combo', X_AXIS_UNIT_CHOICES, _CHANGED),
119
+ choice('x_secondary_axis_target_unit', 'x_secondary_axis_target_unit_combo', X_AXIS_UNIT_CHOICES, _CHANGED),
120
+
121
+ *_axis_rows('y'),
122
+
123
+ check('legend_visible', 'ui.legend_visible_checkbox', _CHANGED + ('_on_legend_visibility_changed',)),
124
+ # ドラッグした位置を消してから書き戻すので、ほかの欄と違う処理につなぐ
125
+ shown_text('legend_loc', 'legend_loc_combo', ('_on_legend_loc_changed',)),
126
+ # グリッドのチェックは補助グリッドと詳細の欄の有効/無効も切り替える
127
+ check('grid_visible', 'ui.grid_visible_checkbox', _CHANGED + ('_on_grid_visibility_changed',)),
128
+ check('minor_grid_visible', 'ui.minor_grid_visible_checkbox', _CHANGED + ('_on_grid_visibility_changed',)),
129
+
130
+ *_grid_rows('x', 'major'),
131
+ *_grid_rows('x', 'minor'),
132
+ *_grid_rows('y', 'major'),
133
+ *_grid_rows('y', 'minor'),
134
+ shown_text('major_tick_direction', 'major_tick_direction_combo', _CHANGED),
135
+ shown_text('minor_tick_direction', 'minor_tick_direction_combo', _CHANGED),
136
+ shown_text('major_tick_direction_y2', 'major_tick_direction_y2_combo', _CHANGED),
137
+ shown_text('minor_tick_direction_y2', 'minor_tick_direction_y2_combo', _CHANGED),
138
+ check('x_ticks_visible', 'x_ticks_visible_checkbox', _CHANGED),
139
+ check('x_tick_labels_visible', 'x_tick_labels_visible_checkbox', _CHANGED),
140
+ check('y_ticks_visible', 'y_ticks_visible_checkbox', _CHANGED),
141
+ check('y_tick_labels_visible', 'y_tick_labels_visible_checkbox', _CHANGED),
142
+
143
+ _font('tick_font', '_tick_font'),
144
+ _font('axis_label_font', '_axis_label_font'),
145
+ _font('legend_font', '_legend_font', size_only_when_given=True),
146
+ _attribute('tick_color', '_tick_color'),
147
+ _width('tick_width', 'ui.tick_width_spinbox', '_tick_width'),
148
+ number('major_tick_length', 'major_tick_length_spinbox', _CHANGED),
149
+ _minor_tick_length('minor_tick_length', 'minor_tick_length_spinbox'),
150
+ _attribute('axis_label_color', '_axis_label_color'),
151
+ _attribute('legend_color', '_legend_color'),
152
+ _width('spine_width', 'ui.spine_width_spinbox', '_spine_width'),
153
+ _attribute('spine_color', '_spine_color'),
154
+
155
+ # 2Dマップが無い軸では効かない
156
+ check('colorbar_enabled', 'colorbar_enabled_checkbox', _CHANGED),
157
+ item_data('colorbar_position', 'colorbar_position_combo', _CHANGED),
158
+ number('colorbar_width_fraction', 'colorbar_width_spinbox', _CHANGED),
159
+ text('colorbar_label', 'colorbar_label_edit', _CHANGED),
160
+
161
+ # 第 2 Y 軸。見え方(有効・無効)の切り替えもつなぐ
162
+ # 範囲を欄に入れてから設定を集め直す(_on_y2_autoscale_changed が最後に _on_axis_setting_changed を呼ぶ)。
163
+ # 先に描き直すと、欄の古い値で範囲が決まってしまい、今の範囲を入れられない
164
+ check('y2_autoscale', 'y2_autoscale_checkbox', ('_on_y2_autoscale_changed',)),
165
+ number('y2_min', 'y2_min_spinbox', _CHANGED),
166
+ number('y2_max', 'y2_max_spinbox', _CHANGED),
167
+ check('y2_log', 'y2_log_checkbox', _CHANGED + ('_on_y2_axis_state_changed',)),
168
+ check('y2_invert', 'y2_invert_checkbox', _CHANGED),
169
+ index('y2_major_tick_mode', 'y2_major_tick_mode_combo', _CHANGED + ('_on_y2_axis_state_changed',)),
170
+ number('y2_major_tick_interval', 'y2_major_tick_interval_spinbox', _CHANGED),
171
+ check('y2_minor_ticks_visible', 'y2_minor_ticks_visible_checkbox', _CHANGED + ('_on_y2_axis_state_changed',)),
172
+ number('y2_minor_tick_interval', 'y2_minor_tick_interval_spinbox', _CHANGED),
173
+ )
@@ -0,0 +1,135 @@
1
+ """画面の欄と設定の辞書のキーの対応(バインディング)を表で持ち、集める・戻す・信号を止める・つなぐを表から行う。
2
+
3
+ 表の順が戻す順になる。戻す順は値に効く(スピンボックスの範囲による丸め、途中の例外でどこまで入ったか)ので、
4
+ 行を並べ替えるときは戻したあとの欄の値が変わらないことを確かめる。
5
+ """
6
+ from dataclasses import dataclass
7
+ from typing import Any, Callable
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class Binding:
12
+ key: str
13
+ # 持ち主からの属性の道筋("ui.x_min_spinbox")。None は欄を持たず持ち主の属性に持つもの(フォントと色)
14
+ widget: str | None
15
+ # 欄から値を読む。None は戻すだけの欄(変更は専用の処理が受ける)
16
+ read: Callable[[Any, Any], Any] | None
17
+ write: Callable[[Any, Any, Any], None]
18
+ # 変更の信号の名前。None ならここではつながない
19
+ signal: str | None = None
20
+ # 信号につなぐ持ち主のメソッドの道筋("property_panel.on_x")か WATCH。つないだ順に呼ばれる
21
+ slots: tuple[Any, ...] = ()
22
+ # 保存元から値を取り出す。None ならキーで引く
23
+ load: Callable[[Any], Any] | None = None
24
+
25
+
26
+ # slots に置くと、Binder.connect に渡した watch(signal, widget) でつなぐ
27
+ WATCH = object()
28
+
29
+
30
+ def text(key, widget, slots):
31
+ return Binding(key, widget, lambda _o, w: w.text(), lambda _o, w, v: w.setText(v), 'textChanged', slots)
32
+
33
+
34
+ def check(key, widget, slots, signal='stateChanged'):
35
+ return Binding(key, widget, lambda _o, w: w.isChecked(), lambda _o, w, v: w.setChecked(v), signal, slots)
36
+
37
+
38
+ def number(key, widget, slots):
39
+ return Binding(key, widget, lambda _o, w: w.value(), lambda _o, w, v: w.setValue(v), 'valueChanged', slots)
40
+
41
+
42
+ def index(key, widget, slots):
43
+ return Binding(key, widget, lambda _o, w: w.currentIndex(), lambda _o, w, v: w.setCurrentIndex(v),
44
+ 'currentIndexChanged', slots)
45
+
46
+
47
+ def shown_text(key, widget, slots):
48
+ return Binding(key, widget, lambda _o, w: w.currentText(), lambda _o, w, v: w.setCurrentText(v),
49
+ 'currentTextChanged', slots)
50
+
51
+
52
+ def item_data(key, widget, slots):
53
+ """選択肢に持たせたデータ。見つからない値は先頭の選択肢にする。"""
54
+ def write(_owner, combo, value):
55
+ found = combo.findData(value)
56
+ combo.setCurrentIndex(found if found != -1 else 0)
57
+ return Binding(key, widget, lambda _o, w: w.currentData(), write, 'currentIndexChanged', slots)
58
+
59
+
60
+ def found_text(key, widget, slots):
61
+ """選択肢の文字。見つからない値は先頭の選択肢にする。"""
62
+ def write(_owner, combo, value):
63
+ found = combo.findText(value)
64
+ combo.setCurrentIndex(found if found != -1 else 0)
65
+ return Binding(key, widget, lambda _o, w: w.currentText(), write, 'currentTextChanged', slots)
66
+
67
+
68
+ def choice(key, widget, choices, slots):
69
+ """選択肢の番号と choices の値の対応。見つからない値は先頭の選択肢にする。"""
70
+ def write(_owner, combo, value):
71
+ combo.setCurrentIndex(choices.index(value) if value in choices else 0)
72
+ return Binding(key, widget, lambda _o, w: choices[w.currentIndex()], write, 'currentIndexChanged', slots)
73
+
74
+
75
+ def resolve(owner, path):
76
+ obj = owner
77
+ for part in path.split('.'):
78
+ obj = getattr(obj, part)
79
+ return obj
80
+
81
+
82
+ class Binder:
83
+ """持ち主(PlotterApp)と表から、集める・戻す・止める・つなぐを行う。欄は組み立ての途中で差し替わるので毎回引く。"""
84
+
85
+ def __init__(self, owner, bindings, also_blocked=()):
86
+ self._owner = owner
87
+ self._bindings = tuple(bindings)
88
+ # 表に無いが、戻す間は信号を止める欄(フォントと色のボタン)
89
+ self._also_blocked = tuple(also_blocked)
90
+
91
+ @property
92
+ def bindings(self):
93
+ return self._bindings
94
+
95
+ def _widget(self, binding):
96
+ return None if binding.widget is None else resolve(self._owner, binding.widget)
97
+
98
+ def gather(self):
99
+ return {b.key: b.read(self._owner, self._widget(b)) for b in self._bindings}
100
+
101
+ def binding_for(self, widget):
102
+ """欄から値を読む行のうち、その欄のもの。無ければ None。"""
103
+ for binding in self._bindings:
104
+ if binding.read is not None and binding.widget is not None and self._widget(binding) is widget:
105
+ return binding
106
+ return None
107
+
108
+ def read(self, binding):
109
+ return binding.read(self._owner, self._widget(binding))
110
+
111
+ def restore(self, source, lookup):
112
+ """source の値を表の順に戻す(行に load が無ければ lookup(source, key) で引く)。
113
+ 例外はそのまま上げる(そこまでに入れた欄はそのまま残る)。"""
114
+ for binding in self._bindings:
115
+ value = binding.load(source) if binding.load is not None else lookup(source, binding.key)
116
+ binding.write(self._owner, self._widget(binding), value)
117
+
118
+ def block_signals(self, block):
119
+ for binding in self._bindings:
120
+ if binding.widget is not None:
121
+ self._widget(binding).blockSignals(block)
122
+ for path in self._also_blocked:
123
+ resolve(self._owner, path).blockSignals(block)
124
+
125
+ def connect(self, watch=None):
126
+ for binding in self._bindings:
127
+ if binding.signal is None:
128
+ continue
129
+ widget = self._widget(binding)
130
+ signal = getattr(widget, binding.signal)
131
+ for slot in binding.slots:
132
+ if slot is WATCH:
133
+ watch(signal, widget)
134
+ else:
135
+ signal.connect(resolve(self._owner, slot))
@@ -0,0 +1 @@
1
+ """PlotterApp の画面をパネルごとに組み立てる関数。"""