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,191 @@
|
|
|
1
|
+
"""範囲選択モード: X の範囲をドラッグで選び、その範囲の点を今のデータセットのマスクに加える。
|
|
2
|
+
|
|
3
|
+
SpanSelector は Axes に束縛されるので、Axes を作り直す redraw_all() のたびに作り直すことになる。ほかのモードと同じく
|
|
4
|
+
キャンバス全体の button_press / motion / release で event.inaxes を読む。
|
|
5
|
+
"""
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from matplotlib.patches import Rectangle
|
|
9
|
+
|
|
10
|
+
from graphica.gui import notify
|
|
11
|
+
from graphica.core.commands import SetMaskedRowsCommand
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RangeSelectTool:
|
|
17
|
+
"""状態はこのツールが持ち、PlotterApp の同じ名前は窓口(gui/tools/__init__.py)。"""
|
|
18
|
+
|
|
19
|
+
# PlotterApp から同じ名前で読み書きできるもの(テスト・メニュー・ほかの mixin が使う)
|
|
20
|
+
EXPOSED_NAMES = (
|
|
21
|
+
'_apply_range_mask', '_clear_range_select_preview', '_on_range_select_motion', '_on_range_select_press',
|
|
22
|
+
'_on_range_select_release', '_range_select_axes', '_range_select_background', '_range_select_motion_cid',
|
|
23
|
+
'_range_select_press_cid', '_range_select_preview_artist', '_range_select_release_cid',
|
|
24
|
+
'_range_select_start_x', '_toggle_range_select_mode', 'range_select_mode_enabled',
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def __init__(self, app):
|
|
28
|
+
self._app = app
|
|
29
|
+
self.range_select_mode_enabled = False
|
|
30
|
+
self._range_select_press_cid = None
|
|
31
|
+
self._range_select_motion_cid = None
|
|
32
|
+
self._range_select_release_cid = None
|
|
33
|
+
self._range_select_axes = None
|
|
34
|
+
self._range_select_start_x = None
|
|
35
|
+
self._range_select_preview_artist = None
|
|
36
|
+
|
|
37
|
+
def _toggle_range_select_mode(self, checked):
|
|
38
|
+
self.range_select_mode_enabled = checked
|
|
39
|
+
|
|
40
|
+
if checked:
|
|
41
|
+
self._app._deactivate_other_mouse_modes('range_select')
|
|
42
|
+
|
|
43
|
+
self._range_select_press_cid = self._app.canvas.mpl_connect(
|
|
44
|
+
'button_press_event', self._on_range_select_press
|
|
45
|
+
)
|
|
46
|
+
self._range_select_motion_cid = self._app.canvas.mpl_connect(
|
|
47
|
+
'motion_notify_event', self._on_range_select_motion
|
|
48
|
+
)
|
|
49
|
+
self._range_select_release_cid = self._app.canvas.mpl_connect(
|
|
50
|
+
'button_release_event', self._on_range_select_release
|
|
51
|
+
)
|
|
52
|
+
self._app.statusBar().showMessage(
|
|
53
|
+
"範囲選択モード: カレントデータセット上でドラッグした範囲を"
|
|
54
|
+
"マスク(除外)します", 5000
|
|
55
|
+
)
|
|
56
|
+
else:
|
|
57
|
+
if getattr(self, '_range_select_press_cid', None) is not None:
|
|
58
|
+
self._app.canvas.mpl_disconnect(self._range_select_press_cid)
|
|
59
|
+
self._range_select_press_cid = None
|
|
60
|
+
if getattr(self, '_range_select_motion_cid', None) is not None:
|
|
61
|
+
self._app.canvas.mpl_disconnect(self._range_select_motion_cid)
|
|
62
|
+
self._range_select_motion_cid = None
|
|
63
|
+
if getattr(self, '_range_select_release_cid', None) is not None:
|
|
64
|
+
self._app.canvas.mpl_disconnect(self._range_select_release_cid)
|
|
65
|
+
self._range_select_release_cid = None
|
|
66
|
+
self._clear_range_select_preview()
|
|
67
|
+
self._range_select_axes = None
|
|
68
|
+
self._range_select_start_x = None
|
|
69
|
+
|
|
70
|
+
def _clear_range_select_preview(self):
|
|
71
|
+
"""ドラッグ中のプレビューの矩形と、blit 用の背景を捨てる。描き直しで既に消えていても例外にしない。"""
|
|
72
|
+
artist = getattr(self, '_range_select_preview_artist', None)
|
|
73
|
+
if artist is not None:
|
|
74
|
+
try:
|
|
75
|
+
artist.remove()
|
|
76
|
+
except (ValueError, NotImplementedError):
|
|
77
|
+
pass
|
|
78
|
+
self._range_select_preview_artist = None
|
|
79
|
+
self._app.canvas.draw_idle()
|
|
80
|
+
self._range_select_background = None
|
|
81
|
+
|
|
82
|
+
def _on_range_select_press(self, event):
|
|
83
|
+
if not getattr(self, 'range_select_mode_enabled', False):
|
|
84
|
+
return
|
|
85
|
+
if event.button != 1 or event.inaxes is None or event.xdata is None:
|
|
86
|
+
return
|
|
87
|
+
self._range_select_axes = event.inaxes
|
|
88
|
+
self._range_select_start_x = event.xdata
|
|
89
|
+
# 始めに1回だけ背景を撮り、以降は選択の矩形だけを blit で描き直す(毎回全体を描くと系列が多いほど重い)
|
|
90
|
+
self._app.canvas.draw()
|
|
91
|
+
self._range_select_background = self._app.canvas.copy_from_bbox(event.inaxes.bbox)
|
|
92
|
+
|
|
93
|
+
def _on_range_select_motion(self, event):
|
|
94
|
+
axes = getattr(self, '_range_select_axes', None)
|
|
95
|
+
if axes is None or event.inaxes is not axes or event.xdata is None:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
x0, x1 = sorted((self._range_select_start_x, event.xdata))
|
|
99
|
+
ymin, ymax = axes.get_ylim()
|
|
100
|
+
|
|
101
|
+
rect = getattr(self, '_range_select_preview_artist', None)
|
|
102
|
+
if rect is None:
|
|
103
|
+
# animated=True の Artist は draw() では描かれず、blit だけで出る。矩形は最初に1回作り、座標だけ変える
|
|
104
|
+
rect = Rectangle(
|
|
105
|
+
(x0, ymin), x1 - x0, ymax - ymin,
|
|
106
|
+
facecolor='#3948B3', alpha=0.15, edgecolor='#3948B3',
|
|
107
|
+
linewidth=1, zorder=100, animated=True,
|
|
108
|
+
)
|
|
109
|
+
axes.add_patch(rect)
|
|
110
|
+
self._range_select_preview_artist = rect
|
|
111
|
+
else:
|
|
112
|
+
rect.set_bounds(x0, ymin, x1 - x0, ymax - ymin)
|
|
113
|
+
|
|
114
|
+
background = getattr(self, '_range_select_background', None)
|
|
115
|
+
if background is None:
|
|
116
|
+
# 背景が撮れていなければ全体を描き直す
|
|
117
|
+
self._app.canvas.draw_idle()
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
self._app.canvas.restore_region(background)
|
|
121
|
+
axes.draw_artist(rect)
|
|
122
|
+
self._app.canvas.blit(axes.bbox)
|
|
123
|
+
|
|
124
|
+
def _on_range_select_release(self, event):
|
|
125
|
+
axes = getattr(self, '_range_select_axes', None)
|
|
126
|
+
if axes is None:
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
self._clear_range_select_preview()
|
|
130
|
+
start_x = self._range_select_start_x
|
|
131
|
+
self._range_select_axes = None
|
|
132
|
+
self._range_select_start_x = None
|
|
133
|
+
|
|
134
|
+
end_x = event.xdata if (event.inaxes is axes and event.xdata is not None) else None
|
|
135
|
+
if end_x is None or start_x is None or start_x == end_x:
|
|
136
|
+
return # クリックだけ(ドラッグなし)は選択とみなさない
|
|
137
|
+
|
|
138
|
+
x_min, x_max = sorted((start_x, end_x))
|
|
139
|
+
self._apply_range_mask(axes, x_min, x_max)
|
|
140
|
+
|
|
141
|
+
def _apply_range_mask(self, axes, x_min, x_max):
|
|
142
|
+
"""[x_min, x_max] の点をマスクに加える。今のデータセットがこの軸に描かれていなければ、何もせず案内を出す。"""
|
|
143
|
+
dataset = self._app._get_current_dataset()
|
|
144
|
+
if dataset is None:
|
|
145
|
+
notify.information(self._app, "範囲選択", "マスク対象のデータセットを選択してください。")
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
target_axis = dataset.subplot_target
|
|
149
|
+
if dataset.use_secondary_y:
|
|
150
|
+
expected_axes = (
|
|
151
|
+
self._app.all_secondary_axes[target_axis]
|
|
152
|
+
if 0 <= target_axis < len(self._app.all_secondary_axes) else None
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
expected_axes = (
|
|
156
|
+
self._app.all_axes[target_axis]
|
|
157
|
+
if 0 <= target_axis < len(self._app.all_axes) else None
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if axes is not expected_axes:
|
|
161
|
+
notify.information(
|
|
162
|
+
self._app, "範囲選択",
|
|
163
|
+
"ドラッグしたサブプロットに、選択中のデータセットが描画されていません。"
|
|
164
|
+
)
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
# ドラッグの x はウォーターフォールのずらしが掛かった表示座標なので、データ座標に戻して比べる
|
|
168
|
+
data_x_min, _ = self._app.canvas.display_to_data(dataset, x_min, 0.0)
|
|
169
|
+
data_x_max, _ = self._app.canvas.display_to_data(dataset, x_max, 0.0)
|
|
170
|
+
|
|
171
|
+
x_data = dataset.x_data
|
|
172
|
+
visible_index = dataset.visible_df.index
|
|
173
|
+
in_range_mask = (x_data >= data_x_min) & (x_data <= data_x_max)
|
|
174
|
+
newly_masked = [int(idx) for idx, flag in zip(visible_index, in_range_mask) if flag]
|
|
175
|
+
if not newly_masked:
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
old_masked = list(dataset.masked_row_indices)
|
|
179
|
+
new_masked = sorted(set(old_masked) | set(newly_masked))
|
|
180
|
+
if new_masked == old_masked:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
command = SetMaskedRowsCommand(
|
|
184
|
+
dataset, old_masked, new_masked,
|
|
185
|
+
description=f"範囲選択でのマスク({len(newly_masked)}件)",
|
|
186
|
+
)
|
|
187
|
+
self._app.undo_stack.push(command)
|
|
188
|
+
self._app._update_plot()
|
|
189
|
+
self._app.statusBar().showMessage(
|
|
190
|
+
f"「{dataset.name}」の{len(newly_masked)}点をマスクしました", 3000
|
|
191
|
+
)
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""領域強調モード: X 方向に大きくドラッグすると縦の帯、Y 方向なら横の帯を足す。
|
|
2
|
+
|
|
3
|
+
帯は注釈と同じ 'annotations' に type='vspan' / 'hspan' で入れるので、保存・Undo・描画は注釈と共有する。
|
|
4
|
+
データ座標で持つので、データを読み直しても位置は変わらない。
|
|
5
|
+
"""
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from matplotlib.patches import Rectangle
|
|
9
|
+
from PySide6.QtWidgets import QMessageBox
|
|
10
|
+
|
|
11
|
+
from graphica.gui import notify
|
|
12
|
+
from graphica.core.axis_settings import axis_setting
|
|
13
|
+
from graphica.core.commands import SetAnnotationsCommand
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# これより動かなければクリック(誤クリックで極小の帯ができないように)
|
|
18
|
+
REGION_HIGHLIGHT_DRAG_THRESHOLD_PX = 5
|
|
19
|
+
|
|
20
|
+
REGION_HIGHLIGHT_DEFAULT_COLOR = '#F2A72B'
|
|
21
|
+
REGION_HIGHLIGHT_DEFAULT_ALPHA = 0.18
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RegionHighlightTool:
|
|
25
|
+
"""状態はこのツールが持ち、PlotterApp の同じ名前は窓口(gui/tools/__init__.py)。"""
|
|
26
|
+
|
|
27
|
+
# PlotterApp から同じ名前で読み書きできるもの(テスト・メニュー・ほかの mixin が使う)
|
|
28
|
+
EXPOSED_NAMES = (
|
|
29
|
+
'_clear_region_highlight_preview', '_on_region_highlight_motion', '_on_region_highlight_press',
|
|
30
|
+
'_on_region_highlight_release', '_region_highlight_axes', '_region_highlight_motion_cid',
|
|
31
|
+
'_region_highlight_orientation', '_region_highlight_press_cid', '_region_highlight_preview_artist',
|
|
32
|
+
'_region_highlight_release_cid', '_region_highlight_start', '_toggle_region_highlight_mode',
|
|
33
|
+
'_try_delete_region_near', 'region_highlight_mode_enabled',
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def __init__(self, app):
|
|
37
|
+
self._app = app
|
|
38
|
+
self.region_highlight_mode_enabled = False
|
|
39
|
+
self._region_highlight_press_cid = None
|
|
40
|
+
self._region_highlight_motion_cid = None
|
|
41
|
+
self._region_highlight_release_cid = None
|
|
42
|
+
self._region_highlight_axes = None
|
|
43
|
+
self._region_highlight_start = None # (x, y) データ座標
|
|
44
|
+
self._region_highlight_preview_artist = None
|
|
45
|
+
|
|
46
|
+
def _toggle_region_highlight_mode(self, checked):
|
|
47
|
+
self.region_highlight_mode_enabled = checked
|
|
48
|
+
|
|
49
|
+
if checked:
|
|
50
|
+
self._app._deactivate_other_mouse_modes('region_highlight')
|
|
51
|
+
|
|
52
|
+
self._region_highlight_press_cid = self._app.canvas.mpl_connect(
|
|
53
|
+
'button_press_event', self._on_region_highlight_press
|
|
54
|
+
)
|
|
55
|
+
self._region_highlight_motion_cid = self._app.canvas.mpl_connect(
|
|
56
|
+
'motion_notify_event', self._on_region_highlight_motion
|
|
57
|
+
)
|
|
58
|
+
self._region_highlight_release_cid = self._app.canvas.mpl_connect(
|
|
59
|
+
'button_release_event', self._on_region_highlight_release
|
|
60
|
+
)
|
|
61
|
+
self._app.statusBar().showMessage(
|
|
62
|
+
"領域ハイライトモード: 横方向にドラッグで縦帯、縦方向にドラッグで横帯を追加"
|
|
63
|
+
"(右クリックで削除)", 5000
|
|
64
|
+
)
|
|
65
|
+
else:
|
|
66
|
+
if getattr(self, '_region_highlight_press_cid', None) is not None:
|
|
67
|
+
self._app.canvas.mpl_disconnect(self._region_highlight_press_cid)
|
|
68
|
+
self._region_highlight_press_cid = None
|
|
69
|
+
if getattr(self, '_region_highlight_motion_cid', None) is not None:
|
|
70
|
+
self._app.canvas.mpl_disconnect(self._region_highlight_motion_cid)
|
|
71
|
+
self._region_highlight_motion_cid = None
|
|
72
|
+
if getattr(self, '_region_highlight_release_cid', None) is not None:
|
|
73
|
+
self._app.canvas.mpl_disconnect(self._region_highlight_release_cid)
|
|
74
|
+
self._region_highlight_release_cid = None
|
|
75
|
+
self._clear_region_highlight_preview()
|
|
76
|
+
self._region_highlight_axes = None
|
|
77
|
+
self._region_highlight_start = None
|
|
78
|
+
|
|
79
|
+
def _clear_region_highlight_preview(self):
|
|
80
|
+
"""ドラッグ中のプレビューを消す。描き直しで既に消えていても例外にしない。"""
|
|
81
|
+
artist = getattr(self, '_region_highlight_preview_artist', None)
|
|
82
|
+
if artist is not None:
|
|
83
|
+
try:
|
|
84
|
+
artist.remove()
|
|
85
|
+
except (ValueError, NotImplementedError):
|
|
86
|
+
pass
|
|
87
|
+
self._region_highlight_preview_artist = None
|
|
88
|
+
self._app.canvas.draw_idle()
|
|
89
|
+
|
|
90
|
+
def _on_region_highlight_press(self, event):
|
|
91
|
+
if not getattr(self, 'region_highlight_mode_enabled', False):
|
|
92
|
+
return
|
|
93
|
+
if event.inaxes is None or event.xdata is None or event.ydata is None:
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
if event.button == 3: # 右クリックは削除
|
|
97
|
+
self._try_delete_region_near(event)
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
self._region_highlight_axes = event.inaxes
|
|
101
|
+
self._region_highlight_start = (event.xdata, event.ydata)
|
|
102
|
+
|
|
103
|
+
def _on_region_highlight_motion(self, event):
|
|
104
|
+
axes = getattr(self, '_region_highlight_axes', None)
|
|
105
|
+
if axes is None or event.inaxes is not axes or event.xdata is None or event.ydata is None:
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
start_x, start_y = self._region_highlight_start
|
|
109
|
+
orientation = self._region_highlight_orientation(axes, start_x, start_y, event.xdata, event.ydata)
|
|
110
|
+
if orientation is None:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
self._clear_region_highlight_preview()
|
|
114
|
+
xmin, xmax = axes.get_xlim()
|
|
115
|
+
ymin, ymax = axes.get_ylim()
|
|
116
|
+
if orientation == 'vspan':
|
|
117
|
+
x0, x1 = sorted((start_x, event.xdata))
|
|
118
|
+
rect = Rectangle(
|
|
119
|
+
(x0, ymin), x1 - x0, ymax - ymin,
|
|
120
|
+
facecolor=REGION_HIGHLIGHT_DEFAULT_COLOR, alpha=0.25,
|
|
121
|
+
edgecolor=REGION_HIGHLIGHT_DEFAULT_COLOR, linewidth=1, zorder=100,
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
y0, y1 = sorted((start_y, event.ydata))
|
|
125
|
+
rect = Rectangle(
|
|
126
|
+
(xmin, y0), xmax - xmin, y1 - y0,
|
|
127
|
+
facecolor=REGION_HIGHLIGHT_DEFAULT_COLOR, alpha=0.25,
|
|
128
|
+
edgecolor=REGION_HIGHLIGHT_DEFAULT_COLOR, linewidth=1, zorder=100,
|
|
129
|
+
)
|
|
130
|
+
axes.add_patch(rect)
|
|
131
|
+
self._region_highlight_preview_artist = rect
|
|
132
|
+
self._app.canvas.draw_idle()
|
|
133
|
+
|
|
134
|
+
def _on_region_highlight_release(self, event):
|
|
135
|
+
axes = getattr(self, '_region_highlight_axes', None)
|
|
136
|
+
if axes is None:
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
self._clear_region_highlight_preview()
|
|
140
|
+
start = self._region_highlight_start
|
|
141
|
+
self._region_highlight_axes = None
|
|
142
|
+
self._region_highlight_start = None
|
|
143
|
+
if start is None:
|
|
144
|
+
return
|
|
145
|
+
start_x, start_y = start
|
|
146
|
+
|
|
147
|
+
if event.inaxes is not axes or event.xdata is None or event.ydata is None:
|
|
148
|
+
return # 別の軸か軸の外で離した
|
|
149
|
+
|
|
150
|
+
end_x, end_y = event.xdata, event.ydata
|
|
151
|
+
orientation = self._region_highlight_orientation(axes, start_x, start_y, end_x, end_y)
|
|
152
|
+
if orientation is None:
|
|
153
|
+
return # クリックだけ(ドラッグなし)
|
|
154
|
+
|
|
155
|
+
axis_index = self._app._find_axis_index(axes)
|
|
156
|
+
if axis_index is None:
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
if orientation == 'vspan':
|
|
160
|
+
value_range = tuple(sorted((float(start_x), float(end_x))))
|
|
161
|
+
else:
|
|
162
|
+
value_range = tuple(sorted((float(start_y), float(end_y))))
|
|
163
|
+
|
|
164
|
+
self._app._add_annotation(axis_index, {
|
|
165
|
+
'type': orientation, 'range': value_range,
|
|
166
|
+
'color': REGION_HIGHLIGHT_DEFAULT_COLOR, 'alpha': REGION_HIGHLIGHT_DEFAULT_ALPHA,
|
|
167
|
+
}, description="領域ハイライトの追加")
|
|
168
|
+
|
|
169
|
+
def _region_highlight_orientation(self, axes, start_x, start_y, current_x, current_y):
|
|
170
|
+
"""'vspan'(横に大きく動いた)/ 'hspan'(縦に大きく動いた)/ None(まだドラッグとみなせない)。"""
|
|
171
|
+
start_px = axes.transData.transform((start_x, start_y))
|
|
172
|
+
current_px = axes.transData.transform((current_x, current_y))
|
|
173
|
+
dx = abs(current_px[0] - start_px[0])
|
|
174
|
+
dy = abs(current_px[1] - start_px[1])
|
|
175
|
+
if max(dx, dy) < REGION_HIGHLIGHT_DRAG_THRESHOLD_PX:
|
|
176
|
+
return None
|
|
177
|
+
return 'vspan' if dx >= dy else 'hspan'
|
|
178
|
+
|
|
179
|
+
def _try_delete_region_near(self, event):
|
|
180
|
+
"""右クリックした位置の帯を、確認してから消す。重なっていれば最後に足したもの(いちばん手前)。"""
|
|
181
|
+
axis_index = self._app._find_axis_index(event.inaxes)
|
|
182
|
+
if axis_index is None:
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
settings = self._app.project.all_plot_settings[axis_index]
|
|
186
|
+
annotations = axis_setting(settings, 'annotations')
|
|
187
|
+
|
|
188
|
+
target_index = None
|
|
189
|
+
for i, ann in enumerate(annotations):
|
|
190
|
+
ann_type = ann.get('type')
|
|
191
|
+
if ann_type not in ('vspan', 'hspan'):
|
|
192
|
+
continue
|
|
193
|
+
value = event.xdata if ann_type == 'vspan' else event.ydata
|
|
194
|
+
lo, hi = ann.get('range', (None, None))
|
|
195
|
+
if lo is None or hi is None or value is None:
|
|
196
|
+
continue
|
|
197
|
+
if lo <= value <= hi:
|
|
198
|
+
target_index = i # 後ろのものほど手前
|
|
199
|
+
|
|
200
|
+
if target_index is None:
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
target = annotations[target_index]
|
|
204
|
+
label = "縦帯" if target.get('type') == 'vspan' else "横帯"
|
|
205
|
+
lo, hi = target.get('range', (None, None))
|
|
206
|
+
reply = notify.question(
|
|
207
|
+
self._app, "領域ハイライトの削除",
|
|
208
|
+
f"この{label}を削除しますか?\n\n範囲: {lo:.4g} 〜 {hi:.4g}",
|
|
209
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
210
|
+
QMessageBox.StandardButton.Yes
|
|
211
|
+
)
|
|
212
|
+
if reply != QMessageBox.StandardButton.Yes:
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
new_list = list(annotations)
|
|
216
|
+
del new_list[target_index]
|
|
217
|
+
|
|
218
|
+
command = SetAnnotationsCommand(
|
|
219
|
+
self._app.project, axis_index, annotations, new_list,
|
|
220
|
+
self._app._update_plot_appearance, description="領域ハイライトの削除"
|
|
221
|
+
)
|
|
222
|
+
self._app.undo_stack.push(command)
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""スライス抽出モード: 2D マップの上でドラッグした線分に沿った断面を、新しい1次元のデータセットにする。
|
|
2
|
+
|
|
3
|
+
対象は今選んでいるデータセット(その軸に描かれていること)。線分がほぼ水平・垂直ならその軸の値、
|
|
4
|
+
斜めなら始点からの距離を X にする。
|
|
5
|
+
"""
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from graphica.gui import notify
|
|
10
|
+
from graphica.core.provenance import build_provenance
|
|
11
|
+
from graphica.core.dataset import Dataset
|
|
12
|
+
from graphica.core.grid_data import extract_slice, GridDataError
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
SLICE_EXTRACTION_N_POINTS = 200
|
|
17
|
+
|
|
18
|
+
# extract_slice() の 'axis_kind' ごとの、新しいデータセットの X の列名とラベル
|
|
19
|
+
_AXIS_KIND_LABELS = {
|
|
20
|
+
'x': 'X',
|
|
21
|
+
'y': 'Y',
|
|
22
|
+
'distance': '始点からの距離',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SliceExtractionTool:
|
|
27
|
+
"""状態はこのツールが持ち、PlotterApp の同じ名前は窓口(gui/tools/__init__.py)。"""
|
|
28
|
+
|
|
29
|
+
# PlotterApp から同じ名前で読み書きできるもの(テスト・メニュー・ほかの mixin が使う)
|
|
30
|
+
EXPOSED_NAMES = (
|
|
31
|
+
'_apply_slice_extraction', '_clear_slice_extraction_preview', '_create_slice_dataset',
|
|
32
|
+
'_on_slice_extraction_motion', '_on_slice_extraction_press', '_on_slice_extraction_release',
|
|
33
|
+
'_slice_extraction_axes', '_slice_extraction_motion_cid', '_slice_extraction_press_cid',
|
|
34
|
+
'_slice_extraction_preview_artist', '_slice_extraction_release_cid', '_slice_extraction_start',
|
|
35
|
+
'_toggle_slice_extraction_mode', 'slice_extraction_mode_enabled',
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def __init__(self, app):
|
|
39
|
+
self._app = app
|
|
40
|
+
self.slice_extraction_mode_enabled = False
|
|
41
|
+
self._slice_extraction_press_cid = None
|
|
42
|
+
self._slice_extraction_motion_cid = None
|
|
43
|
+
self._slice_extraction_release_cid = None
|
|
44
|
+
self._slice_extraction_axes = None
|
|
45
|
+
self._slice_extraction_start = None # (x, y) データ座標
|
|
46
|
+
self._slice_extraction_preview_artist = None
|
|
47
|
+
|
|
48
|
+
def _toggle_slice_extraction_mode(self, checked):
|
|
49
|
+
self.slice_extraction_mode_enabled = checked
|
|
50
|
+
|
|
51
|
+
if checked:
|
|
52
|
+
self._app._deactivate_other_mouse_modes('slice_extraction')
|
|
53
|
+
|
|
54
|
+
self._slice_extraction_press_cid = self._app.canvas.mpl_connect(
|
|
55
|
+
'button_press_event', self._on_slice_extraction_press
|
|
56
|
+
)
|
|
57
|
+
self._slice_extraction_motion_cid = self._app.canvas.mpl_connect(
|
|
58
|
+
'motion_notify_event', self._on_slice_extraction_motion
|
|
59
|
+
)
|
|
60
|
+
self._slice_extraction_release_cid = self._app.canvas.mpl_connect(
|
|
61
|
+
'button_release_event', self._on_slice_extraction_release
|
|
62
|
+
)
|
|
63
|
+
self._app.statusBar().showMessage(
|
|
64
|
+
"スライス抽出モード: カレントの2Dマップ上でドラッグした線分に沿って"
|
|
65
|
+
"1Dデータセットを抽出します", 5000
|
|
66
|
+
)
|
|
67
|
+
else:
|
|
68
|
+
for attr in ('_slice_extraction_press_cid', '_slice_extraction_motion_cid',
|
|
69
|
+
'_slice_extraction_release_cid'):
|
|
70
|
+
cid = getattr(self, attr, None)
|
|
71
|
+
if cid is not None:
|
|
72
|
+
self._app.canvas.mpl_disconnect(cid)
|
|
73
|
+
setattr(self, attr, None)
|
|
74
|
+
self._clear_slice_extraction_preview()
|
|
75
|
+
self._slice_extraction_axes = None
|
|
76
|
+
self._slice_extraction_start = None
|
|
77
|
+
|
|
78
|
+
def _clear_slice_extraction_preview(self):
|
|
79
|
+
"""ドラッグ中のプレビューを消す。描き直しで既に消えていても例外にしない。"""
|
|
80
|
+
artist = getattr(self, '_slice_extraction_preview_artist', None)
|
|
81
|
+
if artist is not None:
|
|
82
|
+
try:
|
|
83
|
+
artist.remove()
|
|
84
|
+
except (ValueError, NotImplementedError):
|
|
85
|
+
pass
|
|
86
|
+
self._slice_extraction_preview_artist = None
|
|
87
|
+
self._app.canvas.draw_idle()
|
|
88
|
+
|
|
89
|
+
def _on_slice_extraction_press(self, event):
|
|
90
|
+
if not getattr(self, 'slice_extraction_mode_enabled', False):
|
|
91
|
+
return
|
|
92
|
+
if event.button != 1 or event.inaxes is None or event.xdata is None or event.ydata is None:
|
|
93
|
+
return
|
|
94
|
+
self._slice_extraction_axes = event.inaxes
|
|
95
|
+
self._slice_extraction_start = (event.xdata, event.ydata)
|
|
96
|
+
|
|
97
|
+
def _on_slice_extraction_motion(self, event):
|
|
98
|
+
axes = getattr(self, '_slice_extraction_axes', None)
|
|
99
|
+
if axes is None or event.inaxes is not axes or event.xdata is None or event.ydata is None:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
self._clear_slice_extraction_preview()
|
|
103
|
+
start = self._slice_extraction_start
|
|
104
|
+
(line,) = axes.plot(
|
|
105
|
+
[start[0], event.xdata], [start[1], event.ydata],
|
|
106
|
+
color='#E4572E', linestyle='--', linewidth=1.5, zorder=100,
|
|
107
|
+
)
|
|
108
|
+
self._slice_extraction_preview_artist = line
|
|
109
|
+
self._app.canvas.draw_idle()
|
|
110
|
+
|
|
111
|
+
def _on_slice_extraction_release(self, event):
|
|
112
|
+
axes = getattr(self, '_slice_extraction_axes', None)
|
|
113
|
+
if axes is None:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
self._clear_slice_extraction_preview()
|
|
117
|
+
start = self._slice_extraction_start
|
|
118
|
+
self._slice_extraction_axes = None
|
|
119
|
+
self._slice_extraction_start = None
|
|
120
|
+
|
|
121
|
+
if event.inaxes is not axes or event.xdata is None or event.ydata is None:
|
|
122
|
+
return
|
|
123
|
+
end = (event.xdata, event.ydata)
|
|
124
|
+
if start == end:
|
|
125
|
+
return # クリックだけ(ドラッグなし)
|
|
126
|
+
|
|
127
|
+
self._apply_slice_extraction(axes, start, end)
|
|
128
|
+
|
|
129
|
+
def _apply_slice_extraction(self, axes, start, end):
|
|
130
|
+
"""今のデータセットが 2D の格子でないか、この軸に描かれていなければ、何もせず案内を出す。"""
|
|
131
|
+
dataset = self._app._get_current_dataset()
|
|
132
|
+
if dataset is None or dataset.data_kind != '2d_grid':
|
|
133
|
+
notify.information(
|
|
134
|
+
self._app, "スライス抽出", "スライス抽出の対象となる2Dマップのデータセットを選択してください。"
|
|
135
|
+
)
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
target_axis = dataset.subplot_target
|
|
139
|
+
if dataset.use_secondary_y:
|
|
140
|
+
expected_axes = (
|
|
141
|
+
self._app.all_secondary_axes[target_axis]
|
|
142
|
+
if 0 <= target_axis < len(self._app.all_secondary_axes) else None
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
expected_axes = (
|
|
146
|
+
self._app.all_axes[target_axis]
|
|
147
|
+
if 0 <= target_axis < len(self._app.all_axes) else None
|
|
148
|
+
)
|
|
149
|
+
if axes is not expected_axes:
|
|
150
|
+
notify.information(
|
|
151
|
+
self._app, "スライス抽出",
|
|
152
|
+
"ドラッグしたサブプロットに、選択中の2Dマップが描画されていません。"
|
|
153
|
+
)
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
grid = dataset.z_grid
|
|
157
|
+
if grid is None:
|
|
158
|
+
notify.warning(self._app, "スライス抽出", "有効な2Dグリッドデータがありません。")
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
result = extract_slice(
|
|
163
|
+
grid['x_grid'], grid['y_grid'], grid['z_grid'],
|
|
164
|
+
start=start, end=end, n_points=SLICE_EXTRACTION_N_POINTS,
|
|
165
|
+
)
|
|
166
|
+
except GridDataError as e:
|
|
167
|
+
notify.warning(self._app, "スライス抽出", f"スライスの抽出に失敗しました:\n{e}")
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
self._create_slice_dataset(dataset, start, end, result)
|
|
171
|
+
|
|
172
|
+
def _create_slice_dataset(self, source_dataset, start, end, result):
|
|
173
|
+
import pandas as pd
|
|
174
|
+
|
|
175
|
+
axis_kind = result['axis_kind']
|
|
176
|
+
x_label = _AXIS_KIND_LABELS.get(axis_kind, axis_kind)
|
|
177
|
+
df = pd.DataFrame({'x': result['axis_values'], 'y': result['z_values']})
|
|
178
|
+
params = {
|
|
179
|
+
'start': [float(start[0]), float(start[1])],
|
|
180
|
+
'end': [float(end[0]), float(end[1])],
|
|
181
|
+
'axis_kind': axis_kind,
|
|
182
|
+
'n_points': SLICE_EXTRACTION_N_POINTS,
|
|
183
|
+
}
|
|
184
|
+
slice_dataset = Dataset(
|
|
185
|
+
name=f"Slice ({source_dataset.name})",
|
|
186
|
+
df=df, x_col_name='x', y_col_name='y',
|
|
187
|
+
provenance=build_provenance('2d_slice', params, [source_dataset]),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
original_item = self._app._get_dataset_tree_item(source_dataset)
|
|
191
|
+
folder = original_item.parent() if original_item else None
|
|
192
|
+
|
|
193
|
+
def add():
|
|
194
|
+
self._app.dataset_order.append(slice_dataset, folder)
|
|
195
|
+
self._app._update_plot()
|
|
196
|
+
|
|
197
|
+
self._app._push_dataset_additions(add, [slice_dataset], "スライスの抽出")
|
|
198
|
+
self._app.statusBar().showMessage(
|
|
199
|
+
f"「{source_dataset.name}」からスライスを抽出しました(X軸: {x_label})", 4000
|
|
200
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""組み立てた画面の文字を、あとからまとめて tr() で訳す(Designer の .ui で作った部品にも効かせるため)。
|
|
2
|
+
|
|
3
|
+
日本語のときは何もしない。選択肢(QComboBox)は文字そのものを値として保存するものがあるので、ここでは
|
|
4
|
+
番号やデータで値を持つと分かっているものだけを訳す(ほかは作る所で tr() を通す)。
|
|
5
|
+
"""
|
|
6
|
+
from PySide6.QtWidgets import (QAbstractButton, QComboBox, QDockWidget, QGroupBox, QLabel, QLineEdit, QTabWidget,
|
|
7
|
+
QToolBar, QWidget)
|
|
8
|
+
|
|
9
|
+
from graphica.core.i18n import DEFAULT_LANGUAGE, get_language, tr
|
|
10
|
+
|
|
11
|
+
# 選んだ番号で値を持つ選択肢(gui/axis_bindings.py の index)。表示を訳しても保存する値は変わらない
|
|
12
|
+
INDEX_BOUND_COMBOS = ("x_major_tick_mode_combo", "y_major_tick_mode_combo")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _translate(text):
|
|
16
|
+
translated = tr(text)
|
|
17
|
+
return translated if translated != text else None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def translate_widget_texts(root):
|
|
21
|
+
if get_language() == DEFAULT_LANGUAGE:
|
|
22
|
+
return
|
|
23
|
+
for widget in [root] + root.findChildren(QWidget):
|
|
24
|
+
tooltip = widget.toolTip()
|
|
25
|
+
if tooltip and (new := _translate(tooltip)):
|
|
26
|
+
widget.setToolTip(new)
|
|
27
|
+
if isinstance(widget, (QLabel, QAbstractButton)):
|
|
28
|
+
if (new := _translate(widget.text())):
|
|
29
|
+
widget.setText(new)
|
|
30
|
+
elif isinstance(widget, QGroupBox):
|
|
31
|
+
if (new := _translate(widget.title())):
|
|
32
|
+
widget.setTitle(new)
|
|
33
|
+
elif isinstance(widget, (QDockWidget, QToolBar)):
|
|
34
|
+
if (new := _translate(widget.windowTitle())):
|
|
35
|
+
widget.setWindowTitle(new)
|
|
36
|
+
elif isinstance(widget, QLineEdit):
|
|
37
|
+
if widget.placeholderText() and (new := _translate(widget.placeholderText())):
|
|
38
|
+
widget.setPlaceholderText(new)
|
|
39
|
+
elif isinstance(widget, QComboBox) and widget.objectName() in INDEX_BOUND_COMBOS:
|
|
40
|
+
for i in range(widget.count()):
|
|
41
|
+
if (new := _translate(widget.itemText(i))):
|
|
42
|
+
widget.setItemText(i, new)
|
|
43
|
+
if isinstance(widget, QTabWidget):
|
|
44
|
+
for i in range(widget.count()):
|
|
45
|
+
if (new := _translate(widget.tabText(i))):
|
|
46
|
+
widget.setTabText(i, new)
|