Ver Fonte

Milestone: Layers now are drawable with a good UI for drawing,
Undo-redo stack works, saving and loading works, exporting works,
fully functional application with a couple of known bugs.
Bug 1: Undo-Redo always selects base layer when done, (should
stay on same layer if it still exists).
Bug 2: UI still seems to flash between original when painting
layer masks.
Once these bugs are flattened, this branch will be merged with
master and an update put onto the Barrow Repo.

Signed-off-by: Billy Barrow <billyb@pcthingz.com>

Billy Barrow há 8 anos atrás
pai
commit
bb1c2a0d61
5 ficheiros alterados com 479 adições e 75 exclusões
  1. 42 10
      PF2/Layer.py
  2. 82 0
      PF2/VectorMask/Path.py
  3. 55 0
      PF2/VectorMask/__init__.py
  4. 167 28
      PF2/__init__.py
  5. 133 37
      ui/PF2_Activity.glade

+ 42 - 10
PF2/Layer.py

@@ -1,15 +1,18 @@
 from gi.repository import Gtk, GLib
+import PF2.VectorMask as VectorMask
+import cv2
 
 class Layer:
     def __init__(self, base, name, on_change):
         print("init", name)
-        self.mask = []
+        self.mask = VectorMask.VectorMask()
         self.tools = []
         self.tool_map = {}
         self.name = name
         self.enabled = True
         self.selected_tool = 0
         self.editable = not base
+        self.opacity = 1.0
 
         self.selector_row = None
 
@@ -63,8 +66,9 @@ class Layer:
 
         layerDict["tools"] = toolDict
         layerDict["name"] = self.name
-        layerDict["mask"] = self.mask
+        layerDict["mask"] = self.mask.get_vector_mask_dict()
         layerDict["enabled"] = self.enabled
+        layerDict["opacity"] = self.opacity
 
         return layerDict
 
@@ -78,7 +82,7 @@ class Layer:
         # The base layer only has tools.
         if(self.editable):
             # Load Mask Vectors
-            self.mask = dict["mask"]
+            self.mask.set_from_vector_mask_dict(dict["mask"])
 
             # Load Layer Name
             self.name = dict["name"]
@@ -86,14 +90,18 @@ class Layer:
             # Load Enabled State
             self.enabled = dict["enabled"]
 
+            # Load Opacity Fraction
+            self.opacity = dict["opacity"]
+
 
     def render_layer(self, baseImage, image, callback=None):
-        # We are passed a base image (original)
-        # Make a copy of this to pass through the tools
-        layer = baseImage.copy()
+        # Only process if the layer is enabled
+        if(self.enabled and self.opacity != 0.0):
+            # We are passed a base image (original)
+            # Make a copy of this to pass through the tools
+            layer = baseImage.copy()
 
-        # Base layer needs no mask processing
-        if(not self.editable):
+            # Process the Layer
             ntools = len(self.tools)
             count = 1
             for tool in self.tools:
@@ -105,13 +113,26 @@ class Layer:
                 layer = tool.on_update(layer)
                 count += 1
 
-            image = layer
 
-        ## Here we would blend with the mask
+            # Base layer needs no mask processing
+            if(not self.editable):
+                image = layer
+
+            # Here we would blend with the mask
+            else:
+                mask_map = self.mask.get_mask_map()
+                height, width = layer.shape[:2]
+                mask_map = cv2.resize(mask_map, (width, height), interpolation=cv2.INTER_AREA)
+                mask_map = mask_map * self.opacity
+
+                inverted_map = (mask_map * -1) + 1
+                for i in range(0,3):
+                    image[0:, 0:, i] = (layer[0:, 0:, i] * mask_map) + (image[0:, 0:, i] * inverted_map)
 
         return image
 
 
+
     def show_all(self):
 
         self.tool_box.show_all()
@@ -126,3 +147,14 @@ class Layer:
         for tool in self.tools:
             tool.reset()
 
