OSDN Git Service

tablet detection: workaround for laptop touchpad
[mypaint-anime/master.git] / lib / document.py
index 1b9f08f..0c3f561 100644 (file)
@@ -21,6 +21,8 @@ import brush
 N = tiledsurface.N
 LOAD_CHUNK_SIZE = 64*1024
 
+from layer import DEFAULT_COMPOSITE_OP, VALID_COMPOSITE_OPS
+
 class SaveLoadError(Exception):
     """Expected errors on loading or saving, like missing permissions or non-existing files."""
     pass
@@ -42,13 +44,17 @@ class Document():
     #   or discarded as empty before any other action is possible.
     #   (split_stroke)
 
-    def __init__(self):
-        self.brush = brush.Brush()
+    def __init__(self, brushinfo=None):
+        if not brushinfo:
+            brushinfo = brush.BrushInfo()
+            brushinfo.load_defaults()
+        self.brush = brush.Brush(brushinfo)
         self.stroke = None
         self.canvas_observers = []
         self.stroke_observers = [] # callback arguments: stroke, brush (brush is a temporary read-only convenience object)
         self.doc_observers = []
         self.frame_observers = []
+        self.command_stack_observers = []
         self.clear(True)
 
         self._frame = [0, 0, 0, 0]
@@ -114,7 +120,9 @@ class Document():
         if not init:
             bbox = self.get_bbox()
         # throw everything away, including undo stack
+
         self.command_stack = command.CommandStack()
+        self.command_stack.stack_observers = self.command_stack_observers
         self.set_background((255, 255, 255))
         self.layers = []
         self.layer_idx = None
@@ -147,34 +155,46 @@ class Document():
     def select_layer(self, idx):
         self.do(command.SelectLayer(self, idx))
 
-    def move_layer(self, was_idx, new_idx):
-        self.do(command.MoveLayer(self, was_idx, new_idx))
+    def move_layer(self, was_idx, new_idx, select_new=False):
+        self.do(command.MoveLayer(self, was_idx, new_idx, select_new))
+
+    def duplicate_layer(self, insert_idx=None, name=''):
+        self.do(command.DuplicateLayer(self, insert_idx, name))
+
+    def reorder_layers(self, new_layers):
+        self.do(command.ReorderLayers(self, new_layers))
 
     def clear_layer(self):
-        if not self.layer.surface.is_empty():
+        if not self.layer.is_empty():
             self.do(command.ClearLayer(self))
 
-    def stroke_to(self, dtime, x, y, pressure, xtilt,ytilt):
+    def stroke_to(self, dtime, x, y, pressure, xtilt, ytilt):
         if not self.stroke:
             self.stroke = stroke.Stroke()
             self.stroke.start_recording(self.brush)
             self.snapshot_before_stroke = self.layer.save_snapshot()
-        self.stroke.record_event(dtime, x, y, pressure, xtilt,ytilt)
+        self.stroke.record_event(dtime, x, y, pressure, xtilt, ytilt)
 
-        l = self.layer
-        l.surface.begin_atomic()
-        split = self.brush.stroke_to (l.surface, x, y, pressure, xtilt,ytilt, dtime)
-        l.surface.end_atomic()
+        split = self.layer.stroke_to(self.brush, x, y,
+                                pressure, xtilt, ytilt, dtime)
 
         if split:
             self.split_stroke()
 
+    def redo_last_stroke_with_different_brush(self, brush):
+        cmd = self.get_last_command()
+        if not isinstance(cmd, command.Stroke):
+            return
+        cmd = self.undo()
+        assert isinstance(cmd, command.Stroke)
+        new_stroke = cmd.stroke.copy_using_different_brush(brush)
+        snapshot_before = self.layer.save_snapshot()
+        new_stroke.render(self.layer._surface)
+        self.do(command.Stroke(self, new_stroke, snapshot_before))
+
     def straight_line(self, src, dst):
         self.split_stroke()
-        # TODO: undo last stroke if it was very short... (but not at document level?)
-        real_brush = self.brush
-        self.brush = brush.Brush()
-        self.brush.copy_settings_from(real_brush)
+        self.brush.reset() # reset dynamic states (eg. filtered velocity)
 
         duration = 3.0
         pressure = 0.3
@@ -188,7 +208,7 @@ class Document():
         for i in xrange(N):
             self.stroke_to(duration/N, x[i], y[i], pressure, 0.0, 0.0)
         self.split_stroke()
-        self.brush = real_brush
+        self.brush.reset()
 
 
     def layer_modified_cb(self, *args):
@@ -222,16 +242,12 @@ class Document():
         self.split_stroke()
         return self.command_stack.get_last_command()
 
-    def set_brush(self, brush):
-        self.split_stroke()
-        self.brush.copy_settings_from(brush)
-
     def get_bbox(self):
         res = helpers.Rect()
         for layer in self.layers:
             # OPTIMIZE: only visible layers...
             # careful: currently saving assumes that all layers are included
