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.
- graphica/Graphica.ico +0 -0
- graphica/__init__.py +1 -0
- graphica/__main__.py +111 -0
- graphica/assets/__init__.py +0 -0
- graphica/assets/icons/__init__.py +0 -0
- graphica/assets/icons/arrow-right.svg +21 -0
- graphica/assets/icons/bold.svg +20 -0
- graphica/assets/icons/calculator.svg +26 -0
- graphica/assets/icons/chart-histogram.svg +24 -0
- graphica/assets/icons/chart-line.svg +20 -0
- graphica/assets/icons/chevron-down.svg +19 -0
- graphica/assets/icons/chevron-right.svg +19 -0
- graphica/assets/icons/color-swatch.svg +22 -0
- graphica/assets/icons/column-insert-right.svg +21 -0
- graphica/assets/icons/column-remove.svg +21 -0
- graphica/assets/icons/copy.svg +20 -0
- graphica/assets/icons/download.svg +21 -0
- graphica/assets/icons/edit.svg +21 -0
- graphica/assets/icons/eye-off.svg +21 -0
- graphica/assets/icons/eye.svg +20 -0
- graphica/assets/icons/file-plus.svg +22 -0
- graphica/assets/icons/folder-plus.svg +21 -0
- graphica/assets/icons/folder.svg +19 -0
- graphica/assets/icons/highlight.svg +22 -0
- graphica/assets/icons/history.svg +20 -0
- graphica/assets/icons/italic.svg +21 -0
- graphica/assets/icons/layout-grid.svg +22 -0
- graphica/assets/icons/math-function.svg +22 -0
- graphica/assets/icons/message-2.svg +21 -0
- graphica/assets/icons/mountain.svg +20 -0
- graphica/assets/icons/palette.svg +22 -0
- graphica/assets/icons/pointer.svg +19 -0
- graphica/assets/icons/refresh.svg +20 -0
- graphica/assets/icons/row-insert-bottom.svg +21 -0
- graphica/assets/icons/row-remove.svg +21 -0
- graphica/assets/icons/search.svg +20 -0
- graphica/assets/icons/select-all.svg +35 -0
- graphica/assets/icons/subscript.svg +20 -0
- graphica/assets/icons/superscript.svg +20 -0
- graphica/assets/icons/table.svg +21 -0
- graphica/assets/icons/trash.svg +23 -0
- graphica/assets/icons/typography.svg +23 -0
- graphica/assets/icons/x.svg +20 -0
- graphica/core/__init__.py +0 -0
- graphica/core/analysis.py +1278 -0
- graphica/core/app_paths.py +35 -0
- graphica/core/axis_settings.py +128 -0
- graphica/core/caption_export.py +47 -0
- graphica/core/color_palettes.py +56 -0
- graphica/core/commands.py +226 -0
- graphica/core/cvd_simulation.py +49 -0
- graphica/core/dataset.py +443 -0
- graphica/core/diagnostics.py +105 -0
- graphica/core/excel_utils.py +58 -0
- graphica/core/fit_models.py +379 -0
- graphica/core/grid_data.py +128 -0
- graphica/core/i18n.py +44 -0
- graphica/core/json_utils.py +27 -0
- graphica/core/label_utils.py +21 -0
- graphica/core/methods_text.py +115 -0
- graphica/core/named_colors.py +126 -0
- graphica/core/plugin_api.py +515 -0
- graphica/core/plugin_context.py +167 -0
- graphica/core/plugin_install.py +82 -0
- graphica/core/plugin_manifest.py +67 -0
- graphica/core/plugin_testing.py +213 -0
- graphica/core/plugin_types.py +118 -0
- graphica/core/provenance.py +19 -0
- graphica/core/report_export.py +51 -0
- graphica/core/safe_eval.py +194 -0
- graphica/core/script_export.py +272 -0
- graphica/core/translations_en.py +530 -0
- graphica/core/unit_conversion.py +51 -0
- graphica/core/update_check.py +43 -0
- graphica/core/version.py +14 -0
- graphica/gui/__init__.py +0 -0
- graphica/gui/app_settings.py +119 -0
- graphica/gui/axis_bindings.py +173 -0
- graphica/gui/binding.py +135 -0
- graphica/gui/builders/__init__.py +1 -0
- graphica/gui/builders/axis_panel.py +431 -0
- graphica/gui/builders/canvas_area.py +176 -0
- graphica/gui/builders/common.py +120 -0
- graphica/gui/builders/dataset_panel.py +433 -0
- graphica/gui/builders/property_sections.py +208 -0
- graphica/gui/canvas.py +519 -0
- graphica/gui/color_history.py +47 -0
- graphica/gui/color_picker_widget.py +198 -0
- graphica/gui/crash_handler.py +77 -0
- graphica/gui/cvd_preview.py +23 -0
- graphica/gui/data_editor.py +761 -0
- graphica/gui/data_import_flow.py +355 -0
- graphica/gui/dataset_bindings.py +107 -0
- graphica/gui/dataset_style_icon.py +80 -0
- graphica/gui/datasets/__init__.py +1 -0
- graphica/gui/datasets/actions_menu.py +114 -0
- graphica/gui/datasets/colors.py +218 -0
- graphica/gui/datasets/fitting.py +70 -0
- graphica/gui/datasets/host.py +162 -0
- graphica/gui/datasets/operations/__init__.py +1 -0
- graphica/gui/datasets/operations/fitting.py +468 -0
- graphica/gui/datasets/operations/peaks.py +109 -0
- graphica/gui/datasets/operations/processing.py +659 -0
- graphica/gui/datasets/operations/runner.py +148 -0
- graphica/gui/datasets/operations/transfer.py +227 -0
- graphica/gui/datasets/order.py +310 -0
- graphica/gui/datasets/overlays.py +74 -0
- graphica/gui/datasets/peaks.py +21 -0
- graphica/gui/datasets/plugin_runs.py +99 -0
- graphica/gui/datasets/processing.py +68 -0
- graphica/gui/datasets/property_panel.py +538 -0
- graphica/gui/datasets/transfer.py +36 -0
- graphica/gui/detached_canvas_window.py +22 -0
- graphica/gui/dialogs/__init__.py +137 -0
- graphica/gui/dialogs/analysis.py +1322 -0
- graphica/gui/dialogs/app.py +1015 -0
- graphica/gui/dialogs/appearance.py +851 -0
- graphica/gui/dialogs/data_edit.py +577 -0
- graphica/gui/dialogs/data_import.py +679 -0
- graphica/gui/dialogs/export.py +433 -0
- graphica/gui/dock_layout.py +176 -0
- graphica/gui/export_preview_panel.py +321 -0
- graphica/gui/export_settings.py +16 -0
- graphica/gui/file_association.py +97 -0
- graphica/gui/icon_utils.py +44 -0
- graphica/gui/main_app_window.py +210 -0
- graphica/gui/main_window.py +1151 -0
- graphica/gui/mathtext_preview.py +168 -0
- graphica/gui/menu_bar.py +320 -0
- graphica/gui/minimap_widget.py +151 -0
- graphica/gui/mixins/__init__.py +0 -0
- graphica/gui/mixins/export_mixin.py +520 -0
- graphica/gui/mixins/help_mixin.py +127 -0
- graphica/gui/mixins/project_io_mixin.py +289 -0
- graphica/gui/mixins/quick_access_mixin.py +194 -0
- graphica/gui/mixins/ui_setup_mixin.py +206 -0
- graphica/gui/notify.py +61 -0
- graphica/gui/panels/__init__.py +43 -0
- graphica/gui/panels/axis_settings.py +605 -0
- graphica/gui/panels/dataset_tree.py +424 -0
- graphica/gui/plot_type_drawers.py +131 -0
- graphica/gui/plugin_context.py +119 -0
- graphica/gui/project_files.py +361 -0
- graphica/gui/provenance_panel.py +65 -0
- graphica/gui/rendering/__init__.py +1 -0
- graphica/gui/rendering/annotations.py +177 -0
- graphica/gui/rendering/appearance.py +368 -0
- graphica/gui/rendering/common.py +316 -0
- graphica/gui/rendering/data_1d.py +376 -0
- graphica/gui/rendering/data_2d.py +66 -0
- graphica/gui/residual_panel.py +62 -0
- graphica/gui/resources.py +16 -0
- graphica/gui/single_instance.py +141 -0
- graphica/gui/splash.py +83 -0
- graphica/gui/task_runner.py +41 -0
- graphica/gui/theme.py +864 -0
- graphica/gui/tools/__init__.py +56 -0
- graphica/gui/tools/annotation.py +275 -0
- graphica/gui/tools/cursor.py +259 -0
- graphica/gui/tools/layout_edit.py +285 -0
- graphica/gui/tools/manager.py +78 -0
- graphica/gui/tools/peak_placement.py +132 -0
- graphica/gui/tools/range_select.py +191 -0
- graphica/gui/tools/region_highlight.py +222 -0
- graphica/gui/tools/slice_extraction.py +200 -0
- graphica/gui/widget_translation.py +46 -0
- graphica/gui/workers.py +280 -0
- graphica/models/__init__.py +0 -0
- graphica/models/project.py +197 -0
- graphica/plugin/__init__.py +20 -0
- graphica/plugin/testing.py +57 -0
- graphica/sample_data/__init__.py +0 -0
- graphica/sample_data/cooling_curve_sample.csv +42 -0
- graphica/ui_main_window.py +581 -0
- graphica_plot-2.0.0.dist-info/METADATA +317 -0
- graphica_plot-2.0.0.dist-info/RECORD +181 -0
- graphica_plot-2.0.0.dist-info/WHEEL +5 -0
- graphica_plot-2.0.0.dist-info/entry_points.txt +2 -0
- graphica_plot-2.0.0.dist-info/licenses/LICENSE +30 -0
- graphica_plot-2.0.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +91 -0
- graphica_plot-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
"""見た目と注釈のダイアログ。呼び出し側は `from graphica.gui.dialogs import X` で参照する。"""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from PySide6.QtWidgets import (
|
|
5
|
+
QColorDialog,
|
|
6
|
+
QComboBox,
|
|
7
|
+
QDialog,
|
|
8
|
+
QDialogButtonBox,
|
|
9
|
+
QDoubleSpinBox,
|
|
10
|
+
QFormLayout,
|
|
11
|
+
QGridLayout,
|
|
12
|
+
QHBoxLayout,
|
|
13
|
+
QLabel,
|
|
14
|
+
QLineEdit,
|
|
15
|
+
QListWidget,
|
|
16
|
+
QListWidgetItem,
|
|
17
|
+
QMenu,
|
|
18
|
+
QMessageBox,
|
|
19
|
+
QPushButton,
|
|
20
|
+
QToolButton,
|
|
21
|
+
QVBoxLayout,
|
|
22
|
+
QWidget,
|
|
23
|
+
QWidgetAction,
|
|
24
|
+
)
|
|
25
|
+
from PySide6.QtCore import QSize, Qt
|
|
26
|
+
from PySide6.QtGui import QColor, QFont, QIcon, QPixmap
|
|
27
|
+
from graphica.gui import notify
|
|
28
|
+
from graphica.gui import icon_utils
|
|
29
|
+
from graphica.gui.color_history import get_color_with_history
|
|
30
|
+
from graphica.gui.theme import apply_form_spacing
|
|
31
|
+
from graphica.gui.mathtext_preview import FitWidthPixmapLabel
|
|
32
|
+
from graphica.core.i18n import tr
|
|
33
|
+
from graphica.core.color_palettes import BUILTIN_PALETTES, default_color_cycle
|
|
34
|
+
from graphica.core.named_colors import (
|
|
35
|
+
NamedColorError,
|
|
36
|
+
add_named_color,
|
|
37
|
+
load_named_colors,
|
|
38
|
+
move_named_color,
|
|
39
|
+
remove_named_color,
|
|
40
|
+
save_named_colors,
|
|
41
|
+
update_named_color,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class LabelEditDialog(QDialog):
|
|
46
|
+
"""タイトルと軸ラベルの編集。装飾ボタンは選択範囲を mathtext で包み、記号はカーソル位置に入れる。"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, initial_text, window_title, symbol_palette, parent=None):
|
|
49
|
+
"""symbol_palette は main_window の LABEL_SYMBOL_PALETTE(dialogs は main_window を import できないので渡してもらう)。"""
|
|
50
|
+
super().__init__(parent)
|
|
51
|
+
self.setWindowTitle(window_title)
|
|
52
|
+
self.setMinimumWidth(420)
|
|
53
|
+
|
|
54
|
+
layout = QVBoxLayout(self)
|
|
55
|
+
|
|
56
|
+
self.text_edit = QLineEdit(initial_text)
|
|
57
|
+
self.text_edit.setMinimumHeight(34)
|
|
58
|
+
larger_font = QFont(self.text_edit.font())
|
|
59
|
+
larger_font.setPointSize(larger_font.pointSize() + 1)
|
|
60
|
+
self.text_edit.setFont(larger_font)
|
|
61
|
+
layout.addWidget(self.text_edit)
|
|
62
|
+
|
|
63
|
+
# QLineEdit では装飾を見せられないので、描いた結果を下に出す
|
|
64
|
+
self.preview_label = FitWidthPixmapLabel()
|
|
65
|
+
self.preview_label.setObjectName("mathtext_preview_label")
|
|
66
|
+
self.preview_label.setMinimumHeight(36)
|
|
67
|
+
self.preview_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
|
68
|
+
layout.addWidget(self.preview_label)
|
|
69
|
+
self.text_edit.textChanged.connect(self._refresh_preview)
|
|
70
|
+
|
|
71
|
+
button_row = QHBoxLayout()
|
|
72
|
+
button_row.setSpacing(4)
|
|
73
|
+
|
|
74
|
+
def _make_icon_button(icon_name, tooltip):
|
|
75
|
+
button = QPushButton()
|
|
76
|
+
button.setIcon(icon_utils.icon(icon_name, size=16))
|
|
77
|
+
button.setToolTip(tooltip)
|
|
78
|
+
button.setProperty("iconOnly", True)
|
|
79
|
+
button.setFixedSize(28, 28)
|
|
80
|
+
# フォーカスを取るとテキスト欄の選択が消える(下の説明)
|
|
81
|
+
button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
|
82
|
+
button_row.addWidget(button)
|
|
83
|
+
return button
|
|
84
|
+
|
|
85
|
+
bold_button = _make_icon_button("bold", "太字")
|
|
86
|
+
italic_button = _make_icon_button("italic", "イタリック")
|
|
87
|
+
superscript_button = _make_icon_button("superscript", "上付き文字")
|
|
88
|
+
subscript_button = _make_icon_button("subscript", "下付き文字")
|
|
89
|
+
|
|
90
|
+
# ボタンがフォーカスを取ると、pressed が出るより前にテキスト欄の選択が消える。
|
|
91
|
+
# だから装飾ボタンはフォーカスを取らない。pressed で選択を控えるのは、それでも移る環境のための保険
|
|
92
|
+
self._pending_selection = None # (start, selected_text)、選択が無ければ None
|
|
93
|
+
self._pending_cursor = 0
|
|
94
|
+
|
|
95
|
+
for button in (bold_button, italic_button, superscript_button, subscript_button):
|
|
96
|
+
button.pressed.connect(self._capture_pending_selection)
|
|
97
|
+
|
|
98
|
+
bold_button.clicked.connect(lambda: self._apply_wrap("bold", lambda s: f"\\mathbf{{{s}}}"))
|
|
99
|
+
italic_button.clicked.connect(lambda: self._apply_wrap("italic", lambda s: f"\\mathit{{{s}}}"))
|
|
100
|
+
superscript_button.clicked.connect(lambda: self._apply_wrap("super", lambda s: f"{{}}^{{{s}}}"))
|
|
101
|
+
subscript_button.clicked.connect(lambda: self._apply_wrap("sub", lambda s: f"{{}}_{{{s}}}"))
|
|
102
|
+
|
|
103
|
+
# 記号は32個あり、常に並べると場所を取るのでドロップダウンにする
|
|
104
|
+
symbol_button = QToolButton()
|
|
105
|
+
symbol_button.setText("Ω")
|
|
106
|
+
symbol_button.setToolTip("ギリシャ文字・数学記号を挿入")
|
|
107
|
+
symbol_button.setProperty("iconOnly", True)
|
|
108
|
+
symbol_button.setFixedSize(28, 28)
|
|
109
|
+
symbol_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
|
|
110
|
+
# 既定でも NoFocus だが、装飾ボタンと同じ理由で明示する
|
|
111
|
+
symbol_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
|
112
|
+
# メニューを開く動作でも選択が消えうるので、開く前に控える
|
|
113
|
+
symbol_button.pressed.connect(self._capture_pending_selection)
|
|
114
|
+
|
|
115
|
+
symbol_menu = QMenu(symbol_button)
|
|
116
|
+
symbol_panel = QWidget()
|
|
117
|
+
symbol_grid = QGridLayout(symbol_panel)
|
|
118
|
+
symbol_grid.setContentsMargins(6, 6, 6, 6)
|
|
119
|
+
symbol_grid.setSpacing(2)
|
|
120
|
+
# 既定の大きさでは記号が読みにくい
|
|
121
|
+
symbol_font = QFont(self.font())
|
|
122
|
+
symbol_font.setPointSize(symbol_font.pointSize() + 4)
|
|
123
|
+
for index, (glyph, macro) in enumerate(symbol_palette):
|
|
124
|
+
item_button = QToolButton()
|
|
125
|
+
item_button.setText(glyph)
|
|
126
|
+
item_button.setFont(symbol_font)
|
|
127
|
+
# macro=None は mathtext のマクロを持たない文字そのもの(マイナス記号など)
|
|
128
|
+
item_button.setToolTip(f"\\{macro}" if macro else glyph)
|
|
129
|
+
item_button.setFixedSize(30, 30)
|
|
130
|
+
item_button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
|
131
|
+
item_button.clicked.connect(
|
|
132
|
+
lambda checked=False, g=glyph, m=macro, sm=symbol_menu: (self._insert_symbol(g, m), sm.close())
|
|
133
|
+
)
|
|
134
|
+
symbol_grid.addWidget(item_button, index // 4, index % 4)
|
|
135
|
+
symbol_widget_action = QWidgetAction(symbol_button)
|
|
136
|
+
symbol_widget_action.setDefaultWidget(symbol_panel)
|
|
137
|
+
symbol_menu.addAction(symbol_widget_action)
|
|
138
|
+
symbol_button.setMenu(symbol_menu)
|
|
139
|
+
button_row.addWidget(symbol_button)
|
|
140
|
+
|
|
141
|
+
button_row.addStretch(1)
|
|
142
|
+
layout.addLayout(button_row)
|
|
143
|
+
|
|
144
|
+
button_box = QDialogButtonBox(
|
|
145
|
+
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
|
146
|
+
)
|
|
147
|
+
button_box.accepted.connect(self.accept)
|
|
148
|
+
button_box.rejected.connect(self.reject)
|
|
149
|
+
layout.addWidget(button_box)
|
|
150
|
+
|
|
151
|
+
self._refresh_preview()
|
|
152
|
+
|
|
153
|
+
def _refresh_preview(self):
|
|
154
|
+
"""テキスト欄の内容を描いてプレビューに出す(タイトル欄のプレビューと同じ描き方)。"""
|
|
155
|
+
from graphica.gui import theme
|
|
156
|
+
from graphica.gui.mathtext_preview import render_mathtext_to_pixmap
|
|
157
|
+
|
|
158
|
+
tokens = theme.current_tokens()
|
|
159
|
+
text = self.text_edit.text()
|
|
160
|
+
color = tokens["text_primary"] if text else tokens["text_muted"]
|
|
161
|
+
pixmap = render_mathtext_to_pixmap(text if text else " ", color=color, fontsize=15)
|
|
162
|
+
self.preview_label.set_natural_pixmap(pixmap)
|
|
163
|
+
|
|
164
|
+
def _capture_pending_selection(self):
|
|
165
|
+
"""ボタンが押された瞬間の選択範囲を控える(clicked まで待つと消えている環境がある)。"""
|
|
166
|
+
if self.text_edit.hasSelectedText():
|
|
167
|
+
self._pending_selection = (
|
|
168
|
+
self.text_edit.selectionStart(), self.text_edit.selectedText()
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
self._pending_selection = None
|
|
172
|
+
self._pending_cursor = self.text_edit.cursorPosition()
|
|
173
|
+
|
|
174
|
+
# 選択が $...$ で囲んだ1つの断片か(直前の装飾の結果をさらに装飾するとき)
|
|
175
|
+
_MATH_SPAN_RE = re.compile(r'^\$(.*)\$$', re.DOTALL)
|
|
176
|
+
_MATHBF_RE = re.compile(r'^\\mathbf\{(.*)\}$', re.DOTALL)
|
|
177
|
+
_MATHIT_RE = re.compile(r'^\\mathit\{(.*)\}$', re.DOTALL)
|
|
178
|
+
_BOLDSYMBOL_RE = re.compile(r'^\\boldsymbol\{(.*)\}$', re.DOTALL)
|
|
179
|
+
_SUPER_RE = re.compile(r'^\{\}\^\{(.*)\}$', re.DOTALL)
|
|
180
|
+
_SUB_RE = re.compile(r'^\{\}_\{(.*)\}$', re.DOTALL)
|
|
181
|
+
|
|
182
|
+
def _apply_wrap(self, kind, wrap_fn):
|
|
183
|
+
"""選択範囲を wrap_fn の中身で包み、$...$ で囲む。kind は "bold" / "italic" / "super" / "sub"。
|
|
184
|
+
|
|
185
|
+
選択が既に $...$ の断片なら、中身を取り出してから包み直す($ が入れ子になると壊れた構文になる)。
|
|
186
|
+
\\mathbf と \\mathit は入れ子にしても合成されないので、両方なら \\boldsymbol にする。
|
|
187
|
+
同じ装飾がもう外側にかかっていれば外す(連打で入れ子が積み重ならないように)。
|
|
188
|
+
装飾が無くなれば $ も外す(mathtext の中の裸の文字は斜体になる)。
|
|
189
|
+
"""
|
|
190
|
+
if not self._pending_selection:
|
|
191
|
+
# 選択が無ければ何もしない(案内は出さない)
|
|
192
|
+
return
|
|
193
|
+
start, selected = self._pending_selection
|
|
194
|
+
span_match = self._MATH_SPAN_RE.match(selected)
|
|
195
|
+
inner = span_match.group(1) if span_match else selected
|
|
196
|
+
|
|
197
|
+
combined = None
|
|
198
|
+
if kind == "bold":
|
|
199
|
+
m = self._BOLDSYMBOL_RE.match(inner)
|
|
200
|
+
if m:
|
|
201
|
+
combined = f"\\mathit{{{m.group(1)}}}" # 太字だけ外し、イタリックは残す
|
|
202
|
+
else:
|
|
203
|
+
m = self._MATHIT_RE.match(inner)
|
|
204
|
+
if m:
|
|
205
|
+
combined = f"\\boldsymbol{{{m.group(1)}}}"
|
|
206
|
+
else:
|
|
207
|
+
m = self._MATHBF_RE.match(inner)
|
|
208
|
+
if m:
|
|
209
|
+
combined = m.group(1)
|
|
210
|
+
elif kind == "italic":
|
|
211
|
+
m = self._BOLDSYMBOL_RE.match(inner)
|
|
212
|
+
if m:
|
|
213
|
+
combined = f"\\mathbf{{{m.group(1)}}}" # イタリックだけ外し、太字は残す
|
|
214
|
+
else:
|
|
215
|
+
m = self._MATHBF_RE.match(inner)
|
|
216
|
+
if m:
|
|
217
|
+
combined = f"\\boldsymbol{{{m.group(1)}}}"
|
|
218
|
+
else:
|
|
219
|
+
m = self._MATHIT_RE.match(inner)
|
|
220
|
+
if m:
|
|
221
|
+
combined = m.group(1)
|
|
222
|
+
elif kind == "super":
|
|
223
|
+
m = self._SUPER_RE.match(inner)
|
|
224
|
+
if m:
|
|
225
|
+
combined = m.group(1)
|
|
226
|
+
elif kind == "sub":
|
|
227
|
+
m = self._SUB_RE.match(inner)
|
|
228
|
+
if m:
|
|
229
|
+
combined = m.group(1)
|
|
230
|
+
|
|
231
|
+
new_inner = combined if combined is not None else wrap_fn(inner)
|
|
232
|
+
if re.search(r'[\\^_{}]', new_inner):
|
|
233
|
+
replacement = f"${new_inner}$"
|
|
234
|
+
else:
|
|
235
|
+
replacement = new_inner
|
|
236
|
+
text = self.text_edit.text()
|
|
237
|
+
self.text_edit.setText(text[:start] + replacement + text[start + len(selected):])
|
|
238
|
+
self.text_edit.setSelection(start, len(replacement))
|
|
239
|
+
|
|
240
|
+
def _insert_symbol(self, glyph, macro):
|
|
241
|
+
"""記号を選択範囲に置き換えるか、カーソル位置に入れる。macro が None なら $ で包まない(包むと斜体になる)。"""
|
|
242
|
+
text = self.text_edit.text()
|
|
243
|
+
if self._pending_selection:
|
|
244
|
+
start, selected = self._pending_selection
|
|
245
|
+
end = start + len(selected)
|
|
246
|
+
else:
|
|
247
|
+
start = end = self._pending_cursor
|
|
248
|
+
replacement = glyph if macro is None else f"$\\{macro}$"
|
|
249
|
+
self.text_edit.setText(text[:start] + replacement + text[end:])
|
|
250
|
+
self.text_edit.setCursorPosition(start + len(replacement))
|
|
251
|
+
|
|
252
|
+
def get_text(self):
|
|
253
|
+
return self.text_edit.text()
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class ColorPaletteDialog(QDialog):
|
|
257
|
+
"""「自動配色」のパレットを作り、選ぶ。「Matplotlib既定」と組み込みのパレットは読み取り専用。"""
|
|
258
|
+
|
|
259
|
+
DEFAULT_PALETTE_NAME = "Matplotlib既定"
|
|
260
|
+
|
|
261
|
+
def __init__(self, palettes: dict, active_name: str, parent=None):
|
|
262
|
+
super().__init__(parent)
|
|
263
|
+
self.setWindowTitle("配色パレットの管理")
|
|
264
|
+
self.resize(420, 440)
|
|
265
|
+
|
|
266
|
+
# キャンセルしたとき元に戻せるよう、呼び出し側の辞書は変えない
|
|
267
|
+
self.palettes = {name: list(colors) for name, colors in palettes.items()}
|
|
268
|
+
|
|
269
|
+
layout = QVBoxLayout(self)
|
|
270
|
+
|
|
271
|
+
combo_layout = QHBoxLayout()
|
|
272
|
+
combo_layout.addWidget(QLabel("パレット"))
|
|
273
|
+
self.palette_combo = QComboBox()
|
|
274
|
+
self.palette_combo.addItem(self.DEFAULT_PALETTE_NAME)
|
|
275
|
+
self.palette_combo.addItems(sorted(BUILTIN_PALETTES.keys()))
|
|
276
|
+
self.palette_combo.addItems(sorted(self.palettes.keys()))
|
|
277
|
+
combo_layout.addWidget(self.palette_combo, stretch=1)
|
|
278
|
+
layout.addLayout(combo_layout)
|
|
279
|
+
|
|
280
|
+
palette_button_layout = QHBoxLayout()
|
|
281
|
+
self.new_palette_button = QPushButton("新規パレット...")
|
|
282
|
+
self.rename_palette_button = QPushButton("名前を変更...")
|
|
283
|
+
self.delete_palette_button = QPushButton("削除")
|
|
284
|
+
palette_button_layout.addWidget(self.new_palette_button)
|
|
285
|
+
palette_button_layout.addWidget(self.rename_palette_button)
|
|
286
|
+
palette_button_layout.addWidget(self.delete_palette_button)
|
|
287
|
+
layout.addLayout(palette_button_layout)
|
|
288
|
+
|
|
289
|
+
self.color_list = QListWidget()
|
|
290
|
+
layout.addWidget(self.color_list)
|
|
291
|
+
|
|
292
|
+
color_button_layout = QHBoxLayout()
|
|
293
|
+
self.add_color_button = QPushButton("色を追加...")
|
|
294
|
+
self.remove_color_button = QPushButton("選択した色を削除")
|
|
295
|
+
color_button_layout.addWidget(self.add_color_button)
|
|
296
|
+
color_button_layout.addWidget(self.remove_color_button)
|
|
297
|
+
color_button_layout.addStretch()
|
|
298
|
+
layout.addLayout(color_button_layout)
|
|
299
|
+
|
|
300
|
+
info_label = QLabel("「OK」を押すと、選択中のパレットが以後「自動配色」ボタンで使われます。")
|
|
301
|
+
info_label.setWordWrap(True)
|
|
302
|
+
layout.addWidget(info_label)
|
|
303
|
+
|
|
304
|
+
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
305
|
+
QDialogButtonBox.StandardButton.Cancel)
|
|
306
|
+
button_box.accepted.connect(self.accept)
|
|
307
|
+
button_box.rejected.connect(self.reject)
|
|
308
|
+
layout.addWidget(button_box)
|
|
309
|
+
|
|
310
|
+
self.palette_combo.currentTextChanged.connect(self._on_palette_selected)
|
|
311
|
+
self.new_palette_button.clicked.connect(self._on_new_palette)
|
|
312
|
+
self.rename_palette_button.clicked.connect(self._on_rename_palette)
|
|
313
|
+
self.delete_palette_button.clicked.connect(self._on_delete_palette)
|
|
314
|
+
self.add_color_button.clicked.connect(self._on_add_color)
|
|
315
|
+
self.remove_color_button.clicked.connect(self._on_remove_color)
|
|
316
|
+
|
|
317
|
+
# 組み込みのパレットも初期の選択にする(でないと既定の表示のまま OK を押して上書きしてしまう)
|
|
318
|
+
if active_name in self.palettes or active_name in BUILTIN_PALETTES:
|
|
319
|
+
self.palette_combo.setCurrentText(active_name)
|
|
320
|
+
else:
|
|
321
|
+
self.palette_combo.setCurrentText(self.DEFAULT_PALETTE_NAME)
|
|
322
|
+
self._refresh_color_list()
|
|
323
|
+
self._update_button_states()
|
|
324
|
+
|
|
325
|
+
def _is_default_selected(self):
|
|
326
|
+
return self._is_readonly_palette(self.palette_combo.currentText())
|
|
327
|
+
|
|
328
|
+
def _is_readonly_palette(self, name):
|
|
329
|
+
"""「Matplotlib既定」か組み込みのパレット(どちらも編集できない)。"""
|
|
330
|
+
return name == self.DEFAULT_PALETTE_NAME or name in BUILTIN_PALETTES
|
|
331
|
+
|
|
332
|
+
def _update_button_states(self):
|
|
333
|
+
editable = not self._is_default_selected()
|
|
334
|
+
self.rename_palette_button.setEnabled(editable)
|
|
335
|
+
self.delete_palette_button.setEnabled(editable)
|
|
336
|
+
self.add_color_button.setEnabled(editable)
|
|
337
|
+
self.remove_color_button.setEnabled(editable)
|
|
338
|
+
|
|
339
|
+
def _refresh_color_list(self):
|
|
340
|
+
# 行は QSS に任せず色見本+テーマの文字色のラベルにする。QSS で ::item に何か当てると、
|
|
341
|
+
# setBackground / setForeground が無視され、明るい色や暗い色の行が読めなくなる
|
|
342
|
+
self.color_list.clear()
|
|
343
|
+
name = self.palette_combo.currentText()
|
|
344
|
+
if name == self.DEFAULT_PALETTE_NAME:
|
|
345
|
+
colors = default_color_cycle()
|
|
346
|
+
elif name in BUILTIN_PALETTES:
|
|
347
|
+
colors = BUILTIN_PALETTES[name]
|
|
348
|
+
else:
|
|
349
|
+
colors = self.palettes.get(name, [])
|
|
350
|
+
from graphica.gui import theme
|
|
351
|
+
border_color = theme.current_tokens()["border_strong"]
|
|
352
|
+
for color_hex in colors:
|
|
353
|
+
item = QListWidgetItem()
|
|
354
|
+
self.color_list.addItem(item)
|
|
355
|
+
|
|
356
|
+
row_widget = QWidget()
|
|
357
|
+
row_layout = QHBoxLayout(row_widget)
|
|
358
|
+
row_layout.setContentsMargins(6, 2, 6, 2)
|
|
359
|
+
row_layout.setSpacing(8)
|
|
360
|
+
|
|
361
|
+
swatch = QLabel()
|
|
362
|
+
swatch.setFixedSize(20, 20)
|
|
363
|
+
swatch.setStyleSheet(
|
|
364
|
+
f"background-color: {color_hex}; border: 1px solid {border_color}; "
|
|
365
|
+
f"border-radius: 4px;"
|
|
366
|
+
)
|
|
367
|
+
row_layout.addWidget(swatch)
|
|
368
|
+
row_layout.addWidget(QLabel(color_hex), 1)
|
|
369
|
+
|
|
370
|
+
item.setSizeHint(row_widget.sizeHint())
|
|
371
|
+
self.color_list.setItemWidget(item, row_widget)
|
|
372
|
+
|
|
373
|
+
def _on_palette_selected(self, _name):
|
|
374
|
+
self._refresh_color_list()
|
|
375
|
+
self._update_button_states()
|
|
376
|
+
|
|
377
|
+
def _on_new_palette(self):
|
|
378
|
+
name, ok = notify.get_text(self, "新規パレット", "パレット名")
|
|
379
|
+
if not ok or not name:
|
|
380
|
+
return
|
|
381
|
+
if self._is_readonly_palette(name) or name in self.palettes:
|
|
382
|
+
notify.warning(self, "エラー", f"パレット名 '{name}' は既に使われています。")
|
|
383
|
+
return
|
|
384
|
+
self.palettes[name] = []
|
|
385
|
+
self.palette_combo.addItem(name)
|
|
386
|
+
self.palette_combo.setCurrentText(name)
|
|
387
|
+
|
|
388
|
+
def _on_rename_palette(self):
|
|
389
|
+
old_name = self.palette_combo.currentText()
|
|
390
|
+
if self._is_readonly_palette(old_name):
|
|
391
|
+
return
|
|
392
|
+
new_name, ok = notify.get_text(self, "名前を変更", "新しいパレット名", text=old_name)
|
|
393
|
+
if not ok or not new_name or new_name == old_name:
|
|
394
|
+
return
|
|
395
|
+
if self._is_readonly_palette(new_name) or new_name in self.palettes:
|
|
396
|
+
notify.warning(self, "エラー", f"パレット名 '{new_name}' は既に使われています。")
|
|
397
|
+
return
|
|
398
|
+
self.palettes[new_name] = self.palettes.pop(old_name)
|
|
399
|
+
self.palette_combo.setItemText(self.palette_combo.currentIndex(), new_name)
|
|
400
|
+
|
|
401
|
+
def _on_delete_palette(self):
|
|
402
|
+
name = self.palette_combo.currentText()
|
|
403
|
+
if self._is_readonly_palette(name):
|
|
404
|
+
return
|
|
405
|
+
reply = notify.question(
|
|
406
|
+
self, "パレットを削除", f"パレット '{name}' を削除しますか?",
|
|
407
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
408
|
+
)
|
|
409
|
+
if reply != QMessageBox.StandardButton.Yes:
|
|
410
|
+
return
|
|
411
|
+
del self.palettes[name]
|
|
412
|
+
self.palette_combo.removeItem(self.palette_combo.currentIndex())
|
|
413
|
+
|
|
414
|
+
def _on_add_color(self):
|
|
415
|
+
name = self.palette_combo.currentText()
|
|
416
|
+
if self._is_readonly_palette(name):
|
|
417
|
+
return
|
|
418
|
+
color = QColorDialog.getColor()
|
|
419
|
+
if not color.isValid():
|
|
420
|
+
return
|
|
421
|
+
self.palettes.setdefault(name, []).append(color.name())
|
|
422
|
+
self._refresh_color_list()
|
|
423
|
+
|
|
424
|
+
def _on_remove_color(self):
|
|
425
|
+
name = self.palette_combo.currentText()
|
|
426
|
+
if self._is_readonly_palette(name):
|
|
427
|
+
return
|
|
428
|
+
row = self.color_list.currentRow()
|
|
429
|
+
if row < 0:
|
|
430
|
+
return
|
|
431
|
+
del self.palettes[name][row]
|
|
432
|
+
self._refresh_color_list()
|
|
433
|
+
|
|
434
|
+
def get_result(self):
|
|
435
|
+
"""(パレットの辞書, 選んだパレット名)。QSettings への保存は呼び出し側。"""
|
|
436
|
+
return self.palettes, self.palette_combo.currentText()
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _named_color_icon(color_name, size=16):
|
|
440
|
+
"""リスト行の先頭に出す色見本(色欄のポップアップと同じ見た目に揃える)。"""
|
|
441
|
+
from graphica.gui.color_picker_widget import _color_icon
|
|
442
|
+
return _color_icon(color_name, size=size)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class NamedColorManagerDialog(QDialog):
|
|
446
|
+
"""名前を付けて登録した色(core/named_colors.py)の管理。配色パレット(色の並び)とは別物。
|
|
447
|
+
|
|
448
|
+
登録簿を育てる操作で、プロットの見た目は変えないので、OK を待たずにその場で保存する。
|
|
449
|
+
"""
|
|
450
|
+
|
|
451
|
+
def __init__(self, settings, parent=None):
|
|
452
|
+
super().__init__(parent)
|
|
453
|
+
self.setWindowTitle(tr("色名の管理"))
|
|
454
|
+
self.resize(420, 400)
|
|
455
|
+
self._settings = settings
|
|
456
|
+
self.entries = load_named_colors(settings)
|
|
457
|
+
|
|
458
|
+
layout = QVBoxLayout(self)
|
|
459
|
+
|
|
460
|
+
description = QLabel(tr(
|
|
461
|
+
"よく使う色に名前を付けて登録します。データセットの色欄で"
|
|
462
|
+
"スウォッチを押すと、ここに登録した色を名前から選べます。"))
|
|
463
|
+
description.setWordWrap(True)
|
|
464
|
+
layout.addWidget(description)
|
|
465
|
+
|
|
466
|
+
self.color_list = QListWidget()
|
|
467
|
+
self.color_list.setIconSize(QSize(16, 16))
|
|
468
|
+
self.color_list.itemDoubleClicked.connect(lambda _item: self._on_edit())
|
|
469
|
+
layout.addWidget(self.color_list, stretch=1)
|
|
470
|
+
|
|
471
|
+
button_layout = QHBoxLayout()
|
|
472
|
+
self.add_button = QPushButton(tr("追加..."))
|
|
473
|
+
self.edit_button = QPushButton(tr("編集..."))
|
|
474
|
+
self.delete_button = QPushButton(tr("削除"))
|
|
475
|
+
self.up_button = QPushButton(tr("上へ"))
|
|
476
|
+
self.down_button = QPushButton(tr("下へ"))
|
|
477
|
+
for button in (self.add_button, self.edit_button, self.delete_button):
|
|
478
|
+
button_layout.addWidget(button)
|
|
479
|
+
button_layout.addStretch()
|
|
480
|
+
button_layout.addWidget(self.up_button)
|
|
481
|
+
button_layout.addWidget(self.down_button)
|
|
482
|
+
layout.addLayout(button_layout)
|
|
483
|
+
|
|
484
|
+
order_note = QLabel(tr("並び順は、色欄のポップアップにそのまま反映されます。"))
|
|
485
|
+
order_note.setWordWrap(True)
|
|
486
|
+
layout.addWidget(order_note)
|
|
487
|
+
|
|
488
|
+
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
|
489
|
+
button_box.rejected.connect(self.reject)
|
|
490
|
+
button_box.accepted.connect(self.accept)
|
|
491
|
+
layout.addWidget(button_box)
|
|
492
|
+
|
|
493
|
+
self.add_button.clicked.connect(self._on_add)
|
|
494
|
+
self.edit_button.clicked.connect(self._on_edit)
|
|
495
|
+
self.delete_button.clicked.connect(self._on_delete)
|
|
496
|
+
self.up_button.clicked.connect(lambda: self._on_move(-1))
|
|
497
|
+
self.down_button.clicked.connect(lambda: self._on_move(1))
|
|
498
|
+
|
|
499
|
+
self._reload_list()
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _reload_list(self, select_index=None):
|
|
503
|
+
self.color_list.clear()
|
|
504
|
+
for entry in self.entries:
|
|
505
|
+
item = QListWidgetItem(f'{entry["name"]} {entry["color"]}')
|
|
506
|
+
item.setIcon(_named_color_icon(entry["color"]))
|
|
507
|
+
self.color_list.addItem(item)
|
|
508
|
+
if select_index is not None and 0 <= select_index < self.color_list.count():
|
|
509
|
+
self.color_list.setCurrentRow(select_index)
|
|
510
|
+
|
|
511
|
+
def _commit(self, entries, select_index=None):
|
|
512
|
+
self.entries = entries
|
|
513
|
+
save_named_colors(self._settings, self.entries)
|
|
514
|
+
self._reload_list(select_index)
|
|
515
|
+
|
|
516
|
+
def _current_index(self):
|
|
517
|
+
return self.color_list.currentRow()
|
|
518
|
+
|
|
519
|
+
def _ask_name_and_color(self, title, name="", color="#1f77b4"):
|
|
520
|
+
"""名前と色を尋ねる。(name, color) かキャンセルなら None。"""
|
|
521
|
+
chosen = QColorDialog.getColor(QColor(color), self, tr("色を選択"))
|
|
522
|
+
if not chosen.isValid():
|
|
523
|
+
return None
|
|
524
|
+
text, ok = notify.get_text(
|
|
525
|
+
self, title, tr("登録名"), text=name)
|
|
526
|
+
if not ok:
|
|
527
|
+
return None
|
|
528
|
+
return text, chosen.name()
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _on_add(self):
|
|
532
|
+
result = self._ask_name_and_color(tr("色を登録"))
|
|
533
|
+
if result is None:
|
|
534
|
+
return
|
|
535
|
+
name, color = result
|
|
536
|
+
try:
|
|
537
|
+
entries = add_named_color(self.entries, name, color)
|
|
538
|
+
except NamedColorError as e:
|
|
539
|
+
notify.warning(self, tr("色を登録"), str(e))
|
|
540
|
+
return
|
|
541
|
+
self._commit(entries, len(entries) - 1)
|
|
542
|
+
|
|
543
|
+
def _on_edit(self):
|
|
544
|
+
index = self._current_index()
|
|
545
|
+
if index < 0:
|
|
546
|
+
notify.information(self, tr("色名の管理"), tr("編集する登録を選んでください。"))
|
|
547
|
+
return
|
|
548
|
+
current = self.entries[index]
|
|
549
|
+
result = self._ask_name_and_color(
|
|
550
|
+
tr("登録を編集"), current["name"], current["color"])
|
|
551
|
+
if result is None:
|
|
552
|
+
return
|
|
553
|
+
name, color = result
|
|
554
|
+
try:
|
|
555
|
+
entries = update_named_color(self.entries, index, name, color)
|
|
556
|
+
except NamedColorError as e:
|
|
557
|
+
notify.warning(self, tr("登録を編集"), str(e))
|
|
558
|
+
return
|
|
559
|
+
self._commit(entries, index)
|
|
560
|
+
|
|
561
|
+
def _on_delete(self):
|
|
562
|
+
index = self._current_index()
|
|
563
|
+
if index < 0:
|
|
564
|
+
notify.information(self, tr("色名の管理"), tr("削除する登録を選んでください。"))
|
|
565
|
+
return
|
|
566
|
+
try:
|
|
567
|
+
entries = remove_named_color(self.entries, index)
|
|
568
|
+
except NamedColorError as e:
|
|
569
|
+
notify.warning(self, tr("色名の管理"), str(e))
|
|
570
|
+
return
|
|
571
|
+
self._commit(entries, min(index, len(entries) - 1))
|
|
572
|
+
|
|
573
|
+
def _on_move(self, offset):
|
|
574
|
+
index = self._current_index()
|
|
575
|
+
if index < 0:
|
|
576
|
+
return
|
|
577
|
+
try:
|
|
578
|
+
entries = move_named_color(self.entries, index, offset)
|
|
579
|
+
except NamedColorError:
|
|
580
|
+
return
|
|
581
|
+
new_index = max(0, min(index + offset, len(entries) - 1))
|
|
582
|
+
self._commit(entries, new_index)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
class NamedColorPickerDialog(QDialog):
|
|
586
|
+
"""登録した色を名前で絞り込んで選ぶ(色欄のポップアップは先頭 POPUP_LIMIT 件しか並べない)。編集はしない。"""
|
|
587
|
+
|
|
588
|
+
def __init__(self, settings, parent=None, title=None):
|
|
589
|
+
super().__init__(parent)
|
|
590
|
+
self.setWindowTitle(title or tr("登録色を選択"))
|
|
591
|
+
self.resize(380, 420)
|
|
592
|
+
self.entries = load_named_colors(settings)
|
|
593
|
+
self._selected = None
|
|
594
|
+
|
|
595
|
+
layout = QVBoxLayout(self)
|
|
596
|
+
|
|
597
|
+
self.filter_edit = QLineEdit()
|
|
598
|
+
self.filter_edit.setPlaceholderText(tr("名前または色コードで絞り込み"))
|
|
599
|
+
self.filter_edit.setClearButtonEnabled(True)
|
|
600
|
+
layout.addWidget(self.filter_edit)
|
|
601
|
+
|
|
602
|
+
self.color_list = QListWidget()
|
|
603
|
+
self.color_list.setIconSize(QSize(16, 16))
|
|
604
|
+
layout.addWidget(self.color_list, stretch=1)
|
|
605
|
+
|
|
606
|
+
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
607
|
+
QDialogButtonBox.StandardButton.Cancel)
|
|
608
|
+
button_box.accepted.connect(self.accept)
|
|
609
|
+
button_box.rejected.connect(self.reject)
|
|
610
|
+
layout.addWidget(button_box)
|
|
611
|
+
|
|
612
|
+
self.filter_edit.textChanged.connect(self._reload_list)
|
|
613
|
+
self.color_list.itemDoubleClicked.connect(lambda _item: self.accept())
|
|
614
|
+
|
|
615
|
+
self._reload_list()
|
|
616
|
+
|
|
617
|
+
def _reload_list(self, _text=None):
|
|
618
|
+
"""名前と色コードの両方で絞り込む。"""
|
|
619
|
+
needle = self.filter_edit.text().strip().lower()
|
|
620
|
+
self.color_list.clear()
|
|
621
|
+
for entry in self.entries:
|
|
622
|
+
if needle and needle not in entry["name"].lower() and needle not in entry["color"].lower():
|
|
623
|
+
continue
|
|
624
|
+
item = QListWidgetItem(f'{entry["name"]} {entry["color"]}')
|
|
625
|
+
item.setIcon(_named_color_icon(entry["color"]))
|
|
626
|
+
item.setData(Qt.ItemDataRole.UserRole, entry)
|
|
627
|
+
self.color_list.addItem(item)
|
|
628
|
+
if self.color_list.count():
|
|
629
|
+
self.color_list.setCurrentRow(0)
|
|
630
|
+
|
|
631
|
+
def accept(self):
|
|
632
|
+
item = self.color_list.currentItem()
|
|
633
|
+
self._selected = item.data(Qt.ItemDataRole.UserRole) if item else None
|
|
634
|
+
super().accept()
|
|
635
|
+
|
|
636
|
+
def selected_entry(self):
|
|
637
|
+
"""選んだ登録({'name', 'color'})。キャンセルか未選択なら None。"""
|
|
638
|
+
return self._selected
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
class LegendOrderDialog(QDialog):
|
|
642
|
+
"""凡例の並びを、描画順とは別にドラッグで決める。"""
|
|
643
|
+
|
|
644
|
+
def __init__(self, labels, parent=None):
|
|
645
|
+
super().__init__(parent)
|
|
646
|
+
self.setWindowTitle("凡例の順序")
|
|
647
|
+
self.resize(320, 380)
|
|
648
|
+
|
|
649
|
+
layout = QVBoxLayout(self)
|
|
650
|
+
layout.addWidget(QLabel("ドラッグして凡例の表示順序を変更できます:"))
|
|
651
|
+
|
|
652
|
+
self.list_widget = QListWidget()
|
|
653
|
+
self.list_widget.setDragDropMode(QListWidget.DragDropMode.InternalMove)
|
|
654
|
+
self.list_widget.addItems(labels)
|
|
655
|
+
layout.addWidget(self.list_widget)
|
|
656
|
+
|
|
657
|
+
reset_button = QPushButton("描画順にリセット")
|
|
658
|
+
reset_button.clicked.connect(self._on_reset)
|
|
659
|
+
layout.addWidget(reset_button)
|
|
660
|
+
self._original_labels = list(labels)
|
|
661
|
+
|
|
662
|
+
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
663
|
+
QDialogButtonBox.StandardButton.Cancel)
|
|
664
|
+
button_box.accepted.connect(self.accept)
|
|
665
|
+
button_box.rejected.connect(self.reject)
|
|
666
|
+
layout.addWidget(button_box)
|
|
667
|
+
|
|
668
|
+
def _on_reset(self):
|
|
669
|
+
self.list_widget.clear()
|
|
670
|
+
|
|
671
|
+
def get_order(self):
|
|
672
|
+
"""今の並び。「描画順にリセット」なら空のリスト(描画順に従う)。"""
|
|
673
|
+
return [self.list_widget.item(i).text() for i in range(self.list_widget.count())]
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
DEFAULT_ANNOTATION_COLOR = '#000000'
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
class _AnnotationColorButton(QPushButton):
|
|
680
|
+
"""注釈の色の見本。押すと色を選ぶ(最近使った色は settings に残す)。"""
|
|
681
|
+
|
|
682
|
+
def __init__(self, settings, color=DEFAULT_ANNOTATION_COLOR, parent=None):
|
|
683
|
+
super().__init__(parent)
|
|
684
|
+
self._settings = settings
|
|
685
|
+
self.set_color(color)
|
|
686
|
+
self.clicked.connect(self._choose_color)
|
|
687
|
+
|
|
688
|
+
def color(self):
|
|
689
|
+
return self._color
|
|
690
|
+
|
|
691
|
+
def set_color(self, color):
|
|
692
|
+
self._color = QColor(color).name()
|
|
693
|
+
swatch = QPixmap(16, 16)
|
|
694
|
+
swatch.fill(QColor(self._color))
|
|
695
|
+
self.setIcon(QIcon(swatch))
|
|
696
|
+
self.setText(self._color)
|
|
697
|
+
|
|
698
|
+
def _choose_color(self):
|
|
699
|
+
color = get_color_with_history(self._settings, self, initial=QColor(self._color))
|
|
700
|
+
if color.isValid():
|
|
701
|
+
self.set_color(color.name())
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
class TextAnnotationDialog(QDialog):
|
|
705
|
+
"""文字の注釈の文字と色。"""
|
|
706
|
+
|
|
707
|
+
def __init__(self, parent=None, settings=None):
|
|
708
|
+
super().__init__(parent)
|
|
709
|
+
self.setWindowTitle("テキスト注釈の追加")
|
|
710
|
+
self.resize(360, 140)
|
|
711
|
+
|
|
712
|
+
layout = QVBoxLayout(self)
|
|
713
|
+
layout.addWidget(QLabel("表示するテキスト:"))
|
|
714
|
+
self.text_edit = QLineEdit()
|
|
715
|
+
layout.addWidget(self.text_edit)
|
|
716
|
+
|
|
717
|
+
form = QFormLayout()
|
|
718
|
+
self.color_button = _AnnotationColorButton(settings)
|
|
719
|
+
form.addRow("色", self.color_button)
|
|
720
|
+
layout.addLayout(form)
|
|
721
|
+
|
|
722
|
+
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
723
|
+
QDialogButtonBox.StandardButton.Cancel)
|
|
724
|
+
button_box.accepted.connect(self.accept)
|
|
725
|
+
button_box.rejected.connect(self.reject)
|
|
726
|
+
layout.addWidget(button_box)
|
|
727
|
+
|
|
728
|
+
apply_form_spacing(self)
|
|
729
|
+
|
|
730
|
+
def get_text(self):
|
|
731
|
+
return self.text_edit.text().strip()
|
|
732
|
+
|
|
733
|
+
def color(self):
|
|
734
|
+
return self.color_button.color()
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
class ArrowAnnotationDialog(QDialog):
|
|
738
|
+
"""矢印の注釈のラベル、形('single' / 'double' / 'bracket')、曲がり具合。"""
|
|
739
|
+
|
|
740
|
+
STYLE_SINGLE = "通常(片矢印)"
|
|
741
|
+
STYLE_DOUBLE = "両矢印"
|
|
742
|
+
STYLE_BRACKET = "ブラケット(有意差表示等)"
|
|
743
|
+
STYLES = [STYLE_SINGLE, STYLE_DOUBLE, STYLE_BRACKET]
|
|
744
|
+
_STYLE_KEY_BY_LABEL = {STYLE_SINGLE: "single", STYLE_DOUBLE: "double", STYLE_BRACKET: "bracket"}
|
|
745
|
+
|
|
746
|
+
def __init__(self, parent=None, settings=None):
|
|
747
|
+
super().__init__(parent)
|
|
748
|
+
self.setWindowTitle("矢印注釈の追加")
|
|
749
|
+
self.resize(360, 200)
|
|
750
|
+
|
|
751
|
+
layout = QVBoxLayout(self)
|
|
752
|
+
layout.addWidget(QLabel("ラベル(空欄可):"))
|
|
753
|
+
self.text_edit = QLineEdit()
|
|
754
|
+
layout.addWidget(self.text_edit)
|
|
755
|
+
|
|
756
|
+
form = QFormLayout()
|
|
757
|
+
self.style_combo = QComboBox()
|
|
758
|
+
self.style_combo.addItems(self.STYLES)
|
|
759
|
+
form.addRow("矢印の形状", self.style_combo)
|
|
760
|
+
|
|
761
|
+
self.curvature_spinbox = QDoubleSpinBox()
|
|
762
|
+
self.curvature_spinbox.setRange(-1.0, 1.0)
|
|
763
|
+
self.curvature_spinbox.setSingleStep(0.05)
|
|
764
|
+
self.curvature_spinbox.setDecimals(2)
|
|
765
|
+
self.curvature_spinbox.setValue(0.0)
|
|
766
|
+
self.curvature_spinbox.setToolTip(
|
|
767
|
+
"0で直線。正/負で曲がる向きが変わります(matplotlibのarc3 rad相当)。"
|
|
768
|
+
)
|
|
769
|
+
form.addRow("曲率", self.curvature_spinbox)
|
|
770
|
+
|
|
771
|
+
self.color_button = _AnnotationColorButton(settings)
|
|
772
|
+
form.addRow("色", self.color_button)
|
|
773
|
+
layout.addLayout(form)
|
|
774
|
+
|
|
775
|
+
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
776
|
+
QDialogButtonBox.StandardButton.Cancel)
|
|
777
|
+
button_box.accepted.connect(self.accept)
|
|
778
|
+
button_box.rejected.connect(self.reject)
|
|
779
|
+
layout.addWidget(button_box)
|
|
780
|
+
|
|
781
|
+
apply_form_spacing(self)
|
|
782
|
+
|
|
783
|
+
def get_settings(self):
|
|
784
|
+
"""(ラベル, 形, 曲がり具合)"""
|
|
785
|
+
style_text = self.style_combo.currentText()
|
|
786
|
+
style = self._STYLE_KEY_BY_LABEL[style_text]
|
|
787
|
+
return self.text_edit.text().strip(), style, self.curvature_spinbox.value()
|
|
788
|
+
|
|
789
|
+
def color(self):
|
|
790
|
+
return self.color_button.color()
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
class InsetDialog(QDialog):
|
|
794
|
+
"""拡大図の位置(角と大きさ)と、拡大する X の範囲。"""
|
|
795
|
+
|
|
796
|
+
CORNERS = ["右上", "左上", "右下", "左下"]
|
|
797
|
+
|
|
798
|
+
def __init__(self, x_min, x_max, default_zoom_min, default_zoom_max, parent=None):
|
|
799
|
+
super().__init__(parent)
|
|
800
|
+
self.setWindowTitle("インセット(拡大図)を追加")
|
|
801
|
+
self.resize(380, 260)
|
|
802
|
+
|
|
803
|
+
layout = QVBoxLayout(self)
|
|
804
|
+
info_label = QLabel(
|
|
805
|
+
f"データのX範囲: {x_min:.4g} 〜 {x_max:.4g}\n"
|
|
806
|
+
"拡大表示したいX範囲と、インセットを表示する位置を選んでください。"
|
|
807
|
+
)
|
|
808
|
+
info_label.setWordWrap(True)
|
|
809
|
+
layout.addWidget(info_label)
|
|
810
|
+
|
|
811
|
+
form = QFormLayout()
|
|
812
|
+
self.zoom_min_spinbox = QDoubleSpinBox()
|
|
813
|
+
self.zoom_min_spinbox.setRange(-1e12, 1e12)
|
|
814
|
+
self.zoom_min_spinbox.setDecimals(4)
|
|
815
|
+
self.zoom_min_spinbox.setValue(default_zoom_min)
|
|
816
|
+
form.addRow("拡大範囲(最小)", self.zoom_min_spinbox)
|
|
817
|
+
|
|
818
|
+
self.zoom_max_spinbox = QDoubleSpinBox()
|
|
819
|
+
self.zoom_max_spinbox.setRange(-1e12, 1e12)
|
|
820
|
+
self.zoom_max_spinbox.setDecimals(4)
|
|
821
|
+
self.zoom_max_spinbox.setValue(default_zoom_max)
|
|
822
|
+
form.addRow("拡大範囲(最大)", self.zoom_max_spinbox)
|
|
823
|
+
|
|
824
|
+
self.corner_combo = QComboBox()
|
|
825
|
+
self.corner_combo.addItems(self.CORNERS)
|
|
826
|
+
form.addRow("表示位置", self.corner_combo)
|
|
827
|
+
|
|
828
|
+
self.size_spinbox = QDoubleSpinBox()
|
|
829
|
+
self.size_spinbox.setRange(0.15, 0.6)
|
|
830
|
+
self.size_spinbox.setSingleStep(0.05)
|
|
831
|
+
self.size_spinbox.setValue(0.35)
|
|
832
|
+
form.addRow("大きさ(軸に対する比率)", self.size_spinbox)
|
|
833
|
+
layout.addLayout(form)
|
|
834
|
+
|
|
835
|
+
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok |
|
|
836
|
+
QDialogButtonBox.StandardButton.Cancel)
|
|
837
|
+
button_box.accepted.connect(self.accept)
|
|
838
|
+
button_box.rejected.connect(self.reject)
|
|
839
|
+
layout.addWidget(button_box)
|
|
840
|
+
|
|
841
|
+
apply_form_spacing(self)
|
|
842
|
+
|
|
843
|
+
def get_settings(self):
|
|
844
|
+
"""{'corner', 'size', 'zoom_x_range'(昇順)}"""
|
|
845
|
+
lo = self.zoom_min_spinbox.value()
|
|
846
|
+
hi = self.zoom_max_spinbox.value()
|
|
847
|
+
return {
|
|
848
|
+
'corner': self.corner_combo.currentText(),
|
|
849
|
+
'size': self.size_spinbox.value(),
|
|
850
|
+
'zoom_x_range': (min(lo, hi), max(lo, hi)),
|
|
851
|
+
}
|