+
+    def set_size(self, width, height):
+        self.mask.set_dimentions(width, height)
+
+    def set_opacity(self, opacity):
+        self.opacity = opacity
+        self.on_tool_change(None, None)
+
+    def set_enabled(self, enabled):
+        self.enabled = enabled
+        self.on_tool_change(None, None)

+ 82 - 0
PF2/VectorMask/Path.py

@@ -0,0 +1,82 @@
+import cv2
+import numpy
+from gi.repository import Gdk
+
+
+class Path:
+    def __init__(self, brush_size, brush_feather, scale, additive):
+        self.points = []
+        self.brush_size = brush_size
+        self.scale = scale
+        self.brush_feather = brush_feather
+        self.additive = additive
+
+    def add_point(self, x, y, previewShape = None, fill = None):
+        self.points.append([x,y])
+        if(previewShape != None and fill != None and len(self.points) > 1):
+            preview = numpy.zeros(previewShape, dtype=numpy.uint8)
+            points = self.points[-2:]
+            print(points)
+            sx = points[0][0]
+            sy = points[0][1]
+            fx = points[1][0]
+            fy = points[1][1]
+            cv2.line(preview, (sx, sy), (fx, fy), (fill), int(self.brush_size), cv2.LINE_4)
+            return preview
+
+
+    def get_mask_map(self, mask):
+        if(self.additive):
+            return self.draw_additive_path(mask)
+        else:
+            return self.draw_subtractive_path(mask)
+
+    def draw_subtractive_path(self, mask):
+        self.draw_points(mask, self.points, 0)
+        return mask
+
+    def draw_additive_path(self, mask):
+        map = numpy.zeros(mask.shape, dtype=numpy.uint8)
+
+        self.draw_points(map, self.points, 255)
+
+        if(self.brush_feather > 1):
+            blur_size = 2 * round((round(self.brush_feather) + 1) / 2) - 1
+            map = cv2.GaussianBlur(map, (int(blur_size), int(blur_size)), 0)
+            map = map[:, :, numpy.newaxis]
+
+        # Painful workaround for int overflow
+        mask2 = mask.astype(numpy.uint64)
+        mask2 = mask2 + map
+        mask2[mask2 > 255] = 255
+        mask = mask2.astype(numpy.uint8)
+
+        return mask
+
+    def draw_points(self, mask, points, value):
+        for i in range(len(points) - 2):
+            sx = int(self.points[i][0] * self.scale)
+            sy = int(self.points[i][1] * self.scale)
+            fx = int(self.points[i + 1][0] * self.scale)
+            fy = int(self.points[i + 1][1] * self.scale)
+
+            # print("[PATH.PY]", sx, sy, fx, fy)
+            cv2.line(mask, (sx, sy), (fx, fy), (value), int(self.brush_size * self.scale), cv2.LINE_AA)
+
+        return mask
+
+
+    def get_path_dict(self):
+        return {
+            "brush_size": self.brush_size,
+            "scale": self.scale,
+            "brush_feather": self.brush_feather,
+            "additive": self.additive,
+            "points": self.points
+        }
+
+    @staticmethod
+    def new_from_dict(dict):
+        path = Path(dict["brush_size"], dict["brush_feather"], dict["scale"], dict["additive"])
+        path.points = dict["points"]
+        return path

+ 55 - 0
PF2/VectorMask/__init__.py