-            bbox = layer.surface.get_bbox()
+            bbox = layer.get_bbox()
             res.expandToIncludeRect(bbox)
         return res
 
@@ -253,8 +269,11 @@ class Document():
         background.blit_tile_into(dst, tx, ty, mipmap_level)
 
         for layer in layers:
-            surface = layer.surface
-            surface.composite_tile_over(dst, tx, ty, mipmap_level=mipmap_level, opacity=layer.effective_opacity)
+            surface = layer._surface
+            surface.composite_tile(dst, tx, ty,
+                    mipmap_level=mipmap_level,
+                    opacity=layer.effective_opacity,
+                    mode=layer.compositeop)
 
         mypaintlib.tile_convert_rgb16_to_rgb8(dst, dst_8bit)
 
@@ -297,6 +316,15 @@ class Document():
             self.undo()
         self.do(command.SetLayerOpacity(self, opacity, layer))
 
+    def set_layer_compositeop(self, compositeop, layer=None):
+        """Sets the composition-operation of a layer. If layer=None, works on the current layer"""
+        if compositeop not in VALID_COMPOSITE_OPS:
+            compositeop = DEFAULT_COMPOSITE_OP
+        cmd = self.get_last_command()
+        if isinstance(cmd, command.SetLayerCompositeOp):
+            self.undo()
+        self.do(command.SetLayerCompositeOp(self, compositeop, layer))
+
     def set_background(self, obj):
         # This is not an undoable action. One reason is that dragging
         # on the color chooser would get tons of undo steps.
@@ -316,19 +344,19 @@ class Document():
     def is_layered(self):
         count = 0
         for l in self.layers:
-            if not l.surface.is_empty():
+            if not l.is_empty():
                 count += 1
         return count > 1
 
     def is_empty(self):
-        return len(self.layers) == 1 and self.layer.surface.is_empty()
+        return len(self.layers) == 1 and self.layer.is_empty()
 
     def save(self, filename, **kwargs):
         self.split_stroke()
-        trash, ext = os.path.splitext(filename)
+        junk, ext = os.path.splitext(filename)
         ext = ext.lower().replace('.', '')
         save = getattr(self, 'save_' + ext, self.unsupported)
-        try:        
+        try:
             save(filename, **kwargs)
         except gobject.GError, e:
             traceback.print_exc()
@@ -347,7 +375,7 @@ class Document():
             raise SaveLoadError, _('File does not exist: %s') % repr(filename)
         if not os.access(filename,os.R_OK):
             raise SaveLoadError, _('You do not have the necessary permissions to open file: %s') % repr(filename)
-        trash, ext = os.path.splitext(filename)
+        junk, ext = os.path.splitext(filename)
         ext = ext.lower().replace('.', '')
         load = getattr(self, 'load_' + ext, self.unsupported)
         try:
@@ -362,7 +390,7 @@ class Document():
         self.unsaved_painting_time = 0.0
         self.call_doc_observers()
 
-    def unsupported(self, filename):
+    def unsupported(self, filename, *args, **kwargs):
         raise SaveLoadError, _('Unknown file format extension: %s') % repr(filename)
 
     def render_as_pixbuf(self, *args, **kwargs):
@@ -371,6 +399,9 @@ class Document():
     def render_thumbnail(self):
         t0 = time.time()
         x, y, w, h = self.get_effective_bbox()
+        if w == 0 or h == 0:
+            # workaround to save empty documents
+            x, y, w, h = 0, 0, tiledsurface.N, tiledsurface.N
         mipmap_level = 0
         while mipmap_level < tiledsurface.MAX_MIPMAP_LEVEL and max(w, h) >= 512:
             mipmap_level += 1
@@ -391,7 +422,7 @@ class Document():
                 tmp_layer = layer.Layer()
                 for l in self.layers:
                     l.merge_into(tmp_layer)
-                tmp_layer.surface.save(filename, *doc_bbox)
+                tmp_layer.save_as_png(filename, *doc_bbox)
             else:
                 pixbufsurface.save_as_png(self, filename, *doc_bbox, alpha=False, **kwargs)
 
@@ -404,7 +435,7 @@ class Document():
         doc_bbox = self.get_effective_bbox()
         for i, l in enumerate(self.layers):
             filename = '%s.%03d%s' % (prefix, i+1, ext)
-            l.surface.save(filename, *doc_bbox, **kwargs)
+            l.save_as_png(filename, *doc_bbox, **kwargs)
 
     @staticmethod
     def _pixbuf_from_stream(fp, feedback_cb=None):
@@ -430,8 +461,10 @@ class Document():
     load_jpeg = load_from_pixbuf_file
 
     def save_jpg(self, filename, quality=90, **kwargs):
