new layer merging, convert layer to normal mode
[mypaint:mypaint.git] / lib / command.py
1 # This file is part of MyPaint.
2 # Copyright (C) 2007-2008 by Martin Renold <martinxyz@gmx.ch>
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
8
9 import layer
10 import helpers
11 from gettext import gettext as _
12
13 class CommandStack:
14     def __init__(self):
15         self.call_before_action = []
16         self.stack_observers = []
17         self.clear()
18
19     def __repr__(self):
20         return "<CommandStack\n  <Undo len=%d last3=%r>\n" \
21                 "  <Redo len=%d last3=%r> >" % (
22                     len(self.undo_stack), self.undo_stack[-3:],
23                     len(self.redo_stack), self.redo_stack[:3],  )
24
25     def clear(self):
26         self.undo_stack = []
27         self.redo_stack = []
28         self.notify_stack_observers()
29
30     def do(self, command):
31         for f in self.call_before_action: f()
32         self.redo_stack = [] # discard
33         command.redo()
34         self.undo_stack.append(command)
35         self.reduce_undo_history()
36         self.notify_stack_observers()
37
38     def undo(self):
39         if not self.undo_stack: return
40         for f in self.call_before_action: f()
41         command = self.undo_stack.pop()
42         command.undo()
43         self.redo_stack.append(command)
44         self.notify_stack_observers()
45         return command
46
47     def redo(self):
48         if not self.redo_stack: return
49         for f in self.call_before_action: f()
50         command = self.redo_stack.pop()
51         command.redo()
52         self.undo_stack.append(command)
53         self.notify_stack_observers()
54         return command
55
56     def reduce_undo_history(self):
57         stack = self.undo_stack
58         self.undo_stack = []
59         steps = 0
60         for item in reversed(stack):
61             self.undo_stack.insert(0, item)
62             if not item.automatic_undo:
63                 steps += 1
64             if steps == 30: # and memory > ...
65                 break
66
67     def get_last_command(self):
68         if not self.undo_stack: return None
69         return self.undo_stack[-1]
70
71     def update_last_command(self, **kwargs):
72         cmd = self.get_last_command()
73         if cmd is None:
74             return None
75         cmd.update(**kwargs)
76         self.notify_stack_observers() # the display_name may have changed
77         return cmd
78
79     def notify_stack_observers(self):
80         for func in self.stack_observers:
81             func(self)
82
83 class Action:
84     """An undoable, redoable action.
85
86     Base class for all undo/redoable actions. Subclasses must implement the
87     undo and redo methods. They should have a reference to the document in 
88     self.doc.
89
90     """
91     automatic_undo = False
92     display_name = _("Unknown Action")
93
94     def __repr__(self):
95         return "<%s>" % (self.display_name,)
96
97
98     def redo(self):
99         """Callback used to perform, or re-perform the Action.
100         """
101         raise NotImplementedError
102
103
104     def undo(self):
105         """Callback used to un-perform an already performed Action.
106         """
107         raise NotImplementedError
108
109
110     def update(self, **kwargs):
111         """In-place update on the tip of the undo stack.
112
113         This method should update the model in the way specified in `**kwargs`.
114         The interpretation of arguments is left to the concrete implementation.
115
116         Updating the top Action on the command stack is used to prevent
117         situations where an undo() followed by a redo() would result in
118         multiple sendings of GdkEvents by code designed to keep interface state
119         in sync with the model.
120
121         """
122
123         # Updating is used in situations where only the user's final choice of
124         # a state such as layer visibility matters in the command-stream.
125         # Creating a nice workflow for the user by using `undo()` then `do()`
126         # with a replacement Action can sometimes cause GtkAction and
127         # command.Action flurries or loops across multiple GdkEvent callbacks.
128         #
129         # This can make coding difficult elsewhere. For example,
130         # GtkToggleActions must be kept in in sync with undoable boolean model
131         # state, but even when an interlock or check is coded, the fact that
132         # processing happens in multiple GtkEvent handlers can result in,
133         # essentially, a toggle action which turns itself off immediately after
134         # being toggled on. See https://gna.org/bugs/?20096 for a concrete
135         # example.
136
137         raise NotImplementedError
138
139
140     # Utility functions
141     def _notify_canvas_observers(self, affected_layers):
142         bbox = helpers.Rect()
143         for layer in affected_layers:
144             layer_bbox = layer.get_bbox()
145             bbox.expandToIncludeRect(layer_bbox)
146         for func in self.doc.canvas_observers:
147             func(*bbox)
148
149     def _notify_document_observers(self):
150         self.doc.call_doc_observers()
151
152 class Stroke(Action):
153     display_name = _("Painting")
154     def __init__(self, doc, stroke, snapshot_before):
155         """called only when the stroke was just completed and is now fully rendered"""
156         self.doc = doc
157         assert stroke.finished
158         self.stroke = stroke # immutable; not used for drawing any more, just for inspection
159         self.before = snapshot_before
160         self.doc.layer.add_stroke(stroke, snapshot_before)
161         # this snapshot will include the updated stroke list (modified by the line above)
162         self.after = self.doc.layer.save_snapshot()
163     def undo(self):
164         self.doc.layer.load_snapshot(self.before)
165     def redo(self):
166         self.doc.layer.load_snapshot(self.after)
167
168 class ClearLayer(Action):
169     display_name = _("Clear Layer")
170     def __init__(self, doc):
171         self.doc = doc
172     def redo(self):
173         self.before = self.doc.layer.save_snapshot()
174         self.doc.layer.clear()
175         self._notify_document_observers()
176     def undo(self):
177         self.doc.layer.load_snapshot(self.before)
178         del self.before
179         self._notify_document_observers()
180
181 class LoadLayer(Action):
182     display_name = _("Load Layer")
183     def __init__(self, doc, tiledsurface):
184         self.doc = doc
185         self.tiledsurface = tiledsurface
186     def redo(self):
187         layer = self.doc.layer
188         self.before = layer.save_snapshot()
189         layer.load_from_surface(self.tiledsurface)
190     def undo(self):
191         self.doc.layer.load_snapshot(self.before)
192         del self.before
193
194 class MergeLayer(Action):
195     """merge the current layer into dst"""
196     display_name = _("Merge Layers")
197     def __init__(self, doc, dst_idx):
198         self.doc = doc
199         self.dst_layer = self.doc.layers[dst_idx]
200         self.normalize_src = ConvertLayerToNormalMode(doc, doc.layer)
201         self.normalize_dst = ConvertLayerToNormalMode(doc, self.dst_layer)
202         self.remove_src = RemoveLayer(doc)
203     def redo(self):
204         self.normalize_src.redo()
205         self.normalize_dst.redo()
206         self.dst_before = self.dst_layer.save_snapshot()
207         assert self.doc.layer is not self.dst_layer
208         self.doc.layer.merge_into(self.dst_layer)
209         self.remove_src.redo()
210         self.select_dst = SelectLayer(self.doc, self.doc.layers.index(self.dst_layer))
211         self.select_dst.redo()
212         self._notify_document_observers()
213     def undo(self):
214         self.select_dst.undo()
215         del self.select_dst
216         self.remove_src.undo()
217         self.dst_layer.load_snapshot(self.dst_before)
218         del self.dst_before
219         self.normalize_dst.undo()
220         self.normalize_src.undo()
221         self._notify_document_observers()
222
223 class ConvertLayerToNormalMode(Action):
224     display_name = _("Convert Layer Mode")
225     def __init__(self, doc, layer):
226         self.doc = doc
227         self.layer = layer
228         self.set_normal_mode = SetLayerCompositeOp(doc, 'svg:src-over', layer)
229         self.set_opacity = SetLayerOpacity(doc, 1.0, layer)
230     def redo(self):
231         self.before = self.doc.layer.save_snapshot()
232         prev_idx = self.doc.layer_idx
233         self.doc.layer_idx = self.doc.layers.index(self.layer)
234         get_bg = self.doc.get_rendered_image_behind_current_layer
235         self.layer.convert_to_normal_mode(get_bg)
236         self.doc.layer_idx = prev_idx
237         self.set_normal_mode.redo()
238         self.set_opacity.redo()
239     def undo(self):
240         self.set_opacity.undo()
241         self.set_normal_mode.undo()
242         self.doc.layer.load_snapshot(self.before)
243         del self.before
244
245 class AddLayer(Action):
246     display_name = _("Add Layer")
247     def __init__(self, doc, insert_idx=None, after=None, name=''):
248         self.doc = doc
249         self.insert_idx = insert_idx
250         if after:
251             l_idx = self.doc.layers.index(after)
252             self.insert_idx = l_idx + 1
253         self.layer = layer.Layer(name)
254         self.layer.content_observers.append(self.doc.layer_modified_cb)
255         self.layer.set_symmetry_axis(self.doc.get_symmetry_axis())
256     def redo(self):
257         self.doc.layers.insert(self.insert_idx, self.layer)
258         self.prev_idx = self.doc.layer_idx
259         self.doc.layer_idx = self.insert_idx
260         self._notify_document_observers()
261     def undo(self):
262         self.doc.layers.remove(self.layer)
263         self.doc.layer_idx = self.prev_idx
264         self._notify_document_observers()
265
266 class RemoveLayer(Action):
267     """Removes a layer, replacing it with a new one if it was the last.
268     """
269     display_name = _("Remove Layer")
270     def __init__(self, doc,layer=None):
271         self.doc = doc
272         self.layer = layer
273         self.newlayer0 = None
274     def redo(self):
275         if self.layer:
276             self.idx = self.doc.layers.index(self.layer)
277             self.doc.layers.remove(self.layer)
278         else:
279             self.idx = self.doc.layer_idx
280             self.layer = self.doc.layers.pop(self.doc.layer_idx)
281         if len(self.doc.layers) == 0:
282             if self.newlayer0 is None:
283                 ly = layer.Layer("")
284                 ly.content_observers.append(self.doc.layer_modified_cb)
285                 ly.set_symmetry_axis(self.doc.get_symmetry_axis())
286                 self.newlayer0 = ly
287             self.doc.layers.append(self.newlayer0)
288             self.doc.layer_idx = 0
289             assert self.idx == 0
290         else:
291             if self.doc.layer_idx == len(self.doc.layers):
292                 self.doc.layer_idx -= 1
293         self._notify_canvas_observers([self.layer])
294         self._notify_document_observers()
295     def undo(self):
296         if self.newlayer0 is not None:
297             self.doc.layers.remove(self.newlayer0)
298         self.doc.layers.insert(self.idx, self.layer)
299         self.doc.layer_idx = self.idx
300         self._notify_canvas_observers([self.layer])
301         self._notify_document_observers()
302
303 class SelectLayer(Action):
304     display_name = _("Select Layer")
305     automatic_undo = True
306     def __init__(self, doc, idx):
307         self.doc = doc
308         self.idx = idx
309     def redo(self):
310         assert self.idx >= 0 and self.idx < len(self.doc.layers)
311         self.prev_idx = self.doc.layer_idx
312         self.doc.layer_idx = self.idx
313         self._notify_document_observers()
314     def undo(self):
315         self.doc.layer_idx = self.prev_idx
316         self._notify_document_observers()
317
318 class MoveLayer(Action):
319     display_name = _("Move Layer on Canvas")
320     # NOT "Move Layer" for now - old translatable string with different sense
321     def __init__(self, doc, layer_idx, dx, dy, ignore_first_redo=True):
322         self.doc = doc
323         self.layer_idx = layer_idx
324         self.dx = dx
325         self.dy = dy
326         self.ignore_first_redo = ignore_first_redo
327     def redo(self):
328         layer = self.doc.layers[self.layer_idx]
329         if self.ignore_first_redo:
330             # these are typically created interactively, after
331             # the entire layer has been moved
332             self.ignore_first_redo = False
333         else:
334             layer.translate(self.dx, self.dy)
335         self._notify_canvas_observers([layer])
336         self._notify_document_observers()
337     def undo(self):
338         layer = self.doc.layers[self.layer_idx]
339         layer.translate(-self.dx, -self.dy)
340         self._notify_canvas_observers([layer])
341         self._notify_document_observers()
342
343 class ReorderSingleLayer(Action):
344     display_name = _("Reorder Layer in Stack")
345     def __init__(self, doc, was_idx, new_idx, select_new=False):
346         self.doc = doc
347         self.was_idx = was_idx
348         self.new_idx = new_idx
349         self.select_new = select_new
350     def redo(self):
351         moved_layer = self.doc.layers[self.was_idx]
352         self.doc.layers.remove(moved_layer)
353         self.doc.layers.insert(self.new_idx, moved_layer)
354         if self.select_new:
355             self.was_selected = self.doc.layer_idx
356             self.doc.layer_idx = self.new_idx
357         self._notify_canvas_observers([moved_layer])
358         self._notify_document_observers()
359     def undo(self):
360         moved_layer = self.doc.layers[self.new_idx]
361         self.doc.layers.remove(moved_layer)
362         self.doc.layers.insert(self.was_idx, moved_layer)
363         if self.select_new:
364             self.doc.layer_idx = self.was_selected
365             self.was_selected = None
366         self._notify_canvas_observers([moved_layer])
367         self._notify_document_observers()
368
369 class DuplicateLayer(Action):
370     display_name = _("Duplicate Layer")
371     def __init__(self, doc, insert_idx=None, name=''):
372         self.doc = doc
373         self.insert_idx = insert_idx
374         snapshot = self.doc.layers[self.insert_idx].save_snapshot()
375         self.new_layer = layer.Layer(name)
376         self.new_layer.load_snapshot(snapshot)
377         self.new_layer.content_observers.append(self.doc.layer_modified_cb)
378         self.new_layer.set_symmetry_axis(doc.get_symmetry_axis())
379     def redo(self):
380         self.doc.layers.insert(self.insert_idx+1, self.new_layer)
381         self.duplicate_layer = self.doc.layers[self.insert_idx+1]
382         self._notify_canvas_observers([self.duplicate_layer])
383         self._notify_document_observers()
384     def undo(self):
385         self.doc.layers.remove(self.duplicate_layer)
386         original_layer = self.doc.layers[self.insert_idx]
387         self._notify_canvas_observers([original_layer])
388         self._notify_document_observers()
389
390 class ReorderLayers(Action):
391     display_name = _("Reorder Layer Stack")
392     def __init__(self, doc, new_order):
393         self.doc = doc
394         self.old_order = doc.layers[:]
395         self.selection = self.old_order[doc.layer_idx]
396         self.new_order = new_order
397         for layer in new_order:
398             assert layer in self.old_order
399         assert len(self.old_order) == len(new_order)
400     def redo(self):
401         self.doc.layers[:] = self.new_order
402         self.doc.layer_idx = self.doc.layers.index(self.selection)
403         self._notify_canvas_observers(self.doc.layers)
404         self._notify_document_observers()
405     def undo(self):
406         self.doc.layers[:] = self.old_order
407         self.doc.layer_idx = self.doc.layers.index(self.selection)
408         self._notify_canvas_observers(self.doc.layers)
409         self._notify_document_observers()
410
411 class RenameLayer(Action):
412     display_name = _("Rename Layer")
413     def __init__(self, doc, name, layer):
414         self.doc = doc
415         self.new_name = name
416         self.layer = layer
417     def redo(self):
418         self.old_name = self.layer.name
419         self.layer.name = self.new_name
420         self._notify_document_observers()
421     def undo(self):
422         self.layer.name = self.old_name
423         self._notify_document_observers()
424
425 class SetLayerVisibility(Action):
426     def __init__(self, doc, visible, layer):
427         self.doc = doc
428         self.new_visibility = visible
429         self.layer = layer
430     def redo(self):
431         self.old_visibility = self.layer.visible
432         self.layer.visible = self.new_visibility
433         self._notify_canvas_observers([self.layer])
434         self._notify_document_observers()
435     def undo(self):
436         self.layer.visible = self.old_visibility
437         self._notify_canvas_observers([self.layer])
438         self._notify_document_observers()
439     def update(self, visible):
440         self.layer.visible = visible
441         self.new_visibility = visible
442         self._notify_canvas_observers([self.layer])
443         self._notify_document_observers()
444     @property
445     def display_name(self):
446         if self.new_visibility:
447             return _("Make Layer Visible")
448         else:
449             return _("Make Layer Invisible")
450
451 class SetLayerLocked (Action):
452     def __init__(self, doc, locked, layer):
453         self.doc = doc
454         self.new_locked = locked
455         self.layer = layer
456     def redo(self):
457         self.old_locked = self.layer.locked
458         self.layer.locked = self.new_locked
459         self._notify_canvas_observers([self.layer])
460         self._notify_document_observers()
461     def undo(self):
462         self.layer.locked = self.old_locked
463         self._notify_canvas_observers([self.layer])
464         self._notify_document_observers()
465     def update(self, locked):
466         self.layer.locked = locked
467         self.new_locked = locked
468         self._notify_canvas_observers([self.layer])
469         self._notify_document_observers()
470     @property
471     def display_name(self):
472         if self.new_locked:
473             return _("Lock Layer")
474         else:
475             return _("Unlock Layer")
476
477 class SetLayerOpacity(Action):
478     display_name = _("Change Layer Visibility")
479     def __init__(self, doc, opacity, layer=None):
480         self.doc = doc
481         self.new_opacity = opacity
482         self.layer = layer
483     def redo(self):
484         if self.layer:
485             l = self.layer
486         else:
487             l = self.doc.layer
488         previous_effective_opacity = l.effective_opacity
489         self.old_opacity = l.opacity
490         l.opacity = self.new_opacity
491         if l.effective_opacity != previous_effective_opacity:
492             self._notify_canvas_observers([l])
493         self._notify_document_observers()
494     def undo(self):
495         if self.layer:
496             l = self.layer
497         else:
498             l = self.doc.layer
499         previous_effective_opacity = l.effective_opacity
500         l.opacity = self.old_opacity
501         if l.effective_opacity != previous_effective_opacity:
502             self._notify_canvas_observers([l])
503         self._notify_document_observers()
504
505 class SetLayerCompositeOp(Action):
506     display_name = _("Change Layer Blending Mode")
507     def __init__(self, doc, compositeop, layer=None):
508         self.doc = doc
509         self.new_compositeop = compositeop
510         self.layer = layer
511     def redo(self):
512         if self.layer:
513             l = self.layer
514         else:
515             l = self.doc.layer
516         self.old_compositeop = l.compositeop
517         l.compositeop = self.new_compositeop
518         self._notify_canvas_observers([l])
519         self._notify_document_observers()
520     def undo(self):
521         if self.layer:
522             l = self.layer
523         else:
524             l = self.doc.layer
525         l.compositeop = self.old_compositeop
526         self._notify_canvas_observers([l])
527         self._notify_document_observers()
528