@@ -0,0 +1,55 @@
+import numpy
+from PF2.VectorMask import Path
+
+
+class VectorMask:
+    def __init__(self):
+        self.paths = []
+        self.width = 0
+        self.height = 0
+
+    def set_dimentions(self, width, height):
+        self.width = width
+        self.height = height
+
+    def get_new_path(self, brush_size, brush_feather, scale, additive):
+        path = Path.Path(brush_size, brush_feather, scale, additive)
+        self.paths.append(path)
+        return path
+
+    def get_mask_map(self):
+        map = numpy.zeros((self.height, self.width, 1), dtype=numpy.uint8)
+
+        print(map.shape)
+
+        for path in self.paths:
+            map = path.get_mask_map(map)
+
+        print(map.shape)
+
+        map32 = map.astype(numpy.float32)
+        map32 = map32 / 255.0
+
+        print(map32.shape)
+
+        return map32
+
+    def get_vector_mask_dict(self):
+        paths = []
+        for path in self.paths:
+            paths.append(path.get_path_dict())
+
+        return {
+            "width": self.width,
+            "height": self.height,
+            "paths": paths
+        }
+
+    def set_from_vector_mask_dict(self, dict):
+        self.width = dict["width"]
+        self.height = dict["height"]
+        paths = []
+        for path in dict["paths"]:
+            paths.append(Path.Path.new_from_dict(path))
+
+        self.paths = paths

+ 167 - 28
PF2/__init__.py

@@ -37,6 +37,7 @@ class PF2(Activity.Activity):
         # Get all UI Components
         self.ui = {}
         components = [
+            "main",
             "control_reveal",
             "histogram_reveal",
             "layers_reveal",
@@ -72,7 +73,12 @@ class PF2(Activity.Activity):
             "new_layer",
             "layer_mask_reveal",
             "add_layer_button",
-            "remove_layer_button"
+            "remove_layer_button",
+            "mask_brush_feather",
+            "mask_brush_feather_scale",
+            "edit_layer_mask_button",
+            "layer_opacity",
+            "layer_opacity_scale"
         ]
 
         for component in components:
@@ -109,6 +115,7 @@ class PF2(Activity.Activity):
         self.image_path = ""
         self.bit_depth = 8
         self.image = None
+        self.image_is_dirty = True
         self.original_image = None
         self.pwidth = 0
         self.pheight = 0
@@ -126,6 +133,10 @@ class PF2(Activity.Activity):
         self.undo_stack = []
         self.undo_position = 0
         self.undoing = False
+        self.current_layer_path = None
+        self.mousex = 0
+        self.mousey = 0
+        self.layer_order = []
 
         # Setup Open Dialog
         self.ui["open_window"].set_transient_for(self.root)
@@ -147,13 +158,18 @@ class PF2(Activity.Activity):
         self.ui["redo"].connect("clicked", self.on_redo)
         self.ui["reset"].connect("clicked", self.on_reset)
         self.ui["preview_eventbox"].connect('motion-notify-event', self.preview_dragged)
+        self.ui["preview_eventbox"].connect('button-press-event', self.new_path)
         self.ui["mask_draw_toggle"].connect("toggled", self.mask_draw_toggled)
         self.ui["mask_erase_toggle"].connect("toggled", self.mask_erase_toggled)
         self.ui["new_layer"].connect("clicked", self.new_layer_button_clicked)
         self.ui["layers_list"].connect("row-activated", self.layer_ui_activated)
         self.ui["add_layer_button"].connect("clicked", self.new_layer_button_clicked)
         self.ui["remove_layer_button"].connect("clicked", self.remove_layer_button_clicked)
-
+        self.ui["edit_layer_mask_button"].connect("toggled", self.edit_mask_toggled)
+        self.ui["layer_opacity"].connect("value-changed", self.update_layer_opacity)
+        self.ui["scroll_window"].connect_after("draw", self.draw_ui_brush_circle)
+        self.ui["scroll_window"].connect('motion-notify-event', self.mouse_coords_changed)
+        self.ui["mask_brush_size"].connect("value-changed", self.brush_size_changed)
 
 
 
@@ -402,6 +418,8 @@ class PF2(Activity.Activity):
         # Resize OPENCV Copy
         self.original_image = cv2.resize(self.original_image, (int(nw), int(nh)), interpolation = cv2.INTER_AREA)
 
+        self.image_is_dirty = True
+
         self.image = numpy.copy(self.original_image)
 
         # Update image
@@ -416,8 +434,10 @@ class PF2(Activity.Activity):
             time.sleep(0.5)
         self.additional_change_occurred = False
         GLib.idle_add(self.start_work)