-        doc_bbox = self.get_effective_bbox()
-        pixbuf = self.render_as_pixbuf(*doc_bbox, **kwargs)
+        x, y, w, h = self.get_effective_bbox()
+        if w == 0 or h == 0:
+            x, y, w, h = 0, 0, N, N # allow to save empty documents
+        pixbuf = self.render_as_pixbuf(x, y, w, h, **kwargs)
         pixbuf.save(filename, 'jpeg', options={'quality':str(quality)})
 
     save_jpeg = save_jpg
@@ -466,12 +499,12 @@ class Document():
         def store_surface(surface, name, rect=[]):
             tmp = join(tempdir, 'tmp.png')
             t1 = time.time()
-            surface.save(tmp, *rect, **kwargs)
+            surface.save_as_png(tmp, *rect, **kwargs)
             print '  %.3fs surface saving %s' % (time.time() - t1, name)
             z.write(tmp, name)
             os.remove(tmp)
 
-        def add_layer(x, y, opac, surface, name, layer_name, visible=True, rect=[]):
+        def add_layer(x, y, opac, surface, name, layer_name, visible=True, compositeop=DEFAULT_COMPOSITE_OP, rect=[]):
             layer = ET.Element('layer')
             stack.append(layer)
             store_surface(surface, name, rect)
@@ -482,6 +515,9 @@ class Document():
             a['x'] = str(x)
             a['y'] = str(y)
             a['opacity'] = str(opac)
+            if compositeop not in VALID_COMPOSITE_OPS:
+                compositeop = DEFAULT_COMPOSITE_OP
+            a['composite-op'] = compositeop
             if visible:
                 a['visibility'] = 'visible'
             else:
@@ -489,11 +525,11 @@ class Document():
             return layer
 
         for idx, l in enumerate(reversed(self.layers)):
-            if l.surface.is_empty():
+            if l.is_empty():
                 continue
             opac = l.opacity
-            x, y, w, h = l.surface.get_bbox()
-            el = add_layer(x-x0, y-y0, opac, l.surface, 'data/layer%03d.png' % idx, l.name, l.visible, rect=(x, y, w, h))
+            x, y, w, h = l.get_bbox()
+            el = add_layer(x-x0, y-y0, opac, l._surface, 'data/layer%03d.png' % idx, l.name, l.visible, l.compositeop, rect=(x, y, w, h))
             # strokemap
             sio = StringIO()
             l.save_strokemap_to_file(sio, -x, -y)
@@ -506,7 +542,8 @@ class Document():
         bg = self.background
         # save as fully rendered layer
         x, y, w, h = self.get_bbox()
-        l = add_layer(x, y, 1.0, bg, 'data/background.png', 'background', rect=(x,y,w,h))
+        l = add_layer(x-x0, y-y0, 1.0, bg, 'data/background.png', 'background',
+                      DEFAULT_COMPOSITE_OP, rect=(x,y,w,h))
         x, y, w, h = bg.get_pattern_bbox()
         # save as single pattern (with corrected origin)
         store_surface(bg, 'data/background_tile.png', rect=(x+x0, y+y0, w, h))
@@ -614,6 +651,10 @@ class Document():
             x = int(a.get('x', '0'))
             y = int(a.get('y', '0'))
             opac = float(a.get('opacity', '1.0'))
+            compositeop = str(a.get('composite-op', DEFAULT_COMPOSITE_OP))
+            if compositeop not in VALID_COMPOSITE_OPS:
+                compositeop = DEFAULT_COMPOSITE_OP
+
             visible = not 'hidden' in a.get('visibility', 'visible')
             self.add_layer(insert_idx=0, name=name)
             last_pixbuf = pixbuf
@@ -622,6 +663,7 @@ class Document():
             layer = self.layers[0]
 
             self.set_layer_opacity(helpers.clamp(opac, 0.0, 1.0), layer)
+            self.set_layer_compositeop(compositeop, layer)
             self.set_layer_visibility(visible, layer)
             print '  %.3fs converting pixbuf to layer format' % (time.time() - t1)
             # strokemap
@@ -635,14 +677,15 @@ class Document():
                     sio.close()
 
         if len(self.layers) == 1:
-            raise ValueError, 'Could not load any layer.'
+            # no assertion (allow empty documents)
+            print 'Warning: Could not load any layer, document is empty.'
 
         if no_background:
             # recognize solid or tiled background layers, at least those that mypaint <= 0.7.1 saves
             t1 = time.time()
             p = last_pixbuf
             if not p.get_has_alpha() and p.get_width() % N == 0 and p.get_height() % N == 0:
-                tiles = self.layers[0].surface.tiledict.values()
+                tiles = self.layers[0]._surface.tiledict.values()
                 if len(tiles) > 1:
                     all_equal = True
                     for tile in tiles[1:]: