Coverage for graphica/core/commands.py: 100.0%

156 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-12 16:38 +0000

1import logging 

2from PySide6.QtGui import QUndoCommand 

3from typing import TYPE_CHECKING, Any, Callable 

4if TYPE_CHECKING: 

5 from graphica.core.dataset import Dataset 

6 from graphica.models.project import ProjectModel 

7 

8logger = logging.getLogger(__name__) 

9 

10 

11# コマンドは Dataset の公開メソッドだけを呼び、GUI を知らない。表の描き直しと通知は 

12# DataEditorDialog が QUndoStack.indexChanged で行う。 

13 

14class EditCellCommand(QUndoCommand): 

15 def __init__(self, dataset: "Dataset", row_idx: Any, col_name: str, old_value: Any, new_value: Any, 

16 description: str = "セル編集") -> None: 

17 """row_idx は dataset.df の index ラベル。""" 

18 super().__init__(description) 

19 self.dataset = dataset 

20 self.row_idx = row_idx 

21 self.col_name = col_name 

22 self.old_value = old_value 

23 self.new_value = new_value 

24 

25 def redo(self) -> None: 

26 self.dataset.set_cell(self.row_idx, self.col_name, self.new_value) 

27 

28 def undo(self) -> None: 

29 self.dataset.set_cell(self.row_idx, self.col_name, self.old_value) 

30 

31 

32class AddRowCommand(QUndoCommand): 

33 def __init__(self, dataset: "Dataset", description: str = "行追加") -> None: 

34 super().__init__(description) 

35 self.dataset = dataset 

36 

37 def redo(self) -> None: 

38 self.dataset.add_row() 

39 

40 def undo(self) -> None: 

41 self.dataset.delete_last_row() 

42 

43 

44class DeleteRowsCommand(QUndoCommand): 

45 def __init__(self, dataset: "Dataset", row_indices: list[Any], deleted_data: Any, description: str = "行削除") -> None: 

46 """row_indices は df の index ラベル、deleted_data は Undo 用の消した行。""" 

47 super().__init__(description) 

48 self.dataset = dataset 

49 self.row_indices = row_indices 

50 self.deleted_data = deleted_data 

51 

52 def redo(self) -> None: 

53 self.dataset.delete_rows(self.row_indices) 

54 

55 def undo(self) -> None: 

56 self.dataset.restore_rows(self.deleted_data) 

57 

58 

59class AddColumnCommand(QUndoCommand): 

60 def __init__(self, dataset: "Dataset", col_name: str, description: str = "列追加") -> None: 

61 super().__init__(description) 

62 self.dataset = dataset 

63 self.col_name = col_name 

64 

65 def redo(self) -> None: 

66 self.dataset.add_column(self.col_name) 

67 

68 def undo(self) -> None: 

69 # 足した後にその列を X/Y にされていたら、消すと描画が壊れるので取り消さない 

70 if self.dataset.is_column_in_use(self.col_name): 

71 logger.warning("Undo不可: 列 '%s' はプロットに使用されています。", self.col_name) 

72 self.setObsolete(True) 

73 return 

74 self.dataset.remove_column(self.col_name) 

75 

76 

77class DeleteColumnCommand(QUndoCommand): 

78 def __init__(self, dataset: "Dataset", col_name: str, deleted_column_data: Any, description: str = "列削除") -> None: 

79 super().__init__(description) 

80 self.dataset = dataset 

81 self.col_name = col_name 

82 self.deleted_column_data = deleted_column_data 

83 

84 def redo(self) -> None: 

85 # 取り消し → 列を X/Y に設定 → やり直し、の順でも壊れないよう、ここでも確かめる 

86 if self.dataset.is_column_in_use(self.col_name): 

87 logger.warning("Redo不可: 列 '%s' はプロットに使用されています。", self.col_name) 

88 self.setObsolete(True) 

89 return 

90 self.dataset.remove_column(self.col_name) 

91 

92 def undo(self) -> None: 

93 self.dataset.restore_column(self.col_name, self.deleted_column_data) 

94 

95 

96class RenameColumnCommand(QUndoCommand): 

97 """X/Y などの参照は Dataset.rename_column が新しい名前に付け替える。""" 

98 def __init__(self, dataset: "Dataset", old_name: str, new_name: str, description: str = "列名の変更") -> None: 

99 super().__init__(description) 

100 self.dataset = dataset 

101 self.old_name = old_name 

102 self.new_name = new_name 

103 

104 def redo(self) -> None: 

105 self.dataset.rename_column(self.old_name, self.new_name) 

106 

107 def undo(self) -> None: 

108 self.dataset.rename_column(self.new_name, self.old_name) 

109 

110 

111class SetDatasetPropertiesCommand(QUndoCommand): 

112 """Dataset の属性(色、線種、名前、描画先など)をまとめて変える。""" 

113 def __init__(self, dataset: "Dataset", old_values: dict[str, Any], new_values: dict[str, Any], 

114 on_applied: Callable[[], Any], description: str = "プロパティ変更") -> None: 

115 """old_values / new_values は {属性名: 値}。on_applied は適用後の再描画など(GUI 側の仕事)。""" 

116 super().__init__(description) 

117 self.dataset = dataset 

118 self.old_values = old_values 

119 self.new_values = new_values 

120 self.on_applied = on_applied 

121 

122 def redo(self) -> None: 

123 for attr, value in self.new_values.items(): 

124 setattr(self.dataset, attr, value) 

125 self.on_applied() 

126 

127 def undo(self) -> None: 