+        self.image_is_dirty = True
         self.image = numpy.copy(self.original_image)
         self.image = self.run_stack(self.image)
+        self.image_is_dirty = False
         if(self.additional_change_occurred):
             self.update_image()
         else:
@@ -492,8 +512,10 @@ class PF2(Activity.Activity):
         f = open(path, "w")
 
         layerDict = {}
+        layerOrder = []
         for layer in self.layers:
             layerDict[layer.name] = layer.get_layer_dict()
+            layerOrder.append(layer.name)
 
         if(not self.undoing) and (self.has_loaded):
             if(len(self.undo_stack)-1 != self.undo_position):
@@ -503,10 +525,12 @@ class PF2(Activity.Activity):
                 self.undo_position = len(self.undo_stack)-1
                 GLib.idle_add(self.update_undo_state)
 
+
         data = {
             "path":self.image_path,
             "format-revision":1,
-            "layers": layerDict
+            "layers": layerDict,
+            "layer-order": layerOrder
         }
 
         f.write(str(data))
@@ -518,19 +542,21 @@ class PF2(Activity.Activity):
         if(os.path.exists(path)):
             f = open(path, 'r')
             sdata = f.read()
-            if(True):
-            #try:
+            try:
                 data = ast.literal_eval(sdata)
                 if(data["format-revision"] == 1):
-                    for layer in data["layers"]:
-                        GLib.idle_add(self.create_layer_with_data, layer, data["layers"][layer])
+                    if("layer-order" not in data):
+                        # Backwards compatability
+                        data["layer-order"] = ["base",]
+                    for layer_name in data["layer-order"]:
+                        GLib.idle_add(self.create_layer_with_data, layer_name, data["layers"][layer_name])
 
-                    self.undo_stack = [data["layers"],]
+                    self.undo_stack = [data,]
                     self.undo_position = 0
                     loadDefaults = False
-            #except:
-            #    GLib.idle_add(self.show_message,"Unable to load previous edits…",
-            #                                    "The edit file for this photo is corrupted and could not be loaded.")
+            except:
+                GLib.idle_add(self.show_message,"Unable to load previous edits…",
+                                                "The edit file for this photo is corrupted and could not be loaded.")
 
         if(loadDefaults):
             for layer in self.layers:
@@ -538,10 +564,13 @@ class PF2(Activity.Activity):
                     GLib.idle_add(tool.reset)
 
             layerDict = {}
+            layerOrder = []
             for layer in self.layers:
                 layerDict[layer.name] = layer.get_layer_dict()
+                layerOrder.append(layer.name)
 
-            self.undo_stack = [layerDict, ]
+
+            self.undo_stack = [{"layers": layerDict, "layer-order": layerOrder}, ]
             self.undo_position = 0
 
         GLib.idle_add(self.update_undo_state)
@@ -560,12 +589,14 @@ class PF2(Activity.Activity):
 
     def update_from_undo_stack(self, data):
         self.undoing = True
-        for layer in data:
-            if (layer == "base"):
-                self.base_layer.set_from_layer_dict(data[layer])
+        self.delete_all_editable_layers()
+        for layer_name in data["layer-order"]:
+            if (layer_name == "base"):
+                self.base_layer.set_from_layer_dict(data["layers"][layer_name])
             else:
-                ilayer = self.create_layer(layer, False)
-                ilayer.set_from_layer_dict(data[layer])
+                ilayer = self.create_layer(layer_name, False)
+                ilayer.set_from_layer_dict(data["layers"][layer_name])
+                self.show_layers()
 
 
     def get_export_image(self, w, h):
@@ -587,15 +618,77 @@ class PF2(Activity.Activity):
     ## Layers Stuff ##
 
     def preview_dragged(self, widget, event):
