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,321 @@
|
|
|
1
|
+
"""書き出しのプレビューのドック。設定を変えるたびに全サブプロットの完成形を描き直し、ここから保存とコピーもできる。
|
|
2
|
+
|
|
3
|
+
一時的な MplCanvas に描くので、画面のキャンバスには触れない。
|
|
4
|
+
"""
|
|
5
|
+
import io
|
|
6
|
+
import os
|
|
7
|
+
import logging
|
|
8
|
+
import matplotlib as mpl
|
|
9
|
+
|
|
10
|
+
from PySide6.QtCore import Qt, QTimer, QByteArray, QMimeData
|
|
11
|
+
from PySide6.QtGui import QPixmap
|
|
12
|
+
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QFormLayout, QHBoxLayout,
|
|
13
|
+
QLabel, QComboBox, QCheckBox, QDoubleSpinBox, QSpinBox,
|
|
14
|
+
QPushButton, QApplication)
|
|
15
|
+
|
|
16
|
+
from graphica.core.i18n import tr
|
|
17
|
+
from graphica.gui import notify
|
|
18
|
+
from graphica.gui.canvas import MplCanvas
|
|
19
|
+
from graphica.gui.export_settings import export_rc_params
|
|
20
|
+
from graphica.gui.theme import apply_form_spacing
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
# 矢印の連打などで毎回重い描き直しが走らないよう、まとめるまでの待ち(ms)
|
|
25
|
+
PREVIEW_DEBOUNCE_MS = 150
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ExportPreviewPanel(QWidget):
|
|
29
|
+
def __init__(self, main_window, parent=None):
|
|
30
|
+
super().__init__(parent)
|
|
31
|
+
self.main_window = main_window
|
|
32
|
+
self._current_pixmap = None
|
|
33
|
+
|
|
34
|
+
layout = QVBoxLayout(self)
|
|
35
|
+
|
|
36
|
+
form = QFormLayout()
|
|
37
|
+
self.width_spinbox = QDoubleSpinBox()
|
|
38
|
+
self.width_spinbox.setRange(1, 10000)
|
|
39
|
+
self.width_spinbox.setDecimals(1)
|
|
40
|
+
self.width_spinbox.setValue(800)
|
|
41
|
+
form.addRow("幅", self.width_spinbox)
|
|
42
|
+
|
|
43
|
+
self.height_spinbox = QDoubleSpinBox()
|
|
44
|
+
self.height_spinbox.setRange(1, 10000)
|
|
45
|
+
self.height_spinbox.setDecimals(1)
|
|
46
|
+
self.height_spinbox.setValue(600)
|
|
47
|
+
form.addRow("高さ", self.height_spinbox)
|
|
48
|
+
|
|
49
|
+
self.unit_combo = QComboBox()
|
|
50
|
+
# 値は日本語の元の文字(書き出しの処理がそれで単位を見分ける)。表示だけを訳す
|
|
51
|
+
for unit in ("ピクセル (px)", "インチ (in)", "センチメートル (cm)"):
|
|
52
|
+
self.unit_combo.addItem(tr(unit), unit)
|
|
53
|
+
form.addRow("単位", self.unit_combo)
|
|
54
|
+
|
|
55
|
+
self.dpi_spinbox = QSpinBox()
|
|
56
|
+
self.dpi_spinbox.setRange(50, 1200)
|
|
57
|
+
self.dpi_spinbox.setValue(150)
|
|
58
|
+
self.dpi_spinbox.setSuffix(" dpi")
|
|
59
|
+
form.addRow("解像度", self.dpi_spinbox)
|
|
60
|
+
|
|
61
|
+
self.transparent_checkbox = QCheckBox("背景を透過")
|
|
62
|
+
self.transparent_checkbox.setChecked(True)
|
|
63
|
+
form.addRow(self.transparent_checkbox)
|
|
64
|
+
|
|
65
|
+
self.svg_text_as_path_checkbox = QCheckBox("文字をアウトライン化する(SVG)")
|
|
66
|
+
self.svg_text_as_path_checkbox.setToolTip(
|
|
67
|
+
"SVG保存/コピー時、目盛りの数字やラベルの文字をパス(輪郭線)として出力します。"
|
|
68
|
+
)
|
|
69
|
+
form.addRow(self.svg_text_as_path_checkbox)
|
|
70
|
+
|
|
71
|
+
# プレビューは応答を優先して常に間引いて描く。このチェックは保存とコピーだけに効く
|
|
72
|
+
self.full_resolution_checkbox = QCheckBox("フル解像度で保存/コピー(間引きなし)")
|
|
73
|
+
self.full_resolution_checkbox.setToolTip(
|
|
74
|
+
"「名前を付けて保存」「コピー」の出力にのみ適用されます(常時更新される"
|
|
75
|
+
"プレビュー自体は応答性のため常に間引いたまま表示します)。点数の多い"
|
|
76
|
+
"Line(折れ線)データセットや2Dマップ(ヒートマップ/等高線)の間引きを"
|
|
77
|
+
"無効化し、常に全データ点/全解像度で保存/コピーします。"
|
|
78
|
+
)
|
|
79
|
+
form.addRow(self.full_resolution_checkbox)
|
|
80
|
+
|
|
81
|
+
layout.addLayout(form)
|
|
82
|
+
apply_form_spacing(self)
|
|
83
|
+
|
|
84
|
+
self.preview_label = QLabel("プレビューがここに表示されます")
|
|
85
|
+
self.preview_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
86
|
+
self.preview_label.setMinimumSize(200, 150)
|
|
87
|
+
self.preview_label.setFrameShape(QLabel.Shape.StyledPanel)
|
|
88
|
+
self.preview_label.setWordWrap(True)
|
|
89
|
+
layout.addWidget(self.preview_label, 1)
|
|
90
|
+
|
|
91
|
+
button_row = QHBoxLayout()
|
|
92
|
+
# SVG はベクター画像を受け付けるアプリ(Illustrator、Inkscape など)に貼れる
|
|
93
|
+
self.copy_format_combo = QComboBox()
|
|
94
|
+
self.copy_format_combo.addItems(["PNG", "SVG"])
|
|
95
|
+
self.copy_format_combo.setToolTip("クリップボードにコピーする形式を選択します")
|
|
96
|
+
button_row.addWidget(self.copy_format_combo)
|
|
97
|
+
|
|
98
|
+
self.copy_button = QPushButton("コピー")
|
|
99
|
+
self.copy_button.setToolTip("現在の設定でレンダリングした画像をクリップボードにコピーします")
|
|
100
|
+
self.copy_button.clicked.connect(self._on_copy_clicked)
|
|
101
|
+
button_row.addWidget(self.copy_button)
|
|
102
|
+
|
|
103
|
+
self.save_button = QPushButton("名前を付けて保存...")
|
|
104
|
+
self.save_button.clicked.connect(self._on_save_clicked)
|
|
105
|
+
button_row.addWidget(self.save_button)
|
|
106
|
+
layout.addLayout(button_row)
|
|
107
|
+
|
|
108
|
+
self.width_spinbox.valueChanged.connect(self.refresh_preview)
|
|
109
|
+
self.height_spinbox.valueChanged.connect(self.refresh_preview)
|
|
110
|
+
self.unit_combo.currentIndexChanged.connect(self.refresh_preview)
|
|
111
|
+
self.dpi_spinbox.valueChanged.connect(self.refresh_preview)
|
|
112
|
+
|
|
113
|
+
self._refresh_timer = QTimer(self)
|
|
114
|
+
self._refresh_timer.setSingleShot(True)
|
|
115
|
+
self._refresh_timer.setInterval(PREVIEW_DEBOUNCE_MS)
|
|
116
|
+
self._refresh_timer.timeout.connect(self._render_preview)
|
|
117
|
+
|
|
118
|
+
def get_options(self):
|
|
119
|
+
"""ExportDialog と同じ形の設定(_calculate_size_in_inches で使える)。"""
|
|
120
|
+
return {
|
|
121
|
+
"width": self.width_spinbox.value(),
|
|
122
|
+
"height": self.height_spinbox.value(),
|
|
123
|
+
"unit": self.unit_combo.currentData(),
|
|
124
|
+
"dpi": self.dpi_spinbox.value(),
|
|
125
|
+
"transparent": self.transparent_checkbox.isChecked(),
|
|
126
|
+
"svg_text_as_path": self.svg_text_as_path_checkbox.isChecked(),
|
|
127
|
+
"full_resolution": self.full_resolution_checkbox.isChecked(),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
def refresh_preview(self):
|
|
131
|
+
"""少し待ってまとめて描く。隠れている間は描かない(表示したときに refresh_preview が呼ばれる)。"""
|
|
132
|
+
if not self.isVisible():
|
|
133
|
+
return
|
|
134
|
+
self._refresh_timer.start()
|
|
135
|
+
|
|
136
|
+
def _render_preview(self):
|
|
137
|
+
options = self.get_options()
|
|
138
|
+
width_in, height_in = self.main_window._calculate_size_in_inches(options)
|
|
139
|
+
if width_in <= 0 or height_in <= 0:
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
pixmap = self._render_full_figure_pixmap(width_in, height_in, options["dpi"])
|
|
143
|
+
self._current_pixmap = pixmap
|
|
144
|
+
if pixmap is None or pixmap.isNull():
|
|
145
|
+
self.preview_label.setPixmap(QPixmap())
|
|
146
|
+
self.preview_label.setText("プレビューを生成できませんでした\n(データセットがないか、設定を確認してください)")
|
|
147
|
+
return
|
|
148
|
+
self.preview_label.setText("")
|
|
149
|
+
self.preview_label.setPixmap(
|
|
150
|
+
pixmap.scaled(self.preview_label.size(),
|
|
151
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
152
|
+
Qt.TransformationMode.SmoothTransformation)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def resizeEvent(self, event):
|
|
156
|
+
super().resizeEvent(event)
|
|
157
|
+
if self._current_pixmap is not None and not self._current_pixmap.isNull():
|
|
158
|
+
self.preview_label.setPixmap(
|
|
159
|
+
self._current_pixmap.scaled(self.preview_label.size(),
|
|
160
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
161
|
+
Qt.TransformationMode.SmoothTransformation)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def _make_temp_canvas_for_full_figure(self, width_in, height_in, dpi, full_resolution=False):
|
|
165
|
+
"""全サブプロットを描いた一時的な MplCanvas。描けるものが無ければ None。
|
|
166
|
+
|
|
167
|
+
full_resolution=True は間引かない。常に描き直すプレビューでは False、保存とコピーでだけ設定の値を渡す。
|
|
168
|
+
"""
|
|
169
|
+
mw = self.main_window
|
|
170
|
+
layout_mode = getattr(mw.project, 'layout_mode', 'grid')
|
|
171
|
+
if layout_mode == 'free':
|
|
172
|
+
rows, cols = 0, 0
|
|
173
|
+
if not mw.project.all_plot_settings:
|
|
174
|
+
return None
|
|
175
|
+
else:
|
|
176
|
+
rows = mw.subplot_rows_spinbox.value()
|
|
177
|
+
cols = mw.subplot_cols_spinbox.value()
|
|
178
|
+
if rows * cols == 0:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
temp_canvas = MplCanvas(width=width_in, height=height_in, dpi=dpi)
|
|
182
|
+
temp_canvas.dark_mode = mw.canvas.dark_mode
|
|
183
|
+
temp_canvas.redraw_all(
|
|
184
|
+
mw.project.datasets, rows, cols, mw.project.all_plot_settings, layout_mode=layout_mode,
|
|
185
|
+
panel_labels_enabled=mw.project.panel_labels_enabled,
|
|
186
|
+
full_resolution=full_resolution,
|
|
187
|
+
)
|
|
188
|
+
return temp_canvas
|
|
189
|
+
|
|
190
|
+
def _render_full_figure_pixmap(self, width_in, height_in, dpi):
|
|
191
|
+
"""画面のプレビュー用。常に不透明で描く(透過は保存とコピーのときだけ選べる)。"""
|
|
192
|
+
try:
|
|
193
|
+
temp_canvas = self._make_temp_canvas_for_full_figure(width_in, height_in, dpi)
|
|
194
|
+
if temp_canvas is None:
|
|
195
|
+
return None
|
|
196
|
+
try:
|
|
197
|
+
buf = io.BytesIO()
|
|
198
|
+
temp_canvas.fig.savefig(
|
|
199
|
+
buf, format='png', dpi=dpi, bbox_inches='tight',
|
|
200
|
+
facecolor=temp_canvas.fig.get_facecolor()
|
|
201
|
+
)
|
|
202
|
+
buf.seek(0)
|
|
203
|
+
pixmap = QPixmap()
|
|
204
|
+
pixmap.loadFromData(buf.read())
|
|
205
|
+
buf.close()
|
|
206
|
+
return pixmap
|
|
207
|
+
finally:
|
|
208
|
+
temp_canvas.deleteLater()
|
|
209
|
+
except Exception:
|
|
210
|
+
logger.exception("エクスポートプレビューの生成に失敗しました。")
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
def _render_full_figure_bytes(self, width_in, height_in, dpi, fmt, transparent, svg_text_as_path=False,
|
|
214
|
+
full_resolution=False):
|
|
215
|
+
"""'png' か 'svg' で書いたバイト列(保存とコピーで共有)。"""
|
|
216
|
+
try:
|
|
217
|
+
temp_canvas = self._make_temp_canvas_for_full_figure(width_in, height_in, dpi, full_resolution=full_resolution)
|
|
218
|
+
if temp_canvas is None:
|
|
219
|
+
return None
|
|
220
|
+
try:
|
|
221
|
+
buf = io.BytesIO()
|
|
222
|
+
save_kwargs = {'format': fmt, 'bbox_inches': 'tight', 'transparent': transparent}
|
|
223
|
+
if fmt != 'svg':
|
|
224
|
+
save_kwargs['dpi'] = dpi
|
|
225
|
+
with mpl.rc_context(export_rc_params(fmt, svg_text_as_path)):
|
|
226
|
+
temp_canvas.fig.savefig(buf, **save_kwargs)
|
|
227
|
+
buf.seek(0)
|
|
228
|
+
data = buf.read()
|
|
229
|
+
buf.close()
|
|
230
|
+
return data
|
|
231
|
+
finally:
|
|
232
|
+
temp_canvas.deleteLater()
|
|
233
|
+
except Exception:
|
|
234
|
+
logger.exception("エクスポートプレビューのレンダリングに失敗しました(形式: %s)。", fmt)
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
def _on_copy_clicked(self):
|
|
238
|
+
options = self.get_options()
|
|
239
|
+
width_in, height_in = self.main_window._calculate_size_in_inches(options)
|
|
240
|
+
if width_in <= 0 or height_in <= 0:
|
|
241
|
+
notify.warning(self, "コピーエラー", "コピーする画像がありません。")
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
if self.copy_format_combo.currentText() == "SVG":
|
|
245
|
+
svg_bytes = self._render_full_figure_bytes(
|
|
246
|
+
width_in, height_in, options["dpi"], fmt='svg', transparent=options["transparent"],
|
|
247
|
+
svg_text_as_path=options["svg_text_as_path"], full_resolution=options["full_resolution"]
|
|
248
|
+
)
|
|
249
|
+
if svg_bytes is None:
|
|
250
|
+
notify.warning(self, "コピーエラー", "コピーする画像がありません。")
|
|
251
|
+
return
|
|
252
|
+
mime_data = QMimeData()
|
|
253
|
+
mime_data.setData("image/svg+xml", QByteArray(svg_bytes))
|
|
254
|
+
# SVG だけでは Word や PowerPoint が貼り付けられないので、PNG も同じ QMimeData に載せる。
|
|
255
|
+
# その PNG は透過にしない(macOS のクリップボードはアルファを引き継がず、貼ると真っ黒になる)
|
|
256
|
+
png_fallback_bytes = self._render_full_figure_bytes(
|
|
257
|
+
width_in, height_in, options["dpi"], fmt='png', transparent=False,
|
|
258
|
+
full_resolution=options["full_resolution"]
|
|
259
|
+
)
|
|
260
|
+
if png_fallback_bytes is not None:
|
|
261
|
+
fallback_pixmap = QPixmap()
|
|
262
|
+
fallback_pixmap.loadFromData(png_fallback_bytes)
|
|
263
|
+
if not fallback_pixmap.isNull():
|
|
264
|
+
mime_data.setImageData(fallback_pixmap.toImage())
|
|
265
|
+
QApplication.clipboard().setMimeData(mime_data)
|
|
266
|
+
self.main_window.statusBar().showMessage("プレビュー画像をSVG形式でクリップボードにコピーしました", 3000)
|
|
267
|
+
else:
|
|
268
|
+
# 上と同じ理由で透過にしない
|
|
269
|
+
png_bytes = self._render_full_figure_bytes(
|
|
270
|
+
width_in, height_in, options["dpi"], fmt='png', transparent=False,
|
|
271
|
+
full_resolution=options["full_resolution"]
|
|
272
|
+
)
|
|
273
|
+
if png_bytes is None:
|
|
274
|
+
notify.warning(self, "コピーエラー", "コピーする画像がありません。")
|
|
275
|
+
return
|
|
276
|
+
pixmap = QPixmap()
|
|
277
|
+
pixmap.loadFromData(png_bytes)
|
|
278
|
+
QApplication.clipboard().setPixmap(pixmap)
|
|
279
|
+
self.main_window.statusBar().showMessage("プレビュー画像をクリップボードにコピーしました", 3000)
|
|
280
|
+
|
|
281
|
+
def _on_save_clicked(self):
|
|
282
|
+
options = self.get_options()
|
|
283
|
+
width_in, height_in = self.main_window._calculate_size_in_inches(options)
|
|
284
|
+
|
|
285
|
+
layout_mode = getattr(self.main_window.project, 'layout_mode', 'grid')
|
|
286
|
+
if layout_mode == 'free':
|
|
287
|
+
if not self.main_window.project.all_plot_settings:
|
|
288
|
+
notify.warning(self, "保存エラー", "有効なプロットがありません。")
|
|
289
|
+
return
|
|
290
|
+
else:
|
|
291
|
+
rows = self.main_window.subplot_rows_spinbox.value()
|
|
292
|
+
cols = self.main_window.subplot_cols_spinbox.value()
|
|
293
|
+
if rows * cols == 0:
|
|
294
|
+
notify.warning(self, "保存エラー", "有効なプロットがありません。")
|
|
295
|
+
return
|
|
296
|
+
|
|
297
|
+
file_path, _ = notify.get_save_file_name(
|
|
298
|
+
self, "プロットを保存", "", "PNG (*.png);;PDF (*.pdf);;SVG (*.svg)"
|
|
299
|
+
)
|
|
300
|
+
if not file_path:
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
file_ext = os.path.splitext(file_path)[1].lower().lstrip('.')
|
|
304
|
+
temp_canvas = self._make_temp_canvas_for_full_figure(
|
|
305
|
+
width_in, height_in, options["dpi"], full_resolution=options["full_resolution"]
|
|
306
|
+
)
|
|
307
|
+
if temp_canvas is None:
|
|
308
|
+
notify.warning(self, "保存エラー", "有効なプロットがありません。")
|
|
309
|
+
return
|
|
310
|
+
try:
|
|
311
|
+
save_kwargs = {'transparent': options["transparent"], 'bbox_inches': 'tight'}
|
|
312
|
+
if file_ext not in ('pdf', 'svg'):
|
|
313
|
+
save_kwargs['dpi'] = options["dpi"]
|
|
314
|
+
with mpl.rc_context(export_rc_params(file_ext, options.get("svg_text_as_path", False))):
|
|
315
|
+
temp_canvas.fig.savefig(file_path, **save_kwargs)
|
|
316
|
+
self.main_window.statusBar().showMessage(f"保存しました: {file_path}", 3000)
|
|
317
|
+
except Exception as e:
|
|
318
|
+
logger.exception("エクスポートプレビューパネルからの保存に失敗しました。")
|
|
319
|
+
notify.warning(self, "保存エラー", f"エクスポート中にエラーが発生しました:\n{e}")
|
|
320
|
+
finally:
|
|
321
|
+
temp_canvas.deleteLater()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""書き出すときだけ当てる matplotlib の rcParams。書き出しの経路(4つ)はすべてこれを使うこと。"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def export_rc_params(fmt, svg_text_as_path=False):
|
|
5
|
+
"""mpl.rc_context() に渡す辞書。
|
|
6
|
+
|
|
7
|
+
SVG は文字をテキストのまま残すか、パスにするか。PDF はフォントを TrueType(Type 42)で埋め込む
|
|
8
|
+
(既定の Type 3 だとベクター編集ソフトで文字を編集できない)。ラスタは何もしない。
|
|
9
|
+
fmt の先頭のドットと大文字小文字は無視する。
|
|
10
|
+
"""
|
|
11
|
+
fmt = (fmt or '').lower().lstrip('.')
|
|
12
|
+
if fmt == 'svg':
|
|
13
|
+
return {'svg.fonttype': 'path' if svg_text_as_path else 'none'}
|
|
14
|
+
if fmt in ('pdf', 'ps', 'eps'):
|
|
15
|
+
return {'pdf.fonttype': 42, 'ps.fonttype': 42}
|
|
16
|
+
return {}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Windows で .gra と .graphica を Graphica に結び付ける(利用者の範囲 HKCU\\Software\\Classes だけを触る)。
|
|
2
|
+
|
|
3
|
+
インストーラーを使わず exe だけを置いた人のため、環境設定のボタンから登録・解除する。インストーラーは同じ名前で自分で登録する。
|
|
4
|
+
ソースや pip から動かしているときは、ダブルクリックで起動する exe が無いので登録しない。
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
PROG_ID = "Graphica.Project"
|
|
10
|
+
PROG_ID_DESCRIPTION = "Graphica プロジェクト"
|
|
11
|
+
ASSOCIATED_EXTENSIONS = (".gra", ".graphica")
|
|
12
|
+
_CLASSES_ROOT = r"Software\Classes"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _winreg():
|
|
16
|
+
import winreg
|
|
17
|
+
return winreg
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def is_supported():
|
|
21
|
+
"""登録できるのは、Windows の exe(PyInstaller で固めたもの)から動かしているときだけ。"""
|
|
22
|
+
return sys.platform == "win32" and bool(getattr(sys, "frozen", False))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def open_command(executable=None):
|
|
26
|
+
return f'"{executable or sys.executable}" "%1"'
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _set_default(winreg, path, value):
|
|
30
|
+
with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, path, 0, winreg.KEY_WRITE) as key:
|
|
31
|
+
winreg.SetValueEx(key, "", 0, winreg.REG_SZ, value)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _read_default(winreg, path):
|
|
35
|
+
try:
|
|
36
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, path) as key:
|
|
37
|
+
return winreg.QueryValueEx(key, "")[0]
|
|
38
|
+
except OSError:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _delete_tree(winreg, path):
|
|
43
|
+
try:
|
|
44
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0, winreg.KEY_ALL_ACCESS) as key:
|
|
45
|
+
while True:
|
|
46
|
+
try:
|
|
47
|
+
child = winreg.EnumKey(key, 0)
|
|
48
|
+
except OSError:
|
|
49
|
+
break
|
|
50
|
+
_delete_tree(winreg, path + "\\" + child)
|
|
51
|
+
winreg.DeleteKey(winreg.HKEY_CURRENT_USER, path)
|
|
52
|
+
except FileNotFoundError:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _notify_shell():
|
|
57
|
+
"""エクスプローラーにアイコンと関連付けを読み直させる。失敗しても登録自体は済んでいる。"""
|
|
58
|
+
try:
|
|
59
|
+
import ctypes
|
|
60
|
+
shcne_assocchanged, shcnf_idlist = 0x08000000, 0x0000
|
|
61
|
+
ctypes.windll.shell32.SHChangeNotify(shcne_assocchanged, shcnf_idlist, None, None)
|
|
62
|
+
except (AttributeError, OSError):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def register(executable=None, winreg=None, notify_shell=True):
|
|
67
|
+
winreg = winreg or _winreg()
|
|
68
|
+
executable = executable or sys.executable
|
|
69
|
+
prog_id_path = _CLASSES_ROOT + "\\" + PROG_ID
|
|
70
|
+
_set_default(winreg, prog_id_path, PROG_ID_DESCRIPTION)
|
|
71
|
+
_set_default(winreg, prog_id_path + r"\DefaultIcon", f'"{executable}",0')
|
|
72
|
+
_set_default(winreg, prog_id_path + r"\shell\open\command", open_command(executable))
|
|
73
|
+
for ext in ASSOCIATED_EXTENSIONS:
|
|
74
|
+
_set_default(winreg, _CLASSES_ROOT + "\\" + ext, PROG_ID)
|
|
75
|
+
if notify_shell:
|
|
76
|
+
_notify_shell()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def unregister(winreg=None, notify_shell=True):
|
|
80
|
+
"""自分が結び付けた拡張子だけを外す(ほかのソフトに変えられていれば触らない)。"""
|
|
81
|
+
winreg = winreg or _winreg()
|
|
82
|
+
for ext in ASSOCIATED_EXTENSIONS:
|
|
83
|
+
path = _CLASSES_ROOT + "\\" + ext
|
|
84
|
+
if _read_default(winreg, path) == PROG_ID:
|
|
85
|
+
_delete_tree(winreg, path)
|
|
86
|
+
_delete_tree(winreg, _CLASSES_ROOT + "\\" + PROG_ID)
|
|
87
|
+
if notify_shell:
|
|
88
|
+
_notify_shell()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def is_registered(executable=None, winreg=None):
|
|
92
|
+
"""このアプリの exe で開くよう登録されているか。"""
|
|
93
|
+
winreg = winreg or _winreg()
|
|
94
|
+
command = _read_default(winreg, _CLASSES_ROOT + "\\" + PROG_ID + r"\shell\open\command")
|
|
95
|
+
if command is None or os.path.normcase(command) != os.path.normcase(open_command(executable)):
|
|
96
|
+
return False
|
|
97
|
+
return all(_read_default(winreg, _CLASSES_ROOT + "\\" + ext) == PROG_ID for ext in ASSOCIATED_EXTENSIONS)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Tabler Icons(MIT)の SVG を QIcon にする。stroke="currentColor" を色に置き換えて描く。"""
|
|
2
|
+
import os
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
|
|
5
|
+
from PySide6.QtCore import QByteArray, QSize, Qt
|
|
6
|
+
from PySide6.QtGui import QIcon, QPixmap, QPainter
|
|
7
|
+
from PySide6.QtSvg import QSvgRenderer
|
|
8
|
+
|
|
9
|
+
from graphica.gui.resources import resource_path
|
|
10
|
+
|
|
11
|
+
ICONS_DIR = os.path.join("assets", "icons")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def icon(name, color=None, size=16):
|
|
15
|
+
"""assets/icons/{name}.svg。color を省くとテーマの text_secondary(呼ぶたびに決まる)。"""
|
|
16
|
+
if color is None:
|
|
17
|
+
from graphica.gui import theme
|
|
18
|
+
color = theme.current_tokens()["text_secondary"]
|
|
19
|
+
# exe では同梱のアイコンが sys._MEIPASS の下にあるので、このファイルの場所ではなく resource_path() で探す
|
|
20
|
+
svg_path = resource_path(os.path.join(ICONS_DIR, f"{name}.svg"))
|
|
21
|
+
return load_svg_icon(svg_path, color=color, size=size)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@lru_cache(maxsize=None)
|
|
25
|
+
def _load_svg_text(svg_path):
|
|
26
|
+
with open(svg_path, "r", encoding="utf-8") as f:
|
|
27
|
+
return f.read()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_svg_icon(svg_path, color="#4B5157", size=24):
|
|
31
|
+
"""ファイルが無ければ空の QIcon(アイコンが無くてもアプリを落とさない)。"""
|
|
32
|
+
if not os.path.exists(svg_path):
|
|
33
|
+
return QIcon()
|
|
34
|
+
|
|
35
|
+
svg_text = _load_svg_text(svg_path).replace("currentColor", color)
|
|
36
|
+
renderer = QSvgRenderer(QByteArray(svg_text.encode("utf-8")))
|
|
37
|
+
|
|
38
|
+
pixmap = QPixmap(QSize(size, size))
|
|
39
|
+
pixmap.fill(Qt.GlobalColor.transparent)
|
|
40
|
+
painter = QPainter(pixmap)
|
|
41
|
+
renderer.render(painter)
|
|
42
|
+
painter.end()
|
|
43
|
+
|
|
44
|
+
return QIcon(pixmap)
|