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