-        print(event.x, event.y)
+
+        x, y = widget.translate_coordinates(self.ui["preview"], event.x, event.y)
+
+        draw = self.ui["mask_draw_toggle"].get_active()
+        erase = self.ui["mask_erase_toggle"].get_active()
+        layer = self.get_selected_layer()
+        if((draw or erase) and layer.editable and self.current_layer_path):
+
+            if (x < 0.0):
+                x = 0
+            if (y < 0.0):
+                y = 0
+
+            pwidth = self.pimage.get_width()
+            pheight = self.pimage.get_height()
+
+            if (x > pwidth):
+                x = pwidth
+            if (y > pheight):
+                y = pheight
+
+            print(x, y)
+
+            if(not self.image_is_dirty):
+                fill = (0, 0, 255)
+                if(erase):
+                    fill = (255, 0, 0)
+
+                preview = self.current_layer_path.add_point(int(x), int(y), (pheight, pwidth, 3), fill)
+
+                # Bits per pixel
+                bpp = float(str(self.image.dtype).replace("uint", "").replace("float", ""))
+                # Pixel value range
+                np = float(2 ** bpp - 1)
+
+                self.image[preview == 255] = np
+                cv2.imwrite("/tmp/phf2-preview-%s-drag.png" % getpass.getuser(), self.image)
+                temppbuf = GdkPixbuf.Pixbuf.new_from_file("/tmp/phf2-preview-%s-drag.png" % getpass.getuser())
+                self.ui["preview"].set_from_pixbuf(temppbuf)
+
+            self.on_layer_change(layer)
+
+    def new_path(self, widget, event):
+        draw = self.ui["mask_draw_toggle"].get_active()
+        erase = self.ui["mask_erase_toggle"].get_active()
+        layer = self.get_selected_layer()
+        if((draw or erase) and layer.editable):
+            print(self.pimage.get_width(), self.pimage.get_height())
+            width = self.pimage.get_width()
+            size = self.ui["mask_brush_size"].get_value()
+            feather = self.ui["mask_brush_feather"].get_value()
+            self.current_layer_path = layer.mask.get_new_path(size, feather, float(self.awidth)/float(width), draw)
+
 
     def mask_draw_toggled(self, widget):
-        if(widget.get_active()):
-            self.ui["mask_erase_toggle"].set_active(False)
+        self.ui["mask_erase_toggle"].set_active(not widget.get_active())
 
     def mask_erase_toggled(self, widget):
-        if(widget.get_active()):
-            self.ui["mask_draw_toggle"].set_active(False)
+        self.ui["mask_draw_toggle"].set_active(not widget.get_active())
+        self.ui["mask_brush_feather_scale"].set_sensitive(not widget.get_active())
+
+
+    def edit_mask_toggled(self, widget):
+        self.ui["layer_mask_reveal"].set_reveal_child(widget.get_active())
+        self.ui["mask_draw_toggle"].set_active(widget.get_active())
+        self.ui["mask_erase_toggle"].set_active(False)
+        self.ui["scroll_window"].queue_draw()
+
+    def update_layer_opacity(self, sender):
+        layer = self.get_selected_layer()
+        layer.set_opacity(sender.get_value())
 
 
     def create_layer(self, layer_name, is_base):
@@ -624,7 +717,7 @@ class PF2(Activity.Activity):
         layer_toggle.set_margin_left(8)
         layer_toggle.set_margin_top(4)
         layer_toggle.set_margin_bottom(4)
-
+        layer_toggle.connect("toggled", self.toggle_layer, layer)
 
         layer_label = Gtk.Label()
         layer_label.set_label(layer.name)
@@ -651,8 +744,15 @@ class PF2(Activity.Activity):
         layer_index = row.get_index()
         self.ui["tool_stack"].set_visible_child(self.layers[layer_index].tool_stack)
         self.ui["tool_box_stack"].set_visible_child(self.layers[layer_index].tool_box)
-        self.ui["layer_mask_reveal"].set_reveal_child(self.layers[layer_index].editable)
         self.ui["remove_layer_button"].set_sensitive(self.layers[layer_index].editable)
