Coverage for graphica/gui/plugin_context.py: 95.8%
102 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
1"""PluginContext の本体側の実装。1つのタブ(PlotterApp)と1つのプラグインの組ごとに作る。"""
2import logging
3import weakref
4from contextlib import contextmanager
5from dataclasses import fields
8from graphica.gui import notify
9from graphica.core.app_paths import get_plugin_data_dir
10from graphica.core.color_palettes import normalize_palettes
11from graphica.core.commands import SetDatasetPropertiesCommand
12from graphica.core.dataset import Dataset
13from graphica.core.named_colors import add_named_color, load_named_colors, save_named_colors
14from graphica.core.plugin_context import PluginContext
16logger = logging.getLogger(__name__)
18_DATASET_FIELDS = frozenset(f.name for f in fields(Dataset))
21class TabPluginContext(PluginContext):
23 def __init__(self, app, plugin_name):
24 # タブを閉じたあとも窓口をプラグインが握っていることがあるので、タブは弱参照で持つ。
25 self._app_ref = weakref.ref(app)
26 self._plugin_name = plugin_name
27 self._datasets_changed_callbacks = []
28 self._selection_changed_callbacks = []
29 # undo_group の中なら {"description", "started"}。マクロは最初の変更で始める(空のマクロも Undo 1回分になる)
30 self._undo_group = None
32 @property
33 def _app(self):
34 app = self._app_ref()
35 if app is None: 35 ↛ 36line 35 didn't jump to line 36 because the condition on line 35 was never true
36 raise RuntimeError("このタブはすでに閉じられています。")
37 return app
39 def datasets(self):
40 return list(self._app.project.datasets)
42 def current_dataset(self):
43 return self._app.current_dataset()
45 def selected_datasets(self):
46 return self._app.selected_datasets()
48 def add_dataset(self, dataset, description=None):
49 self._start_pending_undo_group()
50 self._app.add_dataset_with_undo(
51 dataset, description=description or f"[{self._plugin_name}] データセットの追加"
52 )
54 def set_dataset_properties(self, dataset, values, description=None):
55 unknown = sorted(set(values) - _DATASET_FIELDS)
56 if unknown:
57 raise AttributeError(f"Dataset に無い属性です: {', '.join(unknown)}")
58 app = self._app
59 new_values = dict(values)
60 old_values = {key: getattr(dataset, key) for key in new_values}
61 self._start_pending_undo_group()
62 app.undo_stack.push(SetDatasetPropertiesCommand(
63 dataset, old_values, new_values,
64 on_applied=lambda: app.refresh_after_dataset_property_change(
65 dataset, changed_keys=new_values.keys(), old_values=old_values, new_values=new_values
66 ),
67 description=description or f"[{self._plugin_name}] プロパティの変更",
68 ))
70 @contextmanager
71 def undo_group(self, description):
72 outermost = self._undo_group is None
73 if outermost:
74 self._undo_group = {"description": description, "started": False}
75 try:
76 yield
77 finally:
78 if outermost:
79 group, self._undo_group = self._undo_group, None
80 if group["started"]:
81 self._app.end_batched_undo_group()
83 def _start_pending_undo_group(self):
84 group = self._undo_group
85 if group is not None and not group["started"]:
86 self._app.begin_batched_undo_group(group["description"])
87 group["started"] = True
89 def redraw(self):
90 self._app.redraw()
92 def on_datasets_changed(self, callback):
93 self._datasets_changed_callbacks.append(callback)
95 def on_selection_changed(self, callback):
96 self._selection_changed_callbacks.append(callback)
98 def _notify_datasets_changed(self):
99 for callback in list(self._datasets_changed_callbacks):
100 self._call_isolated(callback)
102 def _notify_selection_changed(self, dataset):
103 for callback in list(self._selection_changed_callbacks):
104 self._call_isolated(callback, dataset)
106 def _call_isolated(self, callback, *args):
107 # 通知先のプラグインが失敗しても、描画や選択の処理を止めない。
108 try:
109 callback(*args)
110 except Exception:
111 logger.exception("[plugin:%s] 通知の処理に失敗しました", self._plugin_name)
113 @property
114 def parent_widget(self):
115 return self._app_ref()
117 def show_message(self, text, title=None):
118 notify.information(self.parent_widget, title or self._plugin_name, text)
120 def show_error(self, text, title=None):
121 notify.warning(self.parent_widget, title or self._plugin_name, text)
123 @property
124 def data_dir(self):
125 return get_plugin_data_dir(self._plugin_name)
127 def named_colors(self):
128 return load_named_colors(self._app.settings)
130 def set_named_colors(self, entries):
131 validated = []
132 for entry in entries:
133 validated = add_named_color(validated, entry.get("name"), entry.get("color"))
134 save_named_colors(self._app.settings, validated)
136 def color_palettes(self):
137 return {name: list(colors) for name, colors in self._app.colors.load_palettes().items()}
139 def set_color_palettes(self, palettes):
140 self._app.colors.save_palettes(normalize_palettes(palettes))
142 def active_color_cycle(self):
143 return list(self._app.colors.active_color_cycle())
145 def set_active_color_palette(self, name):
146 self._app.colors.set_active_palette(name)