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,148 @@
|
|
|
1
|
+
"""データセットの処理の流れ(対象の確認 → ダイアログ → 入力の確認 → 計算 → 結果の追加)を担う実行役。
|
|
2
|
+
|
|
3
|
+
操作ごとの違い(確認の順番、警告の題と文言、例外を捕まえる範囲、Undo の有無)は操作の本体に残す。
|
|
4
|
+
ここで捕まえるのは本体が calculate() に渡した計算の ValueError だけで、それ以外の例外は今までどおり外に出る。
|
|
5
|
+
"""
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Callable
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from PySide6.QtWidgets import QDialog
|
|
11
|
+
|
|
12
|
+
from graphica.gui import notify
|
|
13
|
+
|
|
14
|
+
INPUT_ERROR_TITLE = "入力エラー"
|
|
15
|
+
EMPTY_OUTPUT_NAME_TEXT = "出力データセット名が空です。"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OperationStopped(Exception):
|
|
19
|
+
"""流れを止める。知らせる必要があれば、止める前に知らせてある。"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OperationState:
|
|
23
|
+
"""操作をまたいで持つもの。非モーダルの結果の窓は、次に同じ処理をしたときに閉じる。"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self.result_windows = {}
|
|
27
|
+
self.copied_style = None # スタイルのコピーの控え(貼り付けるまで持つ)
|
|
28
|
+
self.runners = {} # バックグラウンドで動いている計算。終わるまで同じ種類の計算は始めない
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class Operation:
|
|
33
|
+
title: str # 警告・お知らせの題
|
|
34
|
+
body: Callable[["OperationContext"], None]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class OperationContext:
|
|
38
|
+
"""操作の本体に渡す。流れを止める手順と、結果の追加の手順を持つ。"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, host, state, title):
|
|
41
|
+
self.host = host
|
|
42
|
+
self.state = state
|
|
43
|
+
self.title = title
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def parent(self):
|
|
47
|
+
return self.host.parent_widget
|
|
48
|
+
|
|
49
|
+
# --- 流れを止める ---
|
|
50
|
+
|
|
51
|
+
def stop(self):
|
|
52
|
+
raise OperationStopped
|
|
53
|
+
|
|
54
|
+
def stop_with_warning(self, text, title=None):
|
|
55
|
+
notify.warning(self.parent, title or self.title, text)
|
|
56
|
+
raise OperationStopped
|
|
57
|
+
|
|
58
|
+
def stop_with_information(self, text, title=None):
|
|
59
|
+
notify.information(self.parent, title or self.title, text)
|
|
60
|
+
raise OperationStopped
|
|
61
|
+
|
|
62
|
+
def information(self, text, title=None):
|
|
63
|
+
notify.information(self.parent, title or self.title, text)
|
|
64
|
+
|
|
65
|
+
# --- 対象 ---
|
|
66
|
+
|
|
67
|
+
def current_dataset(self):
|
|
68
|
+
"""今のデータセット。無ければ何も言わずに止める。"""
|
|
69
|
+
dataset = self.host.current_dataset()
|
|
70
|
+
if dataset is None:
|
|
71
|
+
self.stop()
|
|
72
|
+
return dataset
|
|
73
|
+
|
|
74
|
+
def selected_datasets(self, *, exactly=None, at_least=None, message=None):
|
|
75
|
+
"""数が合わなければ止める。message が無ければ何も言わずに止める。"""
|
|
76
|
+
selected = self.host.selected_datasets()
|
|
77
|
+
if (exactly is not None and len(selected) != exactly) or (at_least is not None and len(selected) < at_least):
|
|
78
|
+
if message is None:
|
|
79
|
+
self.stop()
|
|
80
|
+
self.stop_with_information(message)
|
|
81
|
+
return selected
|
|
82
|
+
|
|
83
|
+
def as_numbers(self, dataset, axis):
|
|
84
|
+
"""X か Y の列を数に直す。文字の列(カテゴリ軸)なら、その処理の題で知らせて止める。"""
|
|
85
|
+
values, column = (dataset.x_data, dataset.x_col_name) if axis == "x" else (dataset.y_data, dataset.y_col_name)
|
|
86
|
+
try:
|
|
87
|
+
return np.asarray(values, dtype=float)
|
|
88
|
+
except (TypeError, ValueError):
|
|
89
|
+
if axis == "x":
|
|
90
|
+
self.stop_with_warning(
|
|
91
|
+
f"X軸の列「{column}」が数値ではないため、この処理はできません"
|
|
92
|
+
"(文字の列はカテゴリ軸として表示だけできます)。")
|
|
93
|
+
self.stop_with_warning(f"Y軸の列「{column}」が数値ではないため、この処理はできません。")
|
|
94
|
+
|
|
95
|
+
def valid_points(self, dataset):
|
|
96
|
+
"""X と Y を数に直し、どちらかが NaN の点を除く。"""
|
|
97
|
+
x = self.as_numbers(dataset, "x")
|
|
98
|
+
y = self.as_numbers(dataset, "y")
|
|
99
|
+
valid = ~(np.isnan(x) | np.isnan(y))
|
|
100
|
+
return x[valid], y[valid]
|
|
101
|
+
|
|
102
|
+
# --- ダイアログと入力 ---
|
|
103
|
+
|
|
104
|
+
def ask(self, dialog):
|
|
105
|
+
"""取り消されたら止める。"""
|
|
106
|
+
if dialog.exec() != QDialog.DialogCode.Accepted:
|
|
107
|
+
self.stop()
|
|
108
|
+
return dialog
|
|
109
|
+
|
|
110
|
+
def require_output_name(self, output_name):
|
|
111
|
+
if not output_name:
|
|
112
|
+
self.stop_with_warning(EMPTY_OUTPUT_NAME_TEXT, title=INPUT_ERROR_TITLE)
|
|
113
|
+
|
|
114
|
+
def calculate(self, compute):
|
|
115
|
+
"""compute() の ValueError は、その文言を警告にして止める。"""
|
|
116
|
+
try:
|
|
117
|
+
return compute()
|
|
118
|
+
except ValueError as e:
|
|
119
|
+
self.stop_with_warning(str(e))
|
|
120
|
+
|
|
121
|
+
# --- 結果 ---
|
|
122
|
+
|
|
123
|
+
def add(self, dataset):
|
|
124
|
+
"""Undo できる追加。Undo の名前はその処理の題。"""
|
|
125
|
+
self.host.add_dataset_with_undo(dataset, self.host.target_folder_for_new_dataset(), description=self.title)
|
|
126
|
+
|
|
127
|
+
def status(self, text, timeout_ms):
|
|
128
|
+
self.host.show_status(text, timeout_ms)
|
|
129
|
+
|
|
130
|
+
def add_and_report(self, dataset, timeout_ms=3000):
|
|
131
|
+
self.add(dataset)
|
|
132
|
+
self.status(f"「{dataset.name}」を追加しました", timeout_ms)
|
|
133
|
+
|
|
134
|
+
def show_result_window(self, key, create_window):
|
|
135
|
+
"""同じ key の前の窓を閉じてから作って出す。"""
|
|
136
|
+
previous = self.state.result_windows.get(key)
|
|
137
|
+
if previous is not None:
|
|
138
|
+
previous.close()
|
|
139
|
+
window = create_window()
|
|
140
|
+
self.state.result_windows[key] = window
|
|
141
|
+
window.show()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def run_operation(operation, host, state):
|
|
145
|
+
try:
|
|
146
|
+
operation.body(OperationContext(host, state, operation.title))
|
|
147
|
+
except OperationStopped:
|
|
148
|
+
pass
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""データセットをタブの外とやり取りする本体: データ表の書き出し、別のタブへのコピー・移動、スタイルのコピー、元ファイルからの再読み込み。"""
|
|
2
|
+
import copy
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from PySide6.QtWidgets import QApplication
|
|
10
|
+
|
|
11
|
+
from graphica.gui import notify
|
|
12
|
+
from graphica.core.methods_text import generate_methods_text
|
|
13
|
+
from graphica.gui.datasets.operations.runner import Operation
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# スタイルのコピーで写す見た目の属性。data_kind/z_col_name は「どの列を使うか」という構造の選択なので含めない
|
|
18
|
+
# (写すと相手を意図せず2Dマップ扱いにしてしまう)。
|
|
19
|
+
STYLE_ATTRS = ('plot_type', 'color', 'linestyle', 'linewidth', 'marker', 'markersize', 'smoothing',
|
|
20
|
+
'smoothing_method', 'alpha',
|
|
21
|
+
'error_display', 'colormap', 'vmin', 'vmax', 'grid_interp_method')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _write_failed(op, error):
|
|
25
|
+
logger.exception("データセットの書き出しに失敗しました")
|
|
26
|
+
op.stop_with_warning(f"ファイルの書き出しに失敗しました:\n{error}", title="書き出しエラー")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _export_one(op, dataset):
|
|
30
|
+
default_name = re.sub(r'[\\/:*?"<>|]', '_', dataset.name) or "dataset"
|
|
31
|
+
file_path, selected_filter = notify.get_save_file_name(
|
|
32
|
+
op.parent, "データ表を書き出す", default_name, "CSV Files (*.csv);;Excel Files (*.xlsx)"
|
|
33
|
+
)
|
|
34
|
+
if not file_path:
|
|
35
|
+
op.stop()
|
|
36
|
+
try:
|
|
37
|
+
if file_path.lower().endswith('.xlsx') or "Excel" in selected_filter:
|
|
38
|
+
if not file_path.lower().endswith('.xlsx'):
|
|
39
|
+
file_path += '.xlsx'
|
|
40
|
+
dataset.df.to_excel(file_path, index=False)
|
|
41
|
+
else:
|
|
42
|
+
if not file_path.lower().endswith('.csv'):
|
|
43
|
+
file_path += '.csv'
|
|
44
|
+
dataset.df.to_csv(file_path, index=False, encoding='utf-8-sig')
|
|
45
|
+
except Exception as e:
|
|
46
|
+
_write_failed(op, e)
|
|
47
|
+
op.information(f"書き出しました:\n{file_path}", title="書き出し完了")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _export_book(op, selected):
|
|
51
|
+
file_path, _ = notify.get_save_file_name(op.parent, "データ表を書き出す", "datasets.xlsx", "Excel Files (*.xlsx)")
|
|
52
|
+
if not file_path:
|
|
53
|
+
op.stop()
|
|
54
|
+
if not file_path.lower().endswith('.xlsx'):
|
|
55
|
+
file_path += '.xlsx'
|
|
56
|
+
used_sheet_names = set()
|
|
57
|
+
try:
|
|
58
|
+
with pd.ExcelWriter(file_path, engine='openpyxl') as writer:
|
|
59
|
+
for dataset in selected:
|
|
60
|
+
sheet_name = re.sub(r'[\\/:*?\[\]]', '_', dataset.name)[:31] or "Sheet"
|
|
61
|
+
base_name, suffix = sheet_name, 1
|
|
62
|
+
while sheet_name in used_sheet_names:
|
|
63
|
+
suffix += 1
|
|
64
|
+
sheet_name = f"{base_name[:28]}_{suffix}"
|
|
65
|
+
used_sheet_names.add(sheet_name)
|
|
66
|
+
dataset.df.to_excel(writer, sheet_name=sheet_name, index=False)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
_write_failed(op, e)
|
|
69
|
+
op.information(f"{len(selected)}件を書き出しました:\n{file_path}", title="書き出し完了")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _export_folder(op, selected):
|
|
73
|
+
dir_path = notify.get_existing_directory(op.parent, "書き出し先フォルダを選択")
|
|
74
|
+
if not dir_path:
|
|
75
|
+
op.stop()
|
|
76
|
+
succeeded, failed = [], []
|
|
77
|
+
used_names = set()
|
|
78
|
+
for dataset in selected:
|
|
79
|
+
base_name = re.sub(r'[\\/:*?"<>|]', '_', dataset.name) or "dataset"
|
|
80
|
+
file_name, suffix = base_name, 1
|
|
81
|
+
while file_name in used_names:
|
|
82
|
+
suffix += 1
|
|
83
|
+
file_name = f"{base_name}_{suffix}"
|
|
84
|
+
used_names.add(file_name)
|
|
85
|
+
try:
|
|
86
|
+
dataset.df.to_csv(os.path.join(dir_path, f"{file_name}.csv"), index=False, encoding='utf-8-sig')
|
|
87
|
+
succeeded.append(dataset.name)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
failed.append(f"{dataset.name}: {e}")
|
|
90
|
+
message = f"{len(succeeded)}件を書き出しました。"
|
|
91
|
+
if failed:
|
|
92
|
+
message += "\n\n失敗:\n" + "\n".join(failed)
|
|
93
|
+
op.information(message, title="書き出し完了")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def export_data(op):
|
|
97
|
+
"""1件ならCSVかExcelの1ファイル、複数ならデータセットごとのCSVか、シートに分けた1つのExcelブックに書き出す。"""
|
|
98
|
+
selected = op.selected_datasets(at_least=1)
|
|
99
|
+
if len(selected) == 1:
|
|
100
|
+
_export_one(op, selected[0])
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
format_choice, ok = notify.get_item(
|
|
104
|
+
op.parent, "データ表を書き出す", "書き出し形式を選択してください:",
|
|
105
|
+
["CSV (データセットごとに別ファイル)", "Excel (1ブックにシート分け)"], 0, False
|
|
106
|
+
)
|
|
107
|
+
if not ok:
|
|
108
|
+
op.stop()
|
|
109
|
+
if format_choice.startswith("Excel"):
|
|
110
|
+
_export_book(op, selected)
|
|
111
|
+
else:
|
|
112
|
+
_export_folder(op, selected)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def copy_or_move_to_tab(op, move):
|
|
116
|
+
"""
|
|
117
|
+
選んだデータセットを複製して別のタブに足す(移動なら、足した後でこのタブから消す)。
|
|
118
|
+
タブごとに Undo の履歴が別なので、相手のタブへの追加は Undo できない。移動を Undo しても
|
|
119
|
+
このタブの削除だけが戻り、両方のタブにある状態になる。
|
|
120
|
+
"""
|
|
121
|
+
selected = op.selected_datasets(at_least=1)
|
|
122
|
+
sibling_tabs = op.host.sibling_tabs()
|
|
123
|
+
if not sibling_tabs:
|
|
124
|
+
op.stop_with_information("コピー/移動先の他のタブがありません。")
|
|
125
|
+
|
|
126
|
+
tab_titles = [title for title, _ in sibling_tabs]
|
|
127
|
+
action_label = "移動" if move else "コピー"
|
|
128
|
+
choice, ok = notify.get_item(op.parent, f"別のタブへ{action_label}", "転送先のタブ:", tab_titles, 0, False)
|
|
129
|
+
if not ok:
|
|
130
|
+
op.stop()
|
|
131
|
+
target = sibling_tabs[tab_titles.index(choice)][1]
|
|
132
|
+
|
|
133
|
+
for dataset in selected:
|
|
134
|
+
new_dataset = copy.deepcopy(dataset)
|
|
135
|
+
new_dataset.dataset_id = uuid.uuid4().hex
|
|
136
|
+
target.add_dataset(new_dataset, target.target_folder_for_new_dataset())
|
|
137
|
+
|
|
138
|
+
if move:
|
|
139
|
+
op.host.remove_datasets(selected, description=f"別のタブへ移動({len(selected)}件)")
|
|
140
|
+
|
|
141
|
+
op.host.show_status(f"{len(selected)}件のデータセットを「{choice}」へ{action_label}しました")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def copy_style(op):
|
|
145
|
+
"""今のデータセットの見た目を値で控える(後で元を変えても控えは変わらない)。"""
|
|
146
|
+
dataset = op.current_dataset()
|
|
147
|
+
op.state.copied_style = {attr: getattr(dataset, attr) for attr in STYLE_ATTRS}
|
|
148
|
+
op.host.show_status(f"「{dataset.name}」のスタイルをコピーしました")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def paste_style(op):
|
|
152
|
+
"""控えた見た目を選んだデータセットすべてに当てる(Undo 1回分)。"""
|
|
153
|
+
copied_style = op.state.copied_style
|
|
154
|
+
if copied_style is None:
|
|
155
|
+
op.stop()
|
|
156
|
+
selected_datasets = op.selected_datasets(at_least=1)
|
|
157
|
+
|
|
158
|
+
with op.host.undo_macro(f"スタイルの貼り付け ({len(selected_datasets)}件)", enabled=len(selected_datasets) > 1):
|
|
159
|
+
for dataset in selected_datasets:
|
|
160
|
+
old_values = {attr: getattr(dataset, attr) for attr in STYLE_ATTRS}
|
|
161
|
+
op.host.push_property_change(dataset, old_values, dict(copied_style), description="スタイルの貼り付け")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def reload_from_source(op):
|
|
165
|
+
"""
|
|
166
|
+
元のファイルを読み直して df だけを差し替える。書式・注釈・使う列の選択はそのまま。
|
|
167
|
+
取り込みウィザードでの調整(文字コード・区切り等)は保存していないので、自動判定で読み直す。
|
|
168
|
+
使っている列が無くなっていたら何も変えずに止める。除外した行は古いデータの行番号なので消す。
|
|
169
|
+
"""
|
|
170
|
+
dataset = op.current_dataset()
|
|
171
|
+
if not dataset.source_file:
|
|
172
|
+
op.stop()
|
|
173
|
+
|
|
174
|
+
if not os.path.exists(dataset.source_file):
|
|
175
|
+
op.stop_with_warning(f"元ファイルが見つかりません:\n{dataset.source_file}")
|
|
176
|
+
|
|
177
|
+
from graphica.gui.workers import read_data_file, excel_engine_for
|
|
178
|
+
try:
|
|
179
|
+
if dataset.source_sheet:
|
|
180
|
+
new_df = pd.read_excel(dataset.source_file, sheet_name=dataset.source_sheet,
|
|
181
|
+
engine=excel_engine_for(dataset.source_file))
|
|
182
|
+
else:
|
|
183
|
+
new_df = read_data_file(dataset.source_file)
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.exception("元ファイルからの再読み込みに失敗しました")
|
|
186
|
+
op.stop_with_warning(f"ファイルの読み込みに失敗しました:\n{e}")
|
|
187
|
+
|
|
188
|
+
required_columns = (
|
|
189
|
+
dataset.x_col_name, dataset.y_col_name,
|
|
190
|
+
dataset.x_err_col_name, dataset.y_err_col_name, dataset.point_label_col_name,
|
|
191
|
+
)
|
|
192
|
+
missing = [col for col in required_columns if col and col not in new_df.columns]
|
|
193
|
+
if missing:
|
|
194
|
+
op.stop_with_warning(
|
|
195
|
+
"再読み込みしたファイルに、現在使用中の列が見つかりませんでした:\n"
|
|
196
|
+
+ "\n".join(missing)
|
|
197
|
+
+ "\n\nファイルの構造(列名/区切り文字/ヘッダー行など)が変わった可能性があります。"
|
|
198
|
+
"「データセット追加」から改めてインポートし直すことをお勧めします。"
|
|
199
|
+
"再読み込みは中止しました。"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
old_values = {'df': dataset.df, 'masked_row_indices': list(dataset.masked_row_indices)}
|
|
203
|
+
new_values = {'df': new_df, 'masked_row_indices': []}
|
|
204
|
+
# DataFrame を含む辞書は == で比べられないので、変化の有無は確かめずに積む。
|
|
205
|
+
op.host.push_property_change(dataset, old_values, new_values,
|
|
206
|
+
f"「{dataset.name}」を再読み込み", skip_if_unchanged=False)
|
|
207
|
+
op.host.show_status(f"「{dataset.name}」を元ファイルから再読み込みしました")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def copy_methods_text(op):
|
|
211
|
+
"""今のデータセットの処理履歴から「方法」節向けの説明文を作り、クリップボードに写す。"""
|
|
212
|
+
dataset = op.current_dataset()
|
|
213
|
+
if dataset.provenance is None:
|
|
214
|
+
op.stop()
|
|
215
|
+
QApplication.clipboard().setText(generate_methods_text(dataset, op.host.project))
|
|
216
|
+
op.host.show_status("「方法」文をクリップボードにコピーしました")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
TRANSFER_OPERATIONS = {
|
|
220
|
+
"export_data": Operation("データ表を書き出す", export_data),
|
|
221
|
+
"copy_to_tab": Operation("タブ間のデータセット転送", lambda op: copy_or_move_to_tab(op, move=False)),
|
|
222
|
+
"move_to_tab": Operation("タブ間のデータセット転送", lambda op: copy_or_move_to_tab(op, move=True)),
|
|
223
|
+
"copy_style": Operation("スタイルのコピー", copy_style),
|
|
224
|
+
"paste_style": Operation("スタイルの貼り付け", paste_style),
|
|
225
|
+
"reload_from_source": Operation("再読み込み", reload_from_source),
|
|
226
|
+
"copy_methods_text": Operation("「方法」文のコピー", copy_methods_text),
|
|
227
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""データセットの並び。描画の順(project.datasets)・一覧のツリー・保存用のフォルダ構造(dataset_group_tree)の 3 つと、
|
|
2
|
+
それらを合わせる規則をここに集める。
|
|
3
|
+
|
|
4
|
+
今の規則(変えるときはここだけ):
|
|
5
|
+
- 追加はツリーでは指定したフォルダの末尾。描画順は、最上位なら末尾、フォルダの中ならツリーで直前にあるデータセットの
|
|
6
|
+
すぐ後(一覧と描画の重なりをそろえる。K-18)。保存済みの並びは変えないので、開いたプロジェクトの見た目は変わらない。
|
|
7
|
+
- ドラッグの並べ替えのあとでだけ、描画順をツリーの順(深さ優先)に合わせる。同じフォルダの中の並べ替えだけ Undo できる。
|
|
8
|
+
- 保存用のフォルダ構造は保存・比較の直前にツリーから作り、読み込みではそこからツリーを作り直す。
|
|
9
|
+
- 削除の Undo は、描画順の全体と各項目の親・位置を控えて戻す。
|
|
10
|
+
"""
|
|
11
|
+
from PySide6.QtCore import Qt
|
|
12
|
+
|
|
13
|
+
_DATASET_ROLE = Qt.ItemDataRole.UserRole
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def item_dataset(item):
|
|
17
|
+
"""ツリーの項目のデータセット。フォルダなら None。"""
|
|
18
|
+
return item.data(0, _DATASET_ROLE)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DatasetOrder:
|
|
22
|
+
|
|
23
|
+
def __init__(self, project, tree, make_dataset_item):
|
|
24
|
+
# ツリーは組み立ての途中で差し替わるので、使うたびに取り出す
|
|
25
|
+
self._project = project
|
|
26
|
+
self._tree = tree
|
|
27
|
+
self._make_dataset_item = make_dataset_item
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def tree(self):
|
|
31
|
+
return self._tree()
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def datasets(self):
|
|
35
|
+
return self._project.datasets
|
|
36
|
+
|
|
37
|
+
# --- 探す ---
|
|
38
|
+
|
|
39
|
+
def find_row(self, dataset):
|
|
40
|
+
"""描画順の位置。Dataset の == は DataFrame を比べて例外になるので、同一性(is)で探す。"""
|
|
41
|
+
for i, ds in enumerate(self._project.datasets):
|
|
42
|
+
if ds is dataset:
|
|
43
|
+
return i
|
|
44
|
+
return -1
|
|
45
|
+
|
|
46
|
+
def dataset_items(self, parent_item=None):
|
|
47
|
+
"""データセットの葉を表示順(深さ優先)に返す。"""
|
|
48
|
+
items = []
|
|
49
|
+
source = self.tree.invisibleRootItem() if parent_item is None else parent_item
|
|
50
|
+
for i in range(source.childCount()):
|
|
51
|
+
child = source.child(i)
|
|
52
|
+
if item_dataset(child) is not None:
|
|
53
|
+
items.append(child)
|
|
54
|
+
else:
|
|
55
|
+
items.extend(self.dataset_items(child))
|
|
56
|
+
return items
|
|
57
|
+
|
|
58
|
+
def item_for(self, dataset):
|
|
59
|
+
for item in self.dataset_items():
|
|
60
|
+
if item_dataset(item) is dataset:
|
|
61
|
+
return item
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
def current_dataset(self):
|
|
65
|
+
item = self.tree.currentItem()
|
|
66
|
+
if item is None:
|
|
67
|
+
return None
|
|
68
|
+
return item_dataset(item)
|
|
69
|
+
|
|
70
|
+
def selected_datasets(self):
|
|
71
|
+
return [ds for item in self.tree.selectedItems() if (ds := item_dataset(item)) is not None]
|
|
72
|
+
|
|
73
|
+
def target_folder(self):
|
|
74
|
+
"""選択中の項目がフォルダならそれ(新しいデータセットをその中に入れる)。"""
|
|
75
|
+
current_item = self.tree.currentItem()
|
|
76
|
+
if current_item is not None and item_dataset(current_item) is None:
|
|
77
|
+
return current_item
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def top_level_items(items):
|
|
82
|
+
"""ほかの選択項目の子孫でないものだけ(フォルダと中身が両方選ばれていても二重に扱わないため)。"""
|
|
83
|
+
item_ids = {id(it) for it in items}
|
|
84
|
+
result = []
|
|
85
|
+
for item in items:
|
|
86
|
+
ancestor = item.parent()
|
|
87
|
+
while ancestor is not None and id(ancestor) not in item_ids:
|
|
88
|
+
ancestor = ancestor.parent()
|
|
89
|
+
if ancestor is None:
|
|
90
|
+
result.append(item)
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
# --- 足す ---
|
|
94
|
+
|
|
95
|
+
def append(self, dataset, parent_folder=None):
|
|
96
|
+
"""ツリーのフォルダの末尾と、それに合う描画順の位置に足す。"""
|
|
97
|
+
item = self._make_dataset_item(dataset, parent_folder)
|
|
98
|
+
position = len(self._project.datasets) if parent_folder is None else self._draw_position_for(item)
|
|
99
|
+
self._project.datasets.insert(position, dataset)
|
|
100
|
+
return item
|
|
101
|
+
|
|
102
|
+
def _draw_position_for(self, item):
|
|
103
|
+
"""ツリーで直前にあるデータセットの描画順のすぐ後。前に無ければ直後のものの前、どちらも無ければ末尾。"""
|
|
104
|
+
leaves = self.dataset_items()
|
|
105
|
+
index = next(i for i, leaf in enumerate(leaves) if leaf is item)
|
|
106
|
+
for leaf in reversed(leaves[:index]):
|
|
107
|
+
row = self.find_row(item_dataset(leaf))
|
|
108
|
+
if row != -1:
|
|
109
|
+
return row + 1
|
|
110
|
+
for leaf in leaves[index + 1:]:
|
|
111
|
+
row = self.find_row(item_dataset(leaf))
|
|
112
|
+
if row != -1:
|
|
113
|
+
return row
|
|
114
|
+
return len(self._project.datasets)
|
|
115
|
+
|
|
116
|
+
def add_folder(self, name, parent_item=None):
|
|
117
|
+
from PySide6.QtWidgets import QTreeWidgetItem
|
|
118
|
+
|
|
119
|
+
item = QTreeWidgetItem([name])
|
|
120
|
+
item.setData(0, _DATASET_ROLE, None)
|
|
121
|
+
if parent_item is not None:
|
|
122
|
+
parent_item.addChild(item)
|
|
123
|
+
else:
|
|
124
|
+
self.tree.addTopLevelItem(item)
|
|
125
|
+
item.setExpanded(True)
|
|
126
|
+
return item
|
|
127
|
+
|
|
128
|
+
def detach_item(self, item):
|
|
129
|
+
"""ツリーから外す(親の子か最上位かで外し方が違う)。"""
|
|
130
|
+
parent = item.parent()
|
|
131
|
+
if parent is not None:
|
|
132
|
+
parent.removeChild(item)
|
|
133
|
+
else:
|
|
134
|
+
index = self.tree.indexOfTopLevelItem(item)
|
|
135
|
+
if index != -1:
|
|
136
|
+
self.tree.takeTopLevelItem(index)
|
|
137
|
+
|
|
138
|
+
def remove_added(self, dataset):
|
|
139
|
+
"""append() の取り消し。"""
|
|
140
|
+
row = self.find_row(dataset)
|
|
141
|
+
if row != -1:
|
|
142
|
+
del self._project.datasets[row]
|
|
143
|
+
item = self.item_for(dataset)
|
|
144
|
+
if item is not None:
|
|
145
|
+
self.detach_item(item)
|
|
146
|
+
|
|
147
|
+
# --- 消す ---
|
|
148
|
+
|
|
149
|
+
def removal(self, top_level_items):
|
|
150
|
+
"""項目(データセットかフォルダ)を消す手順と戻す手順。消すものが無ければ None。
|
|
151
|
+
|
|
152
|
+
戻すために、描画順の全体と各項目の親・位置を控える。外した項目はこの手順が持つので、同じものを差し戻せる。
|
|
153
|
+
戻り値は (消す手順, 戻す手順, 消えるデータセット)。
|
|
154
|
+
"""
|
|
155
|
+
tree = self.tree
|
|
156
|
+
snapshots = [] # [(item, parent_item_or_None, index_in_parent), ...]
|
|
157
|
+
for item in top_level_items:
|
|
158
|
+
parent = item.parent()
|
|
159
|
+
index = parent.indexOfChild(item) if parent is not None else tree.indexOfTopLevelItem(item)
|
|
160
|
+
if index == -1:
|
|
161
|
+
continue
|
|
162
|
+
snapshots.append((item, parent, index))
|
|
163
|
+
if not snapshots:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
removed_datasets = []
|
|
167
|
+
for item, _parent, _index in snapshots:
|
|
168
|
+
dataset = item_dataset(item)
|
|
169
|
+
if dataset is not None:
|
|
170
|
+
removed_datasets.append(dataset)
|
|
171
|
+
else:
|
|
172
|
+
removed_datasets.extend(item_dataset(leaf) for leaf in self.dataset_items(item))
|
|
173
|
+
|
|
174
|
+
datasets_before = list(self._project.datasets)
|
|
175
|
+
|
|
176
|
+
def remove():
|
|
177
|
+
# 操作中に currentItemChanged が出ないように
|
|
178
|
+
tree.blockSignals(True)
|
|
179
|
+
try:
|
|
180
|
+
rows = sorted({row for ds in removed_datasets if (row := self.find_row(ds)) != -1}, reverse=True)
|
|
181
|
+
for row in rows:
|
|
182
|
+
del self._project.datasets[row]
|
|
183
|
+
# 同じ親の中でずれないよう、後ろから外す
|
|
184
|
+
for item, parent, _index in sorted(snapshots, key=lambda s: s[2], reverse=True):
|
|
185
|
+
if parent is not None:
|
|
186
|
+
parent.removeChild(item)
|
|
187
|
+
else:
|
|
188
|
+
index = tree.indexOfTopLevelItem(item)
|
|
189
|
+
if index != -1:
|
|
190
|
+
tree.takeTopLevelItem(index)
|
|
191
|
+
finally:
|
|
192
|
+
tree.blockSignals(False)
|
|
193
|
+
|
|
194
|
+
def restore():
|
|
195
|
+
tree.blockSignals(True)
|
|
196
|
+
try:
|
|
197
|
+
for item, parent, index in sorted(snapshots, key=lambda s: s[2]):
|
|
198
|
+
if parent is not None:
|
|
199
|
+
parent.insertChild(min(index, parent.childCount()), item)
|
|
200
|
+
else:
|
|
201
|
+
tree.insertTopLevelItem(min(index, tree.topLevelItemCount()), item)
|
|
202
|
+
# リストの中身だけ差し替える(描画順も戻る)
|
|
203
|
+
self._project.datasets[:] = datasets_before
|
|
204
|
+
finally:
|
|
205
|
+
tree.blockSignals(False)
|
|
206
|
+
|
|
207
|
+
return remove, restore, removed_datasets
|
|
208
|
+
|
|
209
|
+
# --- 描画順とツリーの順 ---
|
|
210
|
+
|
|
211
|
+
def tree_order(self):
|
|
212
|
+
"""ツリーの表示順(深さ優先)に並べたデータセット。"""
|
|
213
|
+
return [item_dataset(item) for item in self.dataset_items()]
|
|
214
|
+
|
|
215
|
+
def sort_tree_to_draw_order(self):
|
|
216
|
+
"""フォルダの中の並びを描画順に合わせる(並べ替えの Undo/Redo 用)。フォルダ自体は動かさない。"""
|
|
217
|
+
tree = self.tree
|
|
218
|
+
order_index = {id(ds): i for i, ds in enumerate(self._project.datasets)}
|
|
219
|
+
selected_ids = {id(item_dataset(item)) for item in tree.selectedItems()}
|
|
220
|
+
current_item = tree.currentItem()
|
|
221
|
+
current_dataset = item_dataset(current_item) if current_item else None
|
|
222
|
+
|
|
223
|
+
tree.blockSignals(True)
|
|
224
|
+
|
|
225
|
+
def sort_children(parent_item):
|
|
226
|
+
source = tree.invisibleRootItem() if parent_item is None else parent_item
|
|
227
|
+
children = [source.child(i) for i in range(source.childCount())]
|
|
228
|
+
|
|
229
|
+
dataset_positions = [i for i, c in enumerate(children) if item_dataset(c) is not None]
|
|
230
|
+
dataset_items_sorted = sorted(
|
|
231
|
+
(children[i] for i in dataset_positions),
|
|
232
|
+
key=lambda it: order_index.get(id(item_dataset(it)), 0)
|
|
233
|
+
)
|
|
234
|
+
new_children = list(children)
|
|
235
|
+
for pos, item in zip(dataset_positions, dataset_items_sorted):
|
|
236
|
+
new_children[pos] = item
|
|
237
|
+
|
|
238
|
+
for _ in range(source.childCount()):
|
|
239
|
+
source.takeChild(0)
|
|
240
|
+
for item in new_children:
|
|
241
|
+
source.addChild(item)
|
|
242
|
+
|
|
243
|
+
for item in new_children:
|
|
244
|
+
if item_dataset(item) is None:
|
|
245
|
+
sort_children(item)
|
|
246
|
+
|
|
247
|
+
sort_children(None)
|
|
248
|
+
|
|
249
|
+
for item in self.dataset_items():
|
|
250
|
+
ds = item_dataset(item)
|
|
251
|
+
if id(ds) in selected_ids:
|
|
252
|
+
item.setSelected(True)
|
|
253
|
+
if ds is current_dataset:
|
|
254
|
+
tree.setCurrentItem(item)
|
|
255
|
+
tree.blockSignals(False)
|
|
256
|
+
|
|
257
|
+
# --- 保存用のフォルダ構造 ---
|
|
258
|
+
|
|
259
|
+
def group_tree(self):
|
|
260
|
+
"""ツリーから保存用のフォルダ構造を作る。"""
|
|
261
|
+
def walk(parent_item):
|
|
262
|
+
children = []
|
|
263
|
+
source = self.tree.invisibleRootItem() if parent_item is None else parent_item
|
|
264
|
+
for i in range(source.childCount()):
|
|
265
|
+
child = source.child(i)
|
|
266
|
+
dataset = item_dataset(child)
|
|
267
|
+
if dataset is not None:
|
|
268
|
+
children.append({'dataset': dataset})
|
|
269
|
+
else:
|
|
270
|
+
children.append({'name': child.text(0), 'children': walk(child)})
|
|
271
|
+
return children
|
|
272
|
+
return {'name': '', 'children': walk(None)}
|
|
273
|
+
|
|
274
|
+
def rebuild_tree(self):
|
|
275
|
+
"""保存用のフォルダ構造からツリーを作り直す(読み込みのあと)。"""
|
|
276
|
+
tree = self.tree
|
|
277
|
+
tree.clear()
|
|
278
|
+
|
|
279
|
+
def build(node, parent_item):
|
|
280
|
+
for child_node in node.get('children', []):
|
|
281
|
+
if 'dataset' in child_node:
|
|
282
|
+
self._make_dataset_item(child_node['dataset'], parent_item)
|
|
283
|
+
else:
|
|
284
|
+
folder_item = self.add_folder(child_node.get('name', 'フォルダ'), parent_item)
|
|
285
|
+
build(child_node, folder_item)
|
|
286
|
+
|
|
287
|
+
build(self._project.dataset_group_tree, None)
|
|
288
|
+
|
|
289
|
+
# --- 検索 ---
|
|
290
|
+
|
|
291
|
+
def filter_by_name(self, text):
|
|
292
|
+
"""名前に検索文字列を含むデータセットだけを出す。フォルダは中に1つでも当たりがあれば出す。"""
|
|
293
|
+
query = text.strip().lower()
|
|
294
|
+
|
|
295
|
+
def apply_filter(item):
|
|
296
|
+
dataset = item_dataset(item)
|
|
297
|
+
if dataset is not None:
|
|
298
|
+
visible = (not query) or (query in dataset.name.lower())
|
|
299
|
+
item.setHidden(not visible)
|
|
300
|
+
return visible
|
|
301
|
+
any_child_visible = False
|
|
302
|
+
for i in range(item.childCount()):
|
|
303
|
+
if apply_filter(item.child(i)):
|
|
304
|
+
any_child_visible = True
|
|
305
|
+
item.setHidden(bool(query) and not any_child_visible)
|
|
306
|
+
return any_child_visible or not query
|
|
307
|
+
|
|
308
|
+
root = self.tree.invisibleRootItem()
|
|
309
|
+
for i in range(root.childCount()):
|
|
310
|
+
apply_filter(root.child(i))
|