+        self.ui["edit_layer_mask_button"].set_sensitive(self.layers[layer_index].editable)
+        self.ui["layer_opacity_scale"].set_sensitive(self.layers[layer_index].editable)
+        self.ui["layer_opacity"].set_value(self.layers[layer_index].opacity)
+        if(self.ui["edit_layer_mask_button"].get_active()):
+            self.ui["edit_layer_mask_button"].set_active(self.layers[layer_index].editable)
+
+    def toggle_layer(self, sender, layer):
+        layer.set_enabled(sender.get_active())
 
 
     def new_layer_button_clicked(self, widget):
@@ -667,9 +767,11 @@ class PF2(Activity.Activity):
 
         # Create the layer
         layer = self.create_layer(layer_name, False)
+        layer.set_size(self.awidth, self.aheight)
 
         # Save changes
         threading.Thread(target=self.save_image_data).start()
+        self.on_layer_change(layer)
 
 
     def remove_layer_button_clicked(self, widget):
@@ -690,11 +792,25 @@ class PF2(Activity.Activity):
         # Select next layer
         self.select_layer(self.layers[selected_index -1])
 
-        if(len(self.layers) == 1):
+        if (len(self.layers) == 1):
             self.ui["layers_reveal"].set_reveal_child(False)
 
-        # Save changes
-        threading.Thread(target=self.save_image_data).start()
+        if(widget != None):
+            # Only do this if the layer was actualy deleted by the user
+            # and not by the undo-redo system for example
+
+            # Save changes
+            threading.Thread(target=self.save_image_data).start()
+
+            self.on_layer_change(self.get_selected_layer())
+
+
+    def get_selected_layer(self):
+        layer_row = self.ui["layers_list"].get_selected_row()
+
+        for layer in self.layers:
+            if (layer.selector_row == layer_row):
+                return layer
 
     def layer_exists(self, layer_name):
         for layer in self.layers:
@@ -707,4 +823,27 @@ class PF2(Activity.Activity):
 
     def select_layer(self, layer):
         self.ui["layers_list"].select_row(layer.selector_row)
-        self.layer_ui_activated(self.ui["layers_list"], layer.selector_row)
+        self.layer_ui_activated(self.ui["layers_list"], layer.selector_row)
+
+    def delete_all_editable_layers(self):
+        count = len(self.layers) -1
+        while(len(self.layers) != 1):
+            self.select_layer(self.layers[1])
+            self.remove_layer_button_clicked(None)
+
+
+    def draw_ui_brush_circle(self, widget, context):
+        drawing = self.ui["edit_layer_mask_button"].get_active()
+        if(drawing):
+            size = self.ui["mask_brush_size"].get_value()
+            context.set_source_rgb(255, 255, 255)
+            context.arc(self.mousex, self.mousey, size/2.0, 0.0, 2 * numpy.pi);
+            context.stroke()
+
+    def mouse_coords_changed(self, widget, event):
+        self.mousex, self.mousey = event.x, event.y
+        print(event.x, event.y)
+        widget.queue_draw()
+
+    def brush_size_changed(self, sender):
+        self.ui["scroll_window"].queue_draw()

+ 133 - 37
ui/PF2_Activity.glade

@@ -94,9 +94,21 @@
       </object>
     </child>
   </object>
+  <object class="GtkAdjustment" id="PF2_layer_opacity">
+    <property name="upper">1</property>
+    <property name="value">1</property>
+    <property name="step_increment">0.10000000000000001</property>
+    <property name="page_increment">0.5</property>
+  </object>
+  <object class="GtkAdjustment" id="PF2_mask_brush_feather">
+    <property name="upper">100</property>
+    <property name="value">10</property>
+    <property name="step_increment">1</property>
+    <property name="page_increment">10</property>
+  </object>
   <object class="GtkAdjustment" id="PF2_mask_brush_size">
     <property name="lower">1</property>
-    <property name="upper">100</property>
+    <property name="upper">200</property>
     <property name="value">20</property>
     <property name="step_increment">1</property>
     <property name="page_increment">10</property>