128 for attr, value in self.old_values.items(): 

129 setattr(self.dataset, attr, value) 

130 self.on_applied() 

131 

132 

133class SetMaskedRowsCommand(QUndoCommand): 

134 """行のマスクをまとめて切り替える。""" 

135 def __init__(self, dataset: "Dataset", old_masked_indices: list[Any], new_masked_indices: list[Any], 

136 description: str = "行の除外/解除", on_applied: Callable[[], Any] | None = None) -> None: 

137 """マスクする行は df の index ラベルのリスト。on_applied はタブの Undo で積むときの描き直し 

138 (データエディタは自分の Undo の通知で描き直すので要らない)。""" 

139 super().__init__(description) 

140 self.dataset = dataset 

141 self.old_masked_indices = list(old_masked_indices) 

142 self.new_masked_indices = list(new_masked_indices) 

143 self.on_applied = on_applied 

144 

145 def redo(self) -> None: 

146 self.dataset.masked_row_indices = list(self.new_masked_indices) 

147 if self.on_applied is not None: 

148 self.on_applied() 

149 

150 def undo(self) -> None: 

151 self.dataset.masked_row_indices = list(self.old_masked_indices) 

152 if self.on_applied is not None: 

153 self.on_applied() 

154 

155 

156class SetAnnotationsCommand(QUndoCommand): 

157 """1つの軸の注釈のリストを丸ごと置き換える(追加も削除もこれ)。""" 

158 def __init__(self, project: "ProjectModel", axis_index: int, old_annotations: list[dict[str, Any]], 

159 new_annotations: list[dict[str, Any]], on_applied: Callable[[], Any], 

160 description: str = "注釈の変更") -> None: 

161 super().__init__(description) 

162 self.project = project 

163 self.axis_index = axis_index 

164 self.old_annotations = list(old_annotations) 

165 self.new_annotations = list(new_annotations) 

166 self.on_applied = on_applied 

167 

168 def redo(self) -> None: 

169 self.project.all_plot_settings[self.axis_index]['annotations'] = list(self.new_annotations) 

170 self.on_applied() 

171 

172 def undo(self) -> None: 

173 self.project.all_plot_settings[self.axis_index]['annotations'] = list(self.old_annotations) 

174 self.on_applied() 

175 

176 

177class AddDatasetCommand(QUndoCommand): 

178 """データセットを1つ追加する(プラグインの処理と解析の結果用)。 

179 

180 追加にはツリーの項目(フォルダの中の位置)が伴うので、追加と削除の処理そのものを GUI から受け取る。 

181 """ 

182 def __init__(self, add_callback: Callable[[], Any], remove_callback: Callable[[], Any], 

183 description: str = "データセットの追加") -> None: 

184 """remove_callback は、直前に add_callback が足したものを取り除く。""" 

185 super().__init__(description) 

186 self.add_callback = add_callback 

187 self.remove_callback = remove_callback 

188 

189 def redo(self) -> None: 

190 self.add_callback() 

191 

192 def undo(self) -> None: 

193 self.remove_callback() 

194 

195 

196class DeferRedrawCommand(QUndoCommand): 

197 """マクロの最初(opening=True)と最後に置き、間の描き直しを最後の1回にまとめる。 

198 

199 Qt はマクロを Undo するとき子を逆順に流すので、Undo では最後のものが遅らせ始め、最初のものが描く。 

200 """ 

201 def __init__(self, begin_deferring: Callable[[], Any], end_deferring: Callable[[], Any], 

202 opening: bool) -> None: 

203 super().__init__("描き直しをまとめる") 

204 self.begin_deferring = begin_deferring 

205 self.end_deferring = end_deferring 

206 self.opening = opening 

207 

208 def redo(self) -> None: 

209 (self.begin_deferring if self.opening else self.end_deferring)() 

210 

211 def undo(self) -> None: 

212 (self.end_deferring if self.opening else self.begin_deferring)() 

213 

214 

215class RemoveDatasetCommand(QUndoCommand): 

216 """データセットやフォルダの削除。 

217 

218 元の親フォルダの元の位置へ戻す必要があるので、削除と復元の処理そのものを GUI から受け取る。 

219 """ 

220 def __init__(self, remove_callback: Callable[[], Any], restore_callback: Callable[[], Any], 

221 description: str = "データセットの削除") -> None: 

222 """restore_callback は、datasets の順とツリーの親・位置の両方を元に戻す。""" 

223 super().__init__(description) 

224 self.remove_callback = remove_callback 

225 self.restore_callback = restore_callback 

226 

227 def redo(self) -> None: 

228 self.remove_callback() 

229 

230 def undo(self) -> None: 

231 self.restore_callback() 

232 

233 

234class ReorderDatasetsCommand(QUndoCommand): 

235 """project.datasets の並び(描画の順と重なり)を変える。""" 

236 def __init__(self, project: "ProjectModel", old_order: "list[Dataset]", new_order: "list[Dataset]", 

237 on_applied: Callable[[], Any], description: str = "データセットの並べ替え") -> None: 

238 """on_applied は一覧の同期と再描画。""" 

239 super().__init__(description) 

240 self.project = project 

241 self.old_order = list(old_order) 

242 self.new_order = list(new_order) 

243 self.on_applied = on_applied 

244 

245 def redo(self) -> None: 

246 self.project.datasets = list(self.new_order) 

247 self.on_applied() 

248 

249 def undo(self) -> None: 

250 self.project.datasets = list(self.old_order) 

251 self.on_applied()