1# This file is part of MyPaint.
2# Copyright (C) 2011-2019 by the MyPaint Development Team
3# Copyright (C) 2007-2012 by Martin Renold <martinxyz@gmx.ch>
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation; either version 2 of the License, or
8# (at your option) any later version.
9
10"""Layer group classes (stacks)"""
11
12
13## Imports
14
15from __future__ import division, print_function
16
17import logging
18from copy import copy
19
20from lib.gettext import C_
21import lib.mypaintlib
22import lib.pixbufsurface
23import lib.helpers as helpers
24import lib.fileutils
25from lib.modes import STANDARD_MODES
26from lib.modes import STACK_MODES
27from lib.modes import PASS_THROUGH_MODE
28from . import core
29from . import data
30import lib.layer.error
31import lib.surface
32import lib.autosave
33import lib.feedback
34import lib.layer.core
35from .rendering import Opcode
36from lib.pycompat import unicode
37
38logger = logging.getLogger(__name__)
39
40
41## Class defs
42
43class LayerStack (core.LayerBase, lib.autosave.Autosaveable):
44    """Ordered stack of layers, linear but nestable
45
46    A stack's sub-layers are stored in the reverse order to that used by
47    rendering: the first element in the sequence, index ``0``, is the
48    topmost layer. As it happens, this makes both interoperability with
49    GTK tree paths and loading from OpenRaster simpler: both use the
50    same top-to-bottom order.
51
52    Layer stacks support list-like access to their child layers.  Using
53    the `insert()`, `pop()`, `remove()` methods or index-based access
54    and assignment maintains the root stack reference sensibly (most of
55    the time) when sublayers are added or deleted from a LayerStack
56    which is part of a tree structure. Permuting the structure this way
57    announces the changes to any registered observer methods for these
58    events.
59
60    """
61
62    ## Class constants
63
64    DEFAULT_NAME = C_(
65        "layer default names",
66        # TRANSLATORS: Short default name for layer groups.
67        "Group",
68    )
69
70    TYPE_DESCRIPTION = C_(
71        "layer type descriptions",
72        u"Layer Group",
73    )
74
75    PERMITTED_MODES = set(STANDARD_MODES + STACK_MODES)
76
77    ## Construction and other lifecycle stuff
78
79    def __init__(self, **kwargs):
80        """Initialize, with no sub-layers.
81
82        Despite an empty layer stack having a zero length, it never
83        tests as False under any circumstances. All layers and layer
84        groups work this way.
85
86        >>> g = LayerStack()
87        >>> len(g)
88        0
89        >>> if not g:
90        ...    raise ValueError("len=0 group tests as False, incorrectly")
91        >>> bool(g)
92        True
93
94        """
95        self._layers = []  # must be done before supercall
96        super(LayerStack, self).__init__(**kwargs)
97
98    def load_from_openraster(self, orazip, elem, cache_dir, progress,
99                             x=0, y=0, **kwargs):
100        """Load this layer from an open .ora file"""
101        if elem.tag != "stack":
102            raise lib.layer.error.LoadingFailed("<stack/> expected")
103
104        if not progress:
105            progress = lib.feedback.Progress()
106        progress.items = 1 + len(list(elem.findall("./*")))
107
108        # Item 1: supercall
109        super(LayerStack, self).load_from_openraster(
110            orazip,
111            elem,
112            cache_dir,
113            progress.open(),
114            x=x, y=y,
115            **kwargs
116        )
117        self.clear()
118        x += int(elem.attrib.get("x", 0))
119        y += int(elem.attrib.get("y", 0))
120
121        # The only combination which can result in a non-isolated mode
122        # under the OpenRaster and W3C definition. Represented
123        # internally with a special mode to make the UI prettier.
124        isolated_flag = unicode(elem.attrib.get("isolation", "auto"))
125        # TODO: Check if this applies to CombineSpectralWGM as well
126        is_pass_through = (self.mode == lib.mypaintlib.CombineNormal
127                           and self.opacity == 1.0
128                           and (isolated_flag.lower() == "auto"))
129        if is_pass_through:
130            self.mode = PASS_THROUGH_MODE
131
132        # Items 2..n: child elements.
133        # Document order is the same as _layers, bottom layer to top.
134        for child_elem in elem.findall("./*"):
135            assert child_elem is not elem
136            self._load_child_layer_from_orazip(
137                orazip,
138                child_elem,
139                cache_dir,
140                progress.open(),
141                x=x, y=y,
142                **kwargs
143            )
144        progress.close()
145
146    def _load_child_layer_from_orazip(self, orazip, elem, cache_dir,
147                                      progress, x=0, y=0, **kwargs):
148        """Loads a single child layer element from an open .ora file"""
149        try:
150            child = _layer_new_from_orazip(
151                orazip,
152                elem,
153                cache_dir,
154                progress,
155                self.root,
156                x=x, y=y,
157                **kwargs
158            )
159        except lib.layer.error.LoadingFailed:
160            logger.warning("Skipping non-loadable layer")
161        else:
162            self.append(child)
163
164    def load_from_openraster_dir(self, oradir, elem, cache_dir, progress,
165                                 x=0, y=0, **kwargs):
166        """Loads layer flags and data from an OpenRaster-style dir"""
167        if elem.tag != "stack":
168            raise lib.layer.error.LoadingFailed("<stack/> expected")
169        super(LayerStack, self).load_from_openraster_dir(
170            oradir,
171            elem,
172            cache_dir,
173            progress,
174            x=x, y=y,
175            **kwargs
176        )
177        self.clear()
178        x += int(elem.attrib.get("x", 0))
179        y += int(elem.attrib.get("y", 0))
180        # Convert normal+nonisolated to the internal pass-thru mode
181        isolated_flag = unicode(elem.attrib.get("isolation", "auto"))
182        # TODO: Check if this applies to CombineSpectralWGM as well
183        is_pass_through = (self.mode == lib.mypaintlib.CombineNormal
184                           and self.opacity == 1.0
185                           and (isolated_flag.lower() == "auto"))
186        if is_pass_through:
187            self.mode = PASS_THROUGH_MODE
188        # Delegate loading of child layers
189        for child_elem in elem.findall("./*"):
190            assert child_elem is not elem
191            self._load_child_layer_from_oradir(
192                oradir,
193                child_elem,
194                cache_dir,
195                progress,
196                x=x, y=y,
197                **kwargs
198            )
199
200    def _load_child_layer_from_oradir(self, oradir, elem, cache_dir,
201                                      progress, x=0, y=0, **kwargs):
202        """Loads a single child layer element from an open .ora file
203
204        Child classes can override this, but otherwise it's an internal
205        method.
206
207        """
208        try:
209            child = _layer_new_from_oradir(
210                oradir,
211                elem,
212                cache_dir,
213                progress,
214                self.root,
215                x=x, y=y,
216                **kwargs
217            )
218        except lib.layer.error.LoadingFailed:
219            logger.warning("Skipping non-loadable layer")
220        else:
221            self.append(child)
222
223    def clear(self):
224        """Clears the layer, and removes any child layers"""
225        super(LayerStack, self).clear()
226        removed = list(self._layers)
227        self._layers[:] = []
228        for i, layer in reversed(list(enumerate(removed))):
229            self._notify_disown(layer, i)
230
231    def __repr__(self):
232        """String representation of a stack
233
234        >>> repr(LayerStack(name='test'))
235        "<LayerStack len=0 'test'>"
236        """
237        if self.name:
238            return '<%s len=%d %r>' % (self.__class__.__name__, len(self),
239                                       self.name)
240        else:
241            return '<%s len=%d>' % (self.__class__.__name__, len(self))
242
243    ## Notification
244
245    def _notify_disown(self, orphan, oldindex):
246        """Recursively process a removed child (root reset, notify)"""
247        # Reset root and notify. No actual tree permutations.
248        orphan.group = None
249        root = self.root
250        orphan.root = None
251        # Recursively disown all descendents of the orphan first
252        if isinstance(orphan, LayerStack):
253            for i, child in reversed(list(enumerate(orphan))):
254                orphan._notify_disown(child, i)
255        # Then notify, now all descendents are gone
256        if root is not None:
257            root._notify_layer_deleted(self, orphan, oldindex)
258
259    def _notify_adopt(self, adoptee, newindex):
260        """Recursively process an added child (set root, notify)"""
261        # Set root and notify. No actual tree permutations.
262        adoptee.group = self
263        root = self.root
264        adoptee.root = root
265        # Notify for the newly adopted layer first
266        if root is not None:
267            root._notify_layer_inserted(self, adoptee, newindex)
268        # Recursively adopt all descendents of the adoptee after
269        if isinstance(adoptee, LayerStack):
270            for i, child in enumerate(adoptee):
271                adoptee._notify_adopt(child, i)
272
273    ## Basic list-of-layers access
274
275    def __len__(self):
276        """Return the number of layers in the stack
277
278        >>> stack = LayerStack()
279        >>> len(stack)
280        0
281        >>> from . import data
282        >>> stack.append(data.PaintingLayer())
283        >>> len(stack)
284        1
285        """
286        return len(self._layers)
287
288    def __iter__(self):
289        """Iterates across child layers in the order used by append()"""
290        return iter(self._layers)
291
292    def append(self, layer):
293        """Appends a layer (notifies root)"""
294        newindex = len(self)
295        self._layers.append(layer)
296        self._notify_adopt(layer, newindex)
297        self._content_changed(*layer.get_full_redraw_bbox())
298
299    def remove(self, layer):
300        """Removes a layer by equality (notifies root)"""
301        oldindex = self._layers.index(layer)
302        assert oldindex is not None
303        removed = self._layers.pop(oldindex)
304        assert removed is not None
305        self._notify_disown(removed, oldindex)
306        self._content_changed(*removed.get_full_redraw_bbox())
307
308    def pop(self, index=None):
309        """Removes a layer by index (notifies root)"""
310        if index is None:
311            index = len(self._layers)-1
312            removed = self._layers.pop()
313        else:
314            index = self._normidx(index)
315            removed = self._layers.pop(index)
316        self._notify_disown(removed, index)
317        self._content_changed(*removed.get_full_redraw_bbox())
318        return removed
319
320    def _normidx(self, i, insert=False):
321        """Normalize an index for array-like access
322
323        >>> group = LayerStack()
324        >>> group.append(data.PaintingLayer())
325        >>> group.append(data.PaintingLayer())
326        >>> group.append(data.PaintingLayer())
327        >>> group._normidx(-4, insert=True)
328        0
329        >>> group._normidx(1)
330        1
331        >>> group._normidx(999)
332        999
333        >>> group._normidx(999, insert=True)
334        3
335        """
336        if i < 0:
337            i = len(self) + i
338        if insert:
339            return max(0, min(len(self), i))
340        return i
341
342    def insert(self, index, layer):
343        """Adds a layer before an index (notifies root)"""
344        index = self._normidx(index, insert=True)
345        self._layers.insert(index, layer)
346        self._notify_adopt(layer, index)
347        self._content_changed(*layer.get_full_redraw_bbox())
348
349    def __setitem__(self, index, layer):
350        """Replaces the layer at an index (notifies root)"""
351        index = self._normidx(index)
352        oldlayer = self._layers[index]
353        self._layers[index] = layer
354        self._notify_disown(oldlayer, index)
355        updates = [oldlayer.get_full_redraw_bbox()]
356        self._notify_adopt(layer, index)
357        updates.append(layer.get_full_redraw_bbox())
358        self._content_changed(*tuple(core.combine_redraws(updates)))
359
360    def __getitem__(self, index):
361        """Fetches the layer at an index"""
362        return self._layers[index]
363
364    def index(self, layer):
365        """Fetches the index of a child layer, by equality"""
366        return self._layers.index(layer)
367
368    ## Info methods
369
370    def get_bbox(self):
371        """Returns the inherent (data) bounding box of the stack"""
372        result = helpers.Rect()
373        for layer in self._layers:
374            result.expandToIncludeRect(layer.get_bbox())
375        return result
376
377    def get_full_redraw_bbox(self):
378        """Returns the full update notification bounding box of the stack"""
379        result = super(LayerStack, self).get_full_redraw_bbox()
380        if result.w == 0 or result.h == 0:
381            return result
382        for layer in self._layers:
383            bbox = layer.get_full_redraw_bbox()
384            if bbox.w == 0 or bbox.h == 0:
385                return bbox
386            result.expandToIncludeRect(bbox)
387        return result
388
389    def is_empty(self):
390        return len(self._layers) == 0
391
392    @property
393    def effective_opacity(self):
394        """The opacity used when compositing a layer: zero if invisible"""
395        if self.visible:
396            return self.opacity
397        else:
398            return 0.0
399
400    ## Renderable implementation
401
402    def get_render_ops(self, spec):
403        """Get rendering instructions."""
404
405        # Defaults, which might be overridden
406        visible = self.visible
407        mode = self.mode
408        opacity = self.opacity
409
410        # Respect the explicit layers list.
411        if spec.layers is not None:
412            if self not in spec.layers:
413                return []
414
415        if spec.previewing:
416            # Previewing mode is a quick flash of how the layer data
417            # looks, unaffected by any modes or visibility settings.
418            visible = True
419            mode = PASS_THROUGH_MODE
420            opacity = 1
421
422        elif spec.solo:
423            # Solo mode shows how the current layer looks by itself when
424            # its visible flag is true, along with any of its child
425            # layers.  Child layers use their natural visibility.
426            # However solo layers are unaffected by any ancestor layers'
427            # modes or visibilities.
428
429            try:
430                ancestor = not spec.__descendent_of_current
431            except AttributeError:
432                ancestor = True
433
434            if self is spec.current:
435                spec = copy(spec)
436                spec.__descendent_of_current = True
437                visible = True
438            elif ancestor:
439                mode = PASS_THROUGH_MODE
440                opacity = 1.0
441                visible = True
442
443        if not visible:
444            return []
445
446        isolate_child_layers = (mode != PASS_THROUGH_MODE)
447
448        ops = []
449        if isolate_child_layers:
450            ops.append((Opcode.PUSH, None, None, None))
451        for child_layer in reversed(self._layers):
452            ops.extend(child_layer.get_render_ops(spec))
453        if isolate_child_layers:
454            ops.append((Opcode.POP, None, mode, opacity))
455
456        return ops
457
458    ## Flood fill
459
460    def flood_fill(self, fill_args, dst_layer=None):
461        """Fills a point on the surface with a color (into other only!)
462
463        See `PaintingLayer.flood_fill() for parameters and semantics. Layer
464        stacks only support flood-filling into other layers because they are
465        not surface backed.
466
467        """
468        assert dst_layer is not self
469        assert dst_layer is not None
470
471        root = self.root
472        if root is None:
473            raise ValueError(
474                "Cannot flood_fill() into a layer group which is not "
475                "a descendent of a RootLayerStack."
476            )
477        src = root.get_tile_accessible_layer_rendering(self)
478        dst = dst_layer._surface
479        return lib.tiledsurface.flood_fill(src, fill_args, dst)
480
481    def get_fillable(self):
482        """False! Stacks can't be filled interactively or directly."""
483        return False
484
485    ## Moving
486
487    def get_move(self, x, y):
488        """Get a translation/move object for this layer"""
489        return LayerStackMove(self, x, y)
490
491    ## Saving
492
493    @lib.fileutils.via_tempfile
494    def save_as_png(self, filename, *rect, **kwargs):
495        """Save to a named PNG file.
496
497        For a layer stack (including the special root one), this works
498        by rendering the stack and its contents in solo mode.
499
500        """
501        root = self.root
502        if root is None:
503            raise ValueError(
504                "Cannot flood_fill() into a layer group which is not "
505                "a descendent of a RootLayerStack."
506            )
507        root.render_layer_to_png_file(self, filename, bbox=rect, **kwargs)
508
509    def save_to_openraster(self, orazip, tmpdir, path,
510                           canvas_bbox, frame_bbox, progress=None,
511                           **kwargs):
512        """Saves the stack's data into an open OpenRaster ZipFile"""
513
514        if not progress:
515            progress = lib.feedback.Progress()
516        progress.items = 1 + len(self)
517
518        # MyPaint uses the same origin internally for all data layers,
519        # meaning the internal stack objects don't impose any offsets on
520        # their children. Any x or y attrs which were present when the
521        # stack was loaded from .ORA were accounted for back then.
522        stack_elem = self._get_stackxml_element("stack")
523
524        # Recursively save out the stack's child layers
525        for layer_idx, layer in list(enumerate(self)):
526            layer_path = tuple(list(path) + [layer_idx])
527            layer_elem = layer.save_to_openraster(orazip, tmpdir, layer_path,
528                                                  canvas_bbox, frame_bbox,
529                                                  progress=progress.open(),
530                                                  **kwargs)
531            stack_elem.append(layer_elem)
532
533        # OpenRaster has no pass-through composite op: need to override.
534        # MyPaint's "Pass-through" mode is internal shorthand for the
535        # default behaviour of OpenRaster.
536        isolation = "isolate"
537        if self.mode == PASS_THROUGH_MODE:
538            stack_elem.attrib.pop("opacity", None)  # => 1.0
539            stack_elem.attrib.pop("composite-op", None)  # => svg:src-over
540            isolation = "auto"
541        stack_elem.attrib["isolation"] = isolation
542
543        progress += 1
544
545        progress.close()
546
547        return stack_elem
548
549    def queue_autosave(self, oradir, taskproc, manifest, bbox, **kwargs):
550        """Queues the layer for auto-saving"""
551        # Build a layers.xml element: no x or y for stacks
552        stack_elem = self._get_stackxml_element("stack")
553        for layer in self._layers:
554            layer_elem = layer.queue_autosave(
555                oradir, taskproc, manifest, bbox,
556                **kwargs
557            )
558            stack_elem.append(layer_elem)
559        # Convert the internal pass-through composite op to its
560        # OpenRaster equivalent: the default, non-isolated src-over.
561        isolation = "isolate"
562        if self.mode == PASS_THROUGH_MODE:
563            stack_elem.attrib.pop("opacity", None)  # => 1.0
564            stack_elem.attrib.pop("composite-op", None)  # => svg:src-over
565            isolation = "auto"
566        stack_elem.attrib["isolation"] = isolation
567        return stack_elem
568
569    ## Snapshotting
570
571    def save_snapshot(self):
572        """Snapshots the state of the layer, for undo purposes"""
573        return LayerStackSnapshot(self)
574
575    ## Trimming
576
577    def trim(self, rect):
578        """Trim the layer to a rectangle, discarding data outside it"""
579        for layer in self:
580            layer.trim(rect)
581
582    ## Type-specific action
583
584    def activate_layertype_action(self):
585        root = self.root
586        if root is None:
587            return
588        path = root.deepindex(self)
589        if path and len(path) > 0:
590            root.expand_layer(path)
591
592    def get_icon_name(self):
593        return "mypaint-layer-group-symbolic"
594
595
596class LayerStackSnapshot (core.LayerBaseSnapshot):
597    """Snapshot of a layer stack's state"""
598
599    def __init__(self, layer):
600        super(LayerStackSnapshot, self).__init__(layer)
601        self.layer_snaps = [l.save_snapshot() for l in layer]
602        self.layer_classes = [l.__class__ for l in layer]
603
604    def restore_to_layer(self, layer):
605        super(LayerStackSnapshot, self).restore_to_layer(layer)
606        layer.clear()
607        for layer_class, snap in zip(self.layer_classes, self.layer_snaps):
608            child = layer_class()
609            child.load_snapshot(snap)
610            layer.append(child)
611
612
613class LayerStackMove (object):
614    """Move object wrapper for layer stacks"""
615
616    def __init__(self, layers, x, y):
617        super(LayerStackMove, self).__init__()
618        self._moves = []
619        for layer in layers:
620            self._moves.append(layer.get_move(x, y))
621
622    def update(self, dx, dy):
623        for move in self._moves:
624            move.update(dx, dy)
625
626    def cleanup(self):
627        for move in self._moves:
628            move.cleanup()
629
630    def process(self, n=200):
631        if len(self._moves) < 1:
632            return False
633        n = max(20, int(n // len(self._moves)))
634        incomplete = False
635        for move in self._moves:
636            incomplete = move.process(n=n) or incomplete
637        return incomplete
638
639
640## Layer factory func
641
642_LAYER_LOADER_CLASS_ORDER = [
643    LayerStack,
644    data.PaintingLayer,
645    data.VectorLayer,
646    data.FallbackBitmapLayer,
647    data.FallbackDataLayer,
648]
649
650
651def _layer_new_from_orazip(orazip, elem, cache_dir, progress,
652                           root, x=0, y=0, **kwargs):
653    """New layer from an OpenRaster zipfile (factory)"""
654    for layer_class in _LAYER_LOADER_CLASS_ORDER:
655        try:
656            return layer_class.new_from_openraster(
657                orazip,
658                elem,
659                cache_dir,
660                progress,
661                root,
662                x=x, y=y,
663                **kwargs
664            )
665        except lib.layer.error.LoadingFailed:
666            pass
667    raise lib.layer.error.LoadingFailed(
668        "No delegate class willing to load %r" % (elem,)
669    )
670
671
672def _layer_new_from_oradir(oradir, elem, cache_dir, progress,
673                           root, x=0, y=0, **kwargs):
674    """New layer from a dir with an OpenRaster-like layout (factory)"""
675    for layer_class in _LAYER_LOADER_CLASS_ORDER:
676        try:
677            return layer_class.new_from_openraster_dir(
678                oradir,
679                elem,
680                cache_dir,
681                progress,
682                root,
683                x=x, y=y,
684                **kwargs
685            )
686        except lib.layer.error.LoadingFailed:
687            pass
688    raise lib.layer.error.LoadingFailed(
689        "No delegate class willing to load %r" % (elem,)
690    )
691
692
693## Module testing
694
695
696def _test():
697    """Run doctest strings"""
698    import doctest
699    doctest.testmod(optionflags=doctest.ELLIPSIS)
700
701
702if __name__ == '__main__':
703    logging.basicConfig(level=logging.DEBUG)
704    _test()
705