@@ -126,8 +138,7 @@
               <object class="GtkEventBox" id="PF2_preview_eventbox">
                 <property name="visible">True</property>
                 <property name="can_focus">False</property>
-                <property name="halign">center</property>
-                <property name="valign">center</property>
+                <property name="above_child">True</property>
                 <child>
                   <object class="GtkImage" id="PF2_preview">
                     <property name="visible">True</property>
@@ -362,55 +373,115 @@
                               </packing>
                             </child>
                             <child>
-                              <object class="GtkButtonBox">
+                              <object class="GtkBox">
                                 <property name="visible">True</property>
                                 <property name="can_focus">False</property>
-                                <property name="spacing">6</property>
-                                <property name="layout_style">end</property>
+                                <property name="spacing">8</property>
                                 <child>
-                                  <object class="GtkButton" id="PF2_add_layer_button">
+                                  <object class="GtkLabel">
                                     <property name="visible">True</property>
-                                    <property name="can_focus">True</property>
-                                    <property name="receives_default">True</property>
-                                    <child>
-                                      <object class="GtkImage">
-                                        <property name="visible">True</property>
-                                        <property name="can_focus">False</property>
-                                        <property name="icon_name">list-add-symbolic</property>
-                                      </object>
-                                    </child>
+                                    <property name="can_focus">False</property>
+                                    <property name="label" translatable="yes">Opacity</property>
                                   </object>
                                   <packing>
                                     <property name="expand">False</property>
-                                    <property name="fill">False</property>
+                                    <property name="fill">True</property>
                                     <property name="position">0</property>
-                                    <property name="non_homogeneous">True</property>
                                   </packing>
                                 </child>
                                 <child>
-                                  <object class="GtkButton" id="PF2_remove_layer_button">
+                                  <object class="GtkScale" id="PF2_layer_opacity_scale">
                                     <property name="visible">True</property>
                                     <property name="can_focus">True</property>
-                                    <property name="receives_default">True</property>
+                                    <property name="adjustment">PF2_layer_opacity</property>
+                                    <property name="round_digits">1</property>
+                                    <property name="digits">2</property>
+                                    <property name="value_pos">right</property>
+                                  </object>
+                                  <packing>
+                                    <property name="expand">True</property>
+                                    <property name="fill">True</property>
+                                    <property name="position">1</property>
+                                  </packing>
+                                </child>
+                                <child>
+                                  <object class="GtkButtonBox">
+                                    <property name="visible">True</property>
+                                    <property name="can_focus">False</property>
+                                    <property name="margin_left">10</property>
+                                    <property name="spacing">6</property>
+                                    <property name="layout_style">end</property>
                                     <child>
-                                      <object class="GtkImage">
+                                      <object class="GtkToggleButton" id="PF2_edit_layer_mask_button">
                                         <property name="visible">True</property>
-                                        <property name="can_focus">False</property>
-                                        <property name="icon_name">list-remove-symbolic</property>
+                                        <property name="can_focus">True</property>
+                                        <property name="receives_default">True</property>
+                                        <child>
+                                          <object class="GtkImage">
+                                            <property name="visible">True</property>
+                                            <property name="can_focus">False</property>
+                                            <property name="icon_name">document-edit-symbolic</property>
+                                          </object>
+                                        </child>
                                       </object>
+                                      <packing>
+                                        <property name="expand">True</property>
+                                        <property name="fill">True</property>
+                                        <property name="position">0</property>
+                                        <property name="non_homogeneous">True</property>
+                                      </packing>
+                                    </child>
+                                    <child>
+                                      <object class="GtkButton" id="PF2_add_layer_button">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">True</property>
+                                        <property name="receives_default">True</property>
+                                        <child>
+                                          <object class="GtkImage">
+                                            <property name="visible">True</property>
+                                            <property name="can_focus">False</property>
+                                            <property name="icon_name">list-add-symbolic</property>
+                                          </object>
+                                        </child>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">False</property>
+                                        <property name="fill">False</property>
+                                        <property name="position">1</property>
+                                        <property name="non_homogeneous">True</property>
+                                      </packing>
+                                    </child>
+                                    <child>
+                                      <object class="GtkButton" id="PF2_remove_layer_button">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">True</property>
+                                        <property name="receives_default">True</property>
+                                        <child>
+                                          <object class="GtkImage">
+                                            <property name="visible">True</property>
+                                            <property name="can_focus">False</property>
+                                            <property name="icon_name">list-remove-symbolic</property>
+                                          </object>
+                                        </child>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">False</property>
+                                        <property name="fill">False</property>
+                                        <property name="position">2</property>
+                                        <property name="non_homogeneous">True</property>
+                                      </packing>
                                     </child>
                                   </object>
                                   <packing>
                                     <property name="expand">False</property>
                                     <property name="fill">False</property>
-                                    <property name="position">1</property>
-                                    <property name="non_homogeneous">True</property>
+                                    <property name="position">2</property>
                                   </packing>
                                 </child>
                               </object>
                               <packing>
                                 <property name="expand">False</property>
-                                <property name="fill">False</property>
+                                <property name="fill">True</property>
                                 <property name="position">2</property>
                               </packing>
                             </child>
@@ -507,37 +578,61 @@
                                   </packing>
                                 </child>
                                 <child>
-                                  <object class="GtkBox">
+                                  <object class="GtkGrid">
                                     <property name="visible">True</property>
                                     <property name="can_focus">False</property>
-                                    <property name="margin_left">8</property>
-                                    <property name="margin_right">8</property>
                                     <property name="margin_bottom">8</property>
+                                    <property name="column_spacing">8</property>
+                                    <child>
+                                      <object class="GtkScale">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">True</property>
+                                        <property name="hexpand">True</property>
+                                        <property name="adjustment">PF2_mask_brush_size</property>
+                                        <property name="round_digits">1</property>
+                                        <property name="value_pos">right</property>
+                                      </object>
+                                      <packing>
+                                        <property name="left_attach">1</property>
+                                        <property name="top_attach">0</property>
+                                      </packing>
+                                    </child>
                                     <child>
                                       <object class="GtkLabel">
                                         <property name="visible">True</property>
                                         <property name="can_focus">False</property>
+                                        <property name="halign">end</property>
                                         <property name="label" translatable="yes">Brush Size</property>
                                       </object>
                                       <packing>
-                                        <property name="expand">False</property>
-                                        <property name="fill">True</property>
-                                        <property name="position">0</property>
+                                        <property name="left_attach">0</property>
+                                        <property name="top_attach">0</property>
                                       </packing>
                                     </child>
                                     <child>
-                                      <object class="GtkScale">
+                                      <object class="GtkLabel">
+                                        <property name="visible">True</property>
+                                        <property name="can_focus">False</property>
+                                        <property name="halign">end</property>
+                                        <property name="label" translatable="yes">Brush Feather</property>
+                                      </object>
+                                      <packing>
+                                        <property name="left_attach">0</property>
+                                        <property name="top_attach">1</property>
+                                      </packing>
+                                    </child>
+                                    <child>
+                                      <object class="GtkScale" id="PF2_mask_brush_feather_scale">
                                         <property name="visible">True</property>
                                         <property name="can_focus">True</property>
                                         <property name="hexpand">True</property>
-                                        <property name="adjustment">PF2_mask_brush_size</property>
+                                        <property name="adjustment">PF2_mask_brush_feather</property>
                                         <property name="round_digits">1</property>
                                         <property name="value_pos">right</property>
                                       </object>
                                       <packing>
-                                        <property name="expand">False</property>
-                                        <property name="fill">True</property>
-                                        <property name="position">1</property>
+                                        <property name="left_attach">1</property>
+                                        <property name="top_attach">1</property>
                                       </packing>
                                     </child>
                                   </object>
@@ -829,6 +924,7 @@
       </object>
       <packing>
         <property name="submenu">main</property>
+        <property name="position">1</property>
       </packing>
     </child>
   </object>