Repository: Capsize-Games/airunner (language: python)

Issue:
Improve the eraser
make eraser work with rasterized lines
error when exporting JPG files
error encountered when exporting to jpg format:

```
OSError: cannot write mode RGBA as JPEG
```

convert image to RGB prior to exporting JPEG.


Discussion / hints:

Fix patch:
diff --git a/src/airunner/canvas.py b/src/airunner/canvas.py
index 6197fbbb2..e905a8e24 100644
--- a/src/airunner/canvas.py
+++ b/src/airunner/canvas.py
@@ -24,6 +24,7 @@ class Canvas(
     saving = False
     select_start = None
     select_end = None
+    shift_is_pressed = False
 
     @property
     def current_layer(self):
@@ -108,6 +109,12 @@ def __init__(
         self.canvas_container.mouseMoveEvent = self.mouse_move_event
         self.canvas_container.mouseReleaseEvent = self.mouse_release_event
 
+        # on shift down
+        self.parent.window.keyPressEvent = self.key_press_event
+
+        # on key up
+        self.parent.window.keyReleaseEvent = self.key_release_event
+
         # on mouse hover
         self.canvas_container.enterEvent = self.enter_event
         self.canvas_container.leaveEvent = self.leave_event
@@ -121,77 +128,13 @@ def __init__(
 
         self.set_canvas_color()
 
-    def handle_outpaint(self, outpaint_box_rect, outpainted_image, action):
-        if len(self.current_layer.images) == 0:
-            point = QPoint(outpaint_box_rect.x(), outpaint_box_rect.y())
-            return outpainted_image, self.image_root_point, point
-
-        # make a copy of the current canvas image
-        existing_image_copy = self.current_layer.images[0].image.copy()
-        width = existing_image_copy.width
-        height = existing_image_copy.height
-        working_width = self.settings_manager.settings.working_width.get()
-        working_height = self.settings_manager.settings.working_height.get()
-
-        is_drawing_left = outpaint_box_rect.x() < self.image_pivot_point.x()
-        is_drawing_up = outpaint_box_rect.y() < self.image_pivot_point.y()
-
-        if is_drawing_left:
-            # get the x overlap of the outpaint box and the image
-            x_overlap = min(width, outpaint_box_rect.width()) - max(0, outpaint_box_rect.x())
-        else:
-            # get the x overlap of the outpaint box and the image
-            x_overlap = min(width, outpaint_box_rect.width()) - max(0, outpaint_box_rect.x() - self.image_pivot_point.x())
-
-        if is_drawing_up:
-            # get the y overlap of the outpaint box and the image
-            y_overlap = min(height, outpaint_box_rect.height()) - max(0, outpaint_box_rect.y())
-        else:
-            # get the y overlap of the outpaint box and the image
-            y_overlap = min(height, outpaint_box_rect.height()) - max(0, outpaint_box_rect.y() - self.image_pivot_point.y())
-
-        # get the x and y overlap of the outpaint box and the image
-        new_dimensions = (int(width + working_width - x_overlap), int(height + working_height - y_overlap))
-        if new_dimensions[0] < width:
-            new_dimensions = (width, new_dimensions[1])
-        if new_dimensions[1] < height:
-            new_dimensions = (new_dimensions[0], height)
-        new_image = Image.new("RGBA", new_dimensions, (0, 0, 0, 0))
-        new_image_a = Image.new("RGBA", new_dimensions, (0, 0, 0, 0))
-        new_image_b = Image.new("RGBA", new_dimensions, (0, 0, 0, 0))
-        existing_image_pos = [0, 0]
-        image_root_point = QPoint(self.image_root_point.x(), self.image_root_point.y())
-        image_pivot_point = QPoint(self.image_pivot_point.x(), self.image_pivot_point.y())
-        if is_drawing_left:
-            current_x_pos = abs(outpaint_box_rect.x() - image_pivot_point.x())
-            left_overlap = abs(outpaint_box_rect.x()) - abs(image_root_point.x())
-            image_root_point.setX(width + left_overlap)
-            image_pivot_point.setX(int(outpaint_box_rect.x()))
-            existing_image_pos = [current_x_pos, existing_image_pos[1]]
-            pos_x = max(0, outpaint_box_rect.x() + self.image_pivot_point.x())
-        else:
-            pos_x = max(0, outpaint_box_rect.x() - self.image_pivot_point.x())
-        if is_drawing_up:
-            current_y_pos = abs(outpaint_box_rect.y() - image_pivot_point.y())
-            up_overlap = abs(outpaint_box_rect.y()) - abs(image_root_point.y())
-            image_root_point.setY(height + up_overlap)
-            image_pivot_point.setY(int(outpaint_box_rect.y()))
-            existing_image_pos = [existing_image_pos[0], current_y_pos]
-            pos_y = max(0, outpaint_box_rect.y() + self.image_pivot_point.y())
-        else:
-            pos_y = max(0, outpaint_box_rect.y() - self.image_pivot_point.y())
-
-        new_image_a.paste(outpainted_image, (int(pos_x), int(pos_y)))
-        new_image_b.paste(existing_image_copy, (int(existing_image_pos[0]), int(existing_image_pos[1])))
+    def key_press_event(self, event):
+        if event.key() == Qt.Key.Key_Shift:
+            self.shift_is_pressed = True
 
-        if action == "outpaint":
-            new_image = Image.alpha_composite(new_image, new_image_a)
-            new_image = Image.alpha_composite(new_image, new_image_b)
-        else:
-            new_image = Image.alpha_composite(new_image, new_image_b)
-            new_image = Image.alpha_composite(new_image, new_image_a)
-
-        return new_image, image_root_point, image_pivot_point
+    def key_release_event(self, event):
+        if event.key() == Qt.Key.Key_Shift:
+            self.shift_is_pressed = False
 
     def set_canvas_color(self):
         self.canvas_container.setStyleSheet(f"background-color: {self.settings_manager.settings.canvas_color.get()};")
@@ -245,6 +188,7 @@ def handle_move_canvas(self, event):
         self.pos_y += event.pos().y() - self.drag_pos.y()
         self.drag_pos = event.pos()
         self.update()
+        self.parent.window.canvas_position.setText(f"X: {self.pos_x}, Y: {self.pos_y}")
 
     def handle_move_layer(self, event):
         point = QPoint(
@@ -277,6 +221,10 @@ def handle_move_layer(self, event):
 
         self.layers[self.current_layer_index].offset = point
 
+    left_mouse_button_down = False
+    right_mouse_button_down = False
+    brush_start = None
+
     def mouse_press_event(self, event):
         if event.button() == Qt.MouseButton.LeftButton:
             self.select_start = event.pos()
@@ -287,7 +235,8 @@ def mouse_press_event(self, event):
                     "layer_index": self.current_layer_index,
                     "lines": self.current_layer.lines.copy()
                 })
-                self.start_drawing_line_index = len(self.current_layer.lines)
+                if not self.left_mouse_button_down and not self.right_mouse_button_down:
+                    self.start_drawing_line_index = len(self.current_layer.lines)
                 start = event.pos() - QPoint(self.pos_x, self.pos_y)
                 end = event.pos() - QPoint(self.pos_x, self.pos_y)
                 pen = self.pen(event)
@@ -300,15 +249,27 @@ def mouse_press_event(self, event):
                 start += self.layers[self.current_layer_index].offset
                 end += self.layers[self.current_layer_index].offset
                 self.current_layer.lines += [line]
+
+                if event.button() == Qt.MouseButton.LeftButton and not self.left_mouse_button_down:
+                    self.right_mouse_button_down = False
+                    self.left_mouse_button_down = True
+                    self.brush_start = start
+                elif event.button() == Qt.MouseButton.RightButton and not self.right_mouse_button_down:
+                    self.left_mouse_button_down = False
+                    self.right_mouse_button_down = True
+                    self.brush_start = start
+
             self.handle_tool(event)
             self.update()
         elif event.button() == Qt.MouseButton.MiddleButton:
             # Start dragging the canvas when the middle or right mouse button is pressed
             self.drag_pos = event.pos()
 
+    last_mouse_pos = None
     def mouse_move_event(self, event):
         # check if LeftButton is pressed
         if Qt.MouseButton.LeftButton in event.buttons() or Qt.MouseButton.RightButton in event.buttons():
+            self.last_mouse_pos = event.pos()
             self.handle_tool(event)
             self.update()
         elif self.drag_pos is not None:
@@ -316,6 +277,9 @@ def mouse_move_event(self, event):
 
     def mouse_release_event(self, event):
         if event.button() in (Qt.MouseButton.LeftButton, Qt.MouseButton.RightButton):
+            self.left_mouse_button_down = False
+            self.right_mouse_button_down = False
+            self.start_drawing_line_index = None
             if self.brush_selected:
                 self.stop_drawing_line_index = len(self.current_layer.lines)
                 self.update()
@@ -325,6 +289,16 @@ def mouse_release_event(self, event):
             # Start dragging the canvas when the middle or right mouse button is pressed
             self.drag_pos = event.pos()
 
+        # self.parent.history.add_event({
+        #     "event": "set_image",
+        #     "layer_index": self.current_layer_index,
+        #     "images": self.current_layer.images,
+        #     "previous_image_root_point": self.image_root_point,
+        #     "previous_image_pivot_point": self.image_pivot_point,
+        # })
+        # self.rasterize_lines()
+
+
     def handle_select(self, event):
         if self.select_selected:
             if self.select_start is None:
diff --git a/src/airunner/extensions.py b/src/airunner/extensions.py
index 1e8b4a386..917def324 100644
--- a/src/airunner/extensions.py
+++ b/src/airunner/extensions.py
@@ -1,5 +1,6 @@
 import os
 from PyQt6 import uic
+from PyQt6.uic.exceptions import UIFileException
 
 
 class BaseExtension:
@@ -8,7 +9,8 @@ class BaseExtension:
     """
     extension_directory = None
 
-    def __init__(self, settings_manager=None):
+    def __init__(self, app, settings_manager=None):
+        self.app = app
         self.settings_manager = settings_manager
 
     @property
@@ -24,7 +26,13 @@ def save_settings(self):
 
     def load_template(self, template_name):
         path = self.model_base_path
-        return uic.loadUi(os.path.join(path, "extensions", self.extension_directory, "templates", f"{template_name}.ui"))
+        extensions_path = self.settings_manager.settings.extensions_path.get() or "extensions"
+        if extensions_path == "extensions":
+            extensions_path = os.path.join(path, extensions_path)
+        try:
+            return uic.loadUi(os.path.join(extensions_path, self.extension_directory, "templates", f"{template_name}.ui"))
+        except UIFileException:
+            return None
 
     def generator_tab_injection(self, tab, name=None):
         """
@@ -35,3 +43,11 @@ def generator_tab_injection(self, tab, name=None):
         :return:
         """
         pass
+
+    def preferences_injection(self, window):
+        """
+        Override this method to inject a widget into the preferences window.
+        :param preferences:
+        :return:
+        """
+        pass
diff --git a/src/airunner/filters.py b/src/airunner/filters.py
deleted file mode 100644
index d6c3c865c..000000000
--- a/src/airunner/filters.py
+++ /dev/null
@@ -1,443 +0,0 @@
-import os
-
-from PIL import ImageEnhance, Image, ImageFilter
-from PIL.ImageFilter import GaussianBlur, BoxBlur, UnsharpMask, Filter
-from PyQt6 import uic
-from aihandler.qtvar import FloatVar, IntVar
-
-
-class ColorBalanceFilter(Filter):
-    name = "Color Balance"
-
-    def __init__(self, cyan_red=0, magenta_green=0, yellow_blue=0):
-        self.cyan_red = cyan_red
-        self.magenta_green = magenta_green
-        self.yellow_blue = yellow_blue
-
-    def filter(self, image):
-        # Apply enhancement
-        image = ImageEnhance.Color(image).enhance(1.0 + self.cyan_red)
-        image = ImageEnhance.Color(image).enhance(1.0 + self.magenta_green)
-        image = ImageEnhance.Color(image).enhance(1.0 + self.yellow_blue)
-        return image
-
-
-class SaturationFilter(Filter):
-    name = "Saturation"
-
-    def __init__(self, factor=1.0):
-        self.factor = factor
-
-    def filter(self, image):
-        return ImageEnhance.Color(image).enhance(1.0 + self.factor)
-
-
-class FilterBase:
-    ui_name = ""
-    window_title = ""
-
-    @property
-    def filter(self):
-        return None
-
-    def update_canvas(self):
-        self.canvas.update()
-
-    def __init__(self, parent):
-        self.filter_window = None
-        self.parent = parent
-        self.canvas = parent.canvas
-
-    def show(self):
-        self.filter_window = uic.loadUi(os.path.join(f"pyqt/{self.ui_name}.ui"))
-        self.filter_window.setWindowTitle(self.window_title)
-
-    def cancel_filter(self):
-        self.filter_window.close()
-        self.parent.current_filter = None
-        self.update_canvas()
-
-    def apply_filter(self):
-        self.canvas.apply_filter()
-        self.filter_window.close()
-        self.update_canvas()
-
-
-class BlurFilter(FilterBase):
-    ui_name = ""
-    window_title = ""
-
-    def __init__(self, parent):
-        super().__init__(parent)
-        self.blur_radius = parent.settings_manager.settings.blur_radius
-        self.default_value = 0.0
-
-
-    def show(self):
-        super().show()
-        self.blur_radius.set(self.default_value)
-
-        self.parent.current_filter = self.filter
-
-        blur_radius = self.blur_radius.get()
-
-        # set the gaussian_blur_window settings values to the current settings
-        self.filter_window.blur_slider.setValue(int(blur_radius))
-        self.filter_window.blur_slider.valueChanged.connect(
-            lambda val: self.handle_blur_radius_slider_change(val))
-        self.filter_window.blur_spinbox.setValue(blur_radius)
-        self.filter_window.blur_spinbox.valueChanged.connect(
-            lambda val: self.handle_blur_radius_spinbox_change(val))
-
-        # on ok button click, apply the filter
-        self.filter_window.buttonBox.rejected.connect(self.cancel_filter)
-        self.filter_window.buttonBox.accepted.connect(self.apply_filter)
-
-        self.filter_window.exec()
-
-    def handle_blur_radius_slider_change(self, val):
-        self.blur_radius.set(float(val))
-        self.filter_window.blur_spinbox.setValue(float(val))
-        self.parent.current_filter = self.filter
-        self.canvas.update()
-
-    def handle_blur_radius_spinbox_change(self, val):
-        self.blur_radius.set(val)
-        self.filter_window.blur_slider.setValue(int(val))
-        self.parent.current_filter = self.filter
-        self.update_canvas()
-
-
-class FilterGaussianBlur(BlurFilter):
-    ui_name = "gaussian_blur_window"
-    window_title = "Gaussian Blur"
-    default_value = 0.5
-
-    @property
-    def filter(self):
-        return GaussianBlur(radius=self.blur_radius.get())
-
-
-class FilterBoxBlur(BlurFilter):
-    ui_name = "box_blur_window"
-    window_title = "Box Blur"
-    default_value = 0.5
-
-    @property
-    def filter(self):
-        return BoxBlur(radius=self.blur_radius.get())
-
-
-class FilterPixelArt(FilterBase):
-    ui_name = "pixel_art"
-    window_title = "Pixel Art"
-    number_of_colors = 24
-    smoothing = 1
-    base_size = 256
-
-    @property
-    def filter(self):
-        return PixelFilter(
-            number_of_colors=self.number_of_colors,
-            smoothing=self.smoothing,
-            base_size=self.base_size
-        )
-
-    def show(self):
-        super().show()
-        self.parent.current_filter = self.filter
-
-        # set the gaussian_blur_window settings values to the current settings
-        self.filter_window.number_of_colors_slider.setValue(self.number_of_colors)
-        self.filter_window.number_of_colors_spinbox.setValue(self.number_of_colors)
-        self.filter_window.number_of_colors_slider.valueChanged.connect(
-            lambda val: self.handle_number_of_colors_change_slider(val))
-        self.filter_window.number_of_colors_spinbox.valueChanged.connect(
-            lambda val: self.handle_number_of_colors_change_spinbox(val))
-
-        # ensure base_size is multiple of 16
-        self.filter_window.base_size_slider.setValue(self.base_size)
-        self.filter_window.base_size_spinbox.setValue(self.base_size)
-        self.filter_window.base_size_slider.valueChanged.connect(
-            lambda val: self.handle_base_size_change_slider(val))
-        self.filter_window.base_size_spinbox.valueChanged.connect(
-            lambda val: self.handle_base_size_change_spinbox(val))
-
-        # on ok button click, apply the filter
-        self.filter_window.buttonBox.rejected.connect(self.cancel_filter)
-        self.filter_window.buttonBox.accepted.connect(self.apply_filter)
-
-        # apply the filter
-        self.parent.current_filter = self.filter
-        self.canvas.apply_filter()
-        self.update_canvas()
-
-        self.filter_window.exec()
-
-    def handle_number_of_colors_change_slider(self, val):
-        self.handle_number_of_colors_change(val)
-        self.filter_window.number_of_colors_spinbox.setValue(val)
-
-    def handle_number_of_colors_change_spinbox(self, val):
-        self.handle_number_of_colors_change(val)
-        self.filter_window.number_of_colors_slider.setValue(val)
-
-    def handle_number_of_colors_change(self, val):
-        self.number_of_colors = val
-        self.parent.current_filter = self.filter
-        self.canvas.update()
-
-    def handle_base_size_change_slider(self, val):
-        val = val - (val % 16)
-        self.handle_base_size_change(val)
-        self.filter_window.base_size_spinbox.setValue(val)
-
-    def handle_base_size_change_spinbox(self, val):
-        val = val - (val % 16)
-        self.handle_base_size_change(val)
-        self.filter_window.base_size_slider.setValue(val)
-
-    def handle_base_size_change(self, val):
-        self.base_size = val
-        self.parent.current_filter = self.filter
-        self.canvas.update()
-
-
-class PixelFilter(ImageFilter.Filter):
-    name = "Resize Filter"
-
-    def __init__(self, number_of_colors=24, smoothing=1, base_size=16):
-        self.number_of_colors = number_of_colors
-        self.smoothing = smoothing
-        self.base_size = base_size
-
-    def filter(self, image):
-        # Downsize while maintaining aspect ratio
-        width, height = image.size
-        scale = min(self.base_size / width, self.base_size / height)
-        new_width = int(width * scale)
-        new_height = int(height * scale)
-        downsized = image.resize((new_width, new_height), Image.NEAREST)
-
-        # Upscale back to original dimensions
-        target_width = int(new_width / scale)
-        target_height = int(new_height / scale)
-        upscaled = downsized.resize((target_width, target_height), Image.NEAREST)
-
-        # Reduce number of colors
-        quantized = upscaled.quantize(self.number_of_colors)
-        quantized = quantized.convert("L")
-        return quantized
-
-
-class FilterUnsharpMask(FilterBase):
-    ui_name = "unsharp_mask_window"
-    window_title = "Unsharp Mask"
-    unsharp_radius = 0.5
-    unsharp_percent = 0.5
-    unsharp_threshold = 0.5
-
-    @property
-    def filter(self):
-        return UnsharpMask(
-            radius=self.unsharp_radius,
-            percent=int(self.unsharp_percent),
-            threshold=int(self.unsharp_threshold)
-        )
-
-    def show(self):
-        super().show()
-
-        self.unsharp_radius = 0.5
-        self.unsharp_percent = 0.5
-        self.unsharp_threshold = 0.5
-
-        def update_filter():
-            self.parent.current_filter = self.filter
-            self.canvas.update()
-
-        def handle_unsharp_radius_slider_change(val):
-            self.unsharp_radius = val
-            self.filter_window.radius_spinbox.setValue(val)
-            self.filter_window.radius_spinbox.update()
-            self.filter_window.radius_slider.update()
-            update_filter()
-
-        def handle_unsharp_radius_spinbox_change(val):
-            self.unsharp_radius = val
-            self.filter_window.radius_slider.setValue(int(val))
-            self.filter_window.radius_spinbox.update()
-            self.filter_window.radius_slider.update()
-            update_filter()
-
-        def handle_unsharp_percent_slider_change(val):
-            self.unsharp_percent = val
-            self.filter_window.percent_spinbox.setValue(val)
-            self.filter_window.percent_spinbox.update()
-            self.filter_window.percent_slider.update()
-            update_filter()
-
-        def handle_unsharp_percent_spinbox_change(val):
-            self.unsharp_percent = val
-            self.filter_window.percent_slider.setValue(int(val))
-            self.filter_window.percent_spinbox.update()
-            self.filter_window.percent_slider.update()
-            update_filter()
-
-        def handle_unsharp_threshold_slider_change(val):
-            self.unsharp_threshold = val
-            self.filter_window.threshold_spinbox.setValue(val)
-            self.filter_window.threshold_spinbox.update()
-            self.filter_window.threshold_slider.update()
-            update_filter()
-
-        def handle_unsharp_threshold_spinbox_change(val):
-            self.unsharp_threshold = val
-            self.filter_window.threshold_slider.setValue(int(val))
-            self.filter_window.threshold_spinbox.update()
-            self.filter_window.threshold_slider.update()
-            update_filter()
-
-        # set the gaussian_blur_window settings values to the current settings
-        self.filter_window.radius_slider.setValue(int(self.unsharp_radius))
-        self.filter_window.radius_slider.valueChanged.connect(lambda val: handle_unsharp_radius_slider_change(val))
-        self.filter_window.radius_spinbox.setValue(self.unsharp_radius)
-        self.filter_window.radius_spinbox.valueChanged.connect(lambda val: handle_unsharp_radius_spinbox_change(val))
-
-        self.filter_window.percent_slider.setValue(int(self.unsharp_percent))
-        self.filter_window.percent_slider.valueChanged.connect(lambda val: handle_unsharp_percent_slider_change(val))
-        self.filter_window.percent_spinbox.setValue(self.unsharp_percent)
-        self.filter_window.percent_spinbox.valueChanged.connect(lambda val: handle_unsharp_percent_spinbox_change(val))
-
-        self.filter_window.threshold_slider.setValue(int(self.unsharp_threshold))
-        self.filter_window.threshold_slider.valueChanged.connect(lambda val: handle_unsharp_threshold_slider_change(val))
-        self.filter_window.threshold_spinbox.setValue(self.unsharp_threshold)
-        self.filter_window.threshold_spinbox.valueChanged.connect(lambda val: handle_unsharp_threshold_spinbox_change(val))
-        self.filter_window = self.filter_window
-        # on ok button click, apply the filter
-        self.filter_window.buttonBox.rejected.connect(self.cancel_filter)
-        self.filter_window.buttonBox.accepted.connect(self.apply_filter)
-
-        self.parent.current_filter = self.filter
-
-        self.filter_window.exec()
-
-
-class FilterSaturation(FilterBase):
-    ui_name = "saturation_window"
-    window_title = "Saturation"
-    factor = 0
-
-    @property
-    def filter(self):
-        return SaturationFilter(factor=self.factor / 1000)
-
-    def show(self):
-        super().show()
-        # set the gaussian_blur_window settings values to the current settings
-        self.filter_window.blur_slider.setValue(int(self.factor))
-        self.filter_window.blur_slider.valueChanged.connect(
-            lambda val: self.handle_blur_radius_slider_change(val))
-        self.filter_window.blur_spinbox.setValue(self.factor / 1000.0)
-        self.filter_window.blur_spinbox.valueChanged.connect(
-            lambda val: self.handle_blur_radius_spinbox_change(val))
-
-        # on ok button click, apply the filter
-        self.filter_window.buttonBox.rejected.connect(self.cancel_filter)
-        self.filter_window.buttonBox.accepted.connect(self.apply_filter)
-
-        self.filter_window.exec()
-
-    def handle_blur_radius_slider_change(self, val):
-        print(val)
-        self.factor = float(val)
-        self.filter_window.blur_spinbox.setValue(val / 1000.0)
-        self.parent.current_filter = self.filter
-        self.canvas.update()
-
-    def handle_blur_radius_spinbox_change(self, val):
-        self.factor = val / 100.0
-        self.filter_window.blur_slider.setValue(int(val * 1000.0))
-        self.parent.current_filter = self.filter
-        self.update_canvas()
-
-
-class FilterColorBalance(FilterBase):
-    ui_name = "color_balance"
-    window_title = "Color Balance"
-
-    def __init__(self, parent):
-        super().__init__(parent)
-        self.cyan_red = parent.settings_manager.settings.cyan_red
-        self.magenta_green = parent.settings_manager.settings.magenta_green
-        self.yellow_blue = parent.settings_manager.settings.yellow_blue
-
-    @property
-    def filter(self):
-        return ColorBalanceFilter(
-            cyan_red=self.cyan_red.get() / 1000,
-            magenta_green=self.magenta_green.get() / 1000,
-            yellow_blue=self.yellow_blue.get() / 1000
-        )
-
-    def show(self):
-        super().show()
-
-        self.cyan_red.set(0)
-        self.magenta_green.set(0)
-        self.yellow_blue.set(0)
-
-        def update_filter():
-            self.parent.current_filter = self.filter
-            self.canvas.update()
-
-        def color_balance_cyan_slider_change(val):
-            self.filter_window.cyan_spinbox.setValue(val / 1000.0)
-            self.filter_window.cyan_spinbox.update()
-            update_filter()
-
-        def color_balance_magenta_slider_change(val):
-            self.filter_window.magenta_spinbox.setValue(val / 1000.0)
-            self.filter_window.magenta_spinbox.update()
-            update_filter()
-
-        def color_balance_yellow_slider_change(val):
-            self.filter_window.yellow_spinbox.setValue(val / 1000.0)
-            self.filter_window.yellow_spinbox.update()
-            update_filter()
-
-        def color_balance_cyan_spinbox_change(val):
-            self.filter_window.cyan_slider.setValue(int(val * 1000.0))
-            self.filter_window.cyan_slider.update()
-            update_filter()
-
-        def color_balance_magenta_spinbox_change(val):
-            self.filter_window.magenta_slider.setValue(int(val * 1000.0))
-            self.filter_window.magenta_slider.update()
-            update_filter()
-
-        def color_balance_yellow_spinbox_change(val):
-            self.filter_window.yellow_slider.setValue(int(val * 1000.0))
-            self.filter_window.yellow_slider.update()
-            update_filter()
-
-        self.filter_window.cyan_slider.setValue(self.cyan_red.get())
-        self.filter_window.cyan_slider.valueChanged.connect(lambda val: color_balance_cyan_slider_change(val))
-        self.filter_window.cyan_spinbox.setValue(self.cyan_red.get() / 1000.0)
-        self.filter_window.cyan_spinbox.valueChanged.connect(lambda val: color_balance_cyan_spinbox_change(val))
-
-        self.filter_window.magenta_slider.setValue(self.magenta_green.get())
-        self.filter_window.magenta_slider.valueChanged.connect(lambda val: color_balance_magenta_slider_change(val))
-        self.filter_window.magenta_spinbox.setValue(self.magenta_green.get() / 1000.0)
-        self.filter_window.magenta_spinbox.valueChanged.connect(lambda val: color_balance_magenta_spinbox_change(val))
-
-        self.filter_window.yellow_slider.setValue(self.yellow_blue.get())
-        self.filter_window.yellow_slider.valueChanged.connect(lambda val: color_balance_yellow_slider_change(val))
-        self.filter_window.yellow_spinbox.setValue(self.yellow_blue.get() / 1000.0)
-        self.filter_window.yellow_spinbox.valueChanged.connect(lambda val: color_balance_yellow_spinbox_change(val))
-
-        # on ok button click, apply the filter
-        self.filter_window.buttonBox.rejected.connect(self.cancel_filter)
-        self.filter_window.buttonBox.accepted.connect(self.apply_filter)
-
-        self.filter_window.exec()
diff --git a/src/airunner/filters/blur_filter.py b/src/airunner/filters/blur_filter.py
new file mode 100644
index 000000000..73efd04da
--- /dev/null
+++ b/src/airunner/filters/blur_filter.py
@@ -0,0 +1,46 @@
+from airunner.filters.filter_base import FilterBase
+
+
+class BlurFilter(FilterBase):
+    ui_name = ""
+    window_title = ""
+    default_value = 0.0
+
+    def __init__(self, parent):
+        super().__init__(parent)
+        self.blur_radius = parent.settings_manager.settings.blur_radius
+
+
+    def show(self):
+        super().show()
+        self.blur_radius.set(self.default_value)
+
+        self.parent.current_filter = self.filter
+
+        blur_radius = self.blur_radius.get()
+
+        # set the gaussian_blur_window settings values to the current settings
+        self.filter_window.blur_slider.setValue(int(blur_radius))
+        self.filter_window.blur_slider.valueChanged.connect(
+            lambda val: self.handle_blur_radius_slider_change(val))
+        self.filter_window.blur_spinbox.setValue(blur_radius)
+        self.filter_window.blur_spinbox.valueChanged.connect(
+            lambda val: self.handle_blur_radius_spinbox_change(val))
+
+        # on ok button click, apply the filter
+        self.filter_window.buttonBox.rejected.connect(self.cancel_filter)
+        self.filter_window.buttonBox.accepted.connect(self.apply_filter)
+        self.preview_filter()
+        self.filter_window.exec()
+
+    def handle_blur_radius_slider_change(self, val):
+        self.blur_radius.set(float(val))
+        self.filter_window.blur_spinbox.setValue(float(val))
+        self.preview_filter()
+        self.canvas.update()
+
+    def handle_blur_radius_spinbox_change(self, val):
+        self.blur_radius.set(val)
+        self.filter_window.blur_slider.setValue(int(val))
+        self.preview_filter()
+        self.update_canvas()
diff --git a/src/airunner/filters/color_balance_filter.py b/src/airunner/filters/color_balance_filter.py
new file mode 100644
index 000000000..9049ff408
--- /dev/null
+++ b/src/airunner/filters/color_balance_filter.py
@@ -0,0 +1,18 @@
+from PIL import ImageEnhance
+from PIL.ImageFilter import Filter
+
+
+class ColorBalanceFilter(Filter):
+    name = "Color Balance"
+
+    def __init__(self, cyan_red=0, magenta_green=0, yellow_blue=0):
+        self.cyan_red = cyan_red
+        self.magenta_green = magenta_green
+        self.yellow_blue = yellow_blue
+
+    def filter(self, image):
+        # Apply enhancement
+        image = ImageEnhance.Color(image).enhance(1.0 + self.cyan_red)
+        image = ImageEnhance.Color(image).enhance(1.0 + self.magenta_green)
+        image = ImageEnhance.Color(image).enhance(1.0 + self.yellow_blue)
+        return image
diff --git a/src/airunner/filters/filter_base.py b/src/airunner/filters/filter_base.py
new file mode 100644
index 000000000..9332915b8
--- /dev/null
+++ b/src/airunner/filters/filter_base.py
@@ -0,0 +1,37 @@
+import os
+from PyQt6 import uic
+
+
+class FilterBase:
+    ui_name = ""
+    window_title = ""
+
+    @property
+    def filter(self):
+        return None
+
+    def update_canvas(self):
+        self.canvas.update()
+
+    def __init__(self, parent):
+        self.filter_window = None
+        self.parent = parent
+        self.canvas = parent.canvas
+
+    def show(self):
+        self.filter_window = uic.loadUi(os.path.join(f"pyqt/{self.ui_name}.ui"))
+        self.filter_window.setWindowTitle(self.window_title)
+
+    def cancel_filter(self):
+        self.filter_window.close()
+        self.parent.canvas.cancel_filter()
+        self.update_canvas()
+
+    def apply_filter(self):
+        self.parent.canvas.apply_filter(self.filter)
+        self.filter_window.close()
+        self.update_canvas()
+
+    def preview_filter(self):
+        self.parent.canvas.preview_filter(self.filter)
+        self.update_canvas()
diff --git a/src/airunner/filters/filter_box_blur.py b/src/airunner/filters/filter_box_blur.py
new file mode 100644
index 000000000..36c1098d0
--- /dev/null
+++ b/src/airunner/filters/filter_box_blur.py
@@ -0,0 +1,12 @@
+from PIL.ImageFilter import BoxBlur
+from airunner.filters.blur_filter import BlurFilter
+
+
+class FilterBoxBlur(BlurFilter):
+    ui_name = "box_blur_window"
+    window_title = "Box Blur"
+    default_value = 1.0
+
+    @property
+    def filter(self):
+        return BoxBlur(radius=self.blur_radius.get())
diff --git a/src/airunner/filters/filter_color_balance.py b/src/airunner/filters/filter_color_balance.py
new file mode 100644
index 000000000..0e9621e9d
--- /dev/null
+++ b/src/airunner/filters/filter_color_balance.py
@@ -0,0 +1,83 @@
+from airunner.filters.color_balance_filter import ColorBalanceFilter
+from airunner.filters.filter_base import FilterBase
+
+
+class FilterColorBalance(FilterBase):
+    ui_name = "color_balance"
+    window_title = "Color Balance"
+
+    def __init__(self, parent):
+        super().__init__(parent)
+        self.cyan_red = parent.settings_manager.settings.cyan_red
+        self.magenta_green = parent.settings_manager.settings.magenta_green
+        self.yellow_blue = parent.settings_manager.settings.yellow_blue
+
+    @property
+    def filter(self):
+        return ColorBalanceFilter(
+            cyan_red=self.cyan_red.get() / 1000,
+            magenta_green=self.magenta_green.get() / 1000,
+            yellow_blue=self.yellow_blue.get() / 1000
+        )
+
+    def show(self):
+        super().show()
+
+        self.cyan_red.set(0)
+        self.magenta_green.set(0)
+        self.yellow_blue.set(0)
+
+        def update_filter():
+            self.parent.current_filter = self.filter
+            self.canvas.update()
+
+        def color_balance_cyan_slider_change(val):
+            self.filter_window.cyan_spinbox.setValue(val / 1000.0)
+            self.filter_window.cyan_spinbox.update()
+            update_filter()
+
+        def color_balance_magenta_slider_change(val):
+            self.filter_window.magenta_spinbox.setValue(val / 1000.0)
+            self.filter_window.magenta_spinbox.update()
+            update_filter()
+
+        def color_balance_yellow_slider_change(val):
+            self.filter_window.yellow_spinbox.setValue(val / 1000.0)
+            self.filter_window.yellow_spinbox.update()
+            update_filter()
+
+        def color_balance_cyan_spinbox_change(val):
+            self.filter_window.cyan_slider.setValue(int(val * 1000.0))
+            self.filter_window.cyan_slider.update()
+            update_filter()
+
+        def color_balance_magenta_spinbox_change(val):
+            self.filter_window.magenta_slider.setValue(int(val * 1000.0))
+            self.filter_window.magenta_slider.update()
+            update_filter()
+
+        def color_balance_yellow_spinbox_change(val):
+            self.filter_window.yellow_slider.setValue(int(val * 1000.0))
+            self.filter_window.yellow_slider.update()
+            update_filter()
+
+        self.filter_window.cyan_slider.setValue(self.cyan_red.get())
+        self.filter_window.cyan_slider.valueChanged.connect(lambda val: color_balance_cyan_slider_change(val))
+        self.filter_window.cyan_spinbox.setValue(self.cyan_red.get() / 1000.0)
+        self.filter_window.cyan_spinbox.valueChanged.connect(lambda val: color_balance_cyan_spinbox_change(val))
+
+        self.filter_window.magenta_slider.setValue(self.magenta_green.get())
+        self.filter_window.magenta_slider.valueChanged.connect(lambda val: color_balance_magenta_slider_change(val))
+        self.filter_window.magenta_spinbox.setValue(self.magenta_green.get() / 1000.0)
+        self.filter_window.magenta_spinbox.valueChanged.connect(lambda val: color_balance_magenta_spinbox_change(val))
+
+        self.filter_window.yellow_slider.setValue(self.yellow_blue.get())
+        self.filter_window.yellow_slider.valueChanged.connect(lambda val: color_balance_yellow_slider_change(val))
+        self.filter_window.yellow_spinbox.setValue(self.yellow_blue.get() / 1000.0)
+        self.filter_window.yellow_spinbox.valueChanged.connect(lambda val: color_balance_yellow_spinbox_change(val))
+
+        # on ok button click, apply the filter
+        self.filter_window.buttonBox.rejected.connect(self.cancel_filter)
+        self.filter_window.buttonBox.accepted.connect(self.apply_filter)
+
+        self.filter_window.exec()
diff --git a/src/airunner/filters/filter_gaussian_blur.py b/src/airunner/filters/filter_gaussian_blur.py
new file mode 100644
index 000000000..1dd28ccb6
--- /dev/null
+++ b/src/airunner/filters/filter_gaussian_blur.py
@@ -0,0 +1,12 @@
+from PIL.ImageFilter import GaussianBlur
+from airunner.filters.blur_filter import BlurFilter
+
+
+class FilterGaussianBlur(BlurFilter):
+    ui_name = "gaussian_blur_window"
+    window_title = "Gaussian Blur"
+    default_value = 1.0
+
+    @property
+    def filter(self):
+        return GaussianBlur(radius=self.blur_radius.get())
diff --git a/src/airunner/filters/filter_pixel_art.py b/src/airunner/filters/filter_pixel_art.py
new file mode 100644
index 000000000..64db70c51
--- /dev/null
+++ b/src/airunner/filters/filter_pixel_art.py
@@ -0,0 +1,74 @@
+from airunner.filters.filter_base import FilterBase
+from airunner.filters.pixel_filter import PixelFilter
+
+
+class FilterPixelArt(FilterBase):
+    ui_name = "pixel_art"
+    window_title = "Pixel Art"
+    number_of_colors = 24
+    smoothing = 1
+    base_size = 256
+
+    @property
+    def filter(self):
+        return PixelFilter(
+            number_of_colors=self.number_of_colors,
+            smoothing=self.smoothing,
+            base_size=self.base_size
+        )
+
+    def show(self):
+        super().show()
+        self.parent.current_filter = self.filter
+
+        # set the gaussian_blur_window settings values to the current settings
+        self.filter_window.number_of_colors_slider.setValue(self.number_of_colors)
+        self.filter_window.number_of_colors_spinbox.setValue(self.number_of_colors)
+        self.filter_window.number_of_colors_slider.valueChanged.connect(
+            lambda val: self.handle_number_of_colors_change_slider(val))
+        self.filter_window.number_of_colors_spinbox.valueChanged.connect(
+            lambda val: self.handle_number_of_colors_change_spinbox(val))
+
+        # ensure base_size is multiple of 16
+        self.filter_window.base_size_slider.setValue(self.base_size)
+        self.filter_window.base_size_spinbox.setValue(self.base_size)
+        self.filter_window.base_size_slider.valueChanged.connect(
+            lambda val: self.handle_base_size_change_slider(val))
+        self.filter_window.base_size_spinbox.valueChanged.connect(
+            lambda val: self.handle_base_size_change_spinbox(val))
+
+        # on ok button click, apply the filter
+        self.filter_window.buttonBox.rejected.connect(self.cancel_filter)
+        self.filter_window.buttonBox.accepted.connect(self.apply_filter)
+
+        # apply the filter
+        self.preview_filter()
+        self.filter_window.exec()
+
+    def handle_number_of_colors_change_slider(self, val):
+        self.handle_number_of_colors_change(val)
+        self.filter_window.number_of_colors_spinbox.setValue(val)
+
+    def handle_number_of_colors_change_spinbox(self, val):
+        self.handle_number_of_colors_change(val)
+        self.filter_window.number_of_colors_slider.setValue(val)
+
+    def handle_number_of_colors_change(self, val):
+        self.number_of_colors = val
+        self.parent.current_filter = self.filter
+        self.preview_filter()
+
+    def handle_base_size_change_slider(self, val):
+        val = val - (val % 16)
+        self.handle_base_size_change(val)
+        self.filter_window.base_size_spinbox.setValue(val)
+
+    def handle_base_size_change_spinbox(self, val):
+        val = val - (val % 16)
+        self.handle_base_size_change(val)
+        self.filter_window.base_size_slider.setValue(val)
+
+    def handle_base_size_change(self, val):
+        self.base_size = val
+        self.parent.current_filter = self.filter
+        self.preview_filter()
diff --git a/src/airunner/filters/filter_saturation.py b/src/airunner/filters/filter_saturation.py
new file mode 100644
index 000000000..a2aafd299
--- /dev/null
+++ b/src/airunner/filters/filter_saturation.py
@@ -0,0 +1,40 @@
+from airunner.filters.filter_base import FilterBase
+from airunner.filters.saturation_filter import SaturationFilter
+
+
+class FilterSaturation(FilterBase):
+    ui_name = "saturation_window"
+    window_title = "Saturation"
+    factor = 0
+
+    @property
+    def filter(self):
+        return SaturationFilter(factor=self.factor / 1000)
+
+    def show(self):
+        super().show()
+        # set the gaussian_blur_window settings values to the current settings
+        self.filter_window.blur_slider.setValue(int(self.factor))
+        self.filter_window.blur_slider.valueChanged.connect(
+            lambda val: self.handle_blur_radius_slider_change(val))
+        self.filter_window.blur_spinbox.setValue(self.factor / 1000.0)
+        self.filter_window.blur_spinbox.valueChanged.connect(
+            lambda val: self.handle_blur_radius_spinbox_change(val))
+
+        # on ok button click, apply the filter
+        self.filter_window.buttonBox.rejected.connect(self.cancel_filter)
+        self.filter_window.buttonBox.accepted.connect(self.apply_filter)
+
+        self.filter_window.exec()
+
+    def handle_blur_radius_slider_change(self, val):
+        self.factor = float(val)
+        self.filter_window.blur_spinbox.setValue(val / 1000.0)
+        self.parent.current_filter = self.filter
+        self.canvas.update()
+
+    def handle_blur_radius_spinbox_change(self, val):
+        self.factor = val / 100.0
+        self.filter_window.blur_slider.setValue(int(val * 1000.0))
+        self.parent.current_filter = self.filter
+        self.update_canvas()
diff --git a/src/airunner/filters/filter_unsharp_mask.py b/src/airunner/filters/filter_unsharp_mask.py
new file mode 100644
index 000000000..623a37f58
--- /dev/null
+++ b/src/airunner/filters/filter_unsharp_mask.py
@@ -0,0 +1,91 @@
+from PIL.ImageFilter import UnsharpMask
+from airunner.filters.filter_base import FilterBase
+
+
+class FilterUnsharpMask(FilterBase):
+    ui_name = "unsharp_mask_window"
+    window_title = "Unsharp Mask"
+    unsharp_radius = 50.0
+    unsharp_percent = 50.0
+    unsharp_threshold = 50.0
+
+    @property
+    def filter(self):
+        return UnsharpMask(
+            radius=self.unsharp_radius,
+            percent=int(self.unsharp_percent),
+            threshold=int(self.unsharp_threshold)
+        )
+
+    def show(self):
+        super().show()
+
+        def update_filter():
+            self.parent.current_filter = self.filter
+            self.canvas.update()
+
+        def handle_unsharp_radius_slider_change(val):
+            self.unsharp_radius = val
+            self.filter_window.radius_spinbox.setValue(val)
+            self.filter_window.radius_spinbox.update()
+            self.filter_window.radius_slider.update()
+            self.preview_filter()
+
+        def handle_unsharp_radius_spinbox_change(val):
+            self.unsharp_radius = val
+            self.filter_window.radius_slider.setValue(int(val))
+            self.filter_window.radius_spinbox.update()
+            self.filter_window.radius_slider.update()
+            self.preview_filter()
+
+        def handle_unsharp_percent_slider_change(val):
+            self.unsharp_percent = val
+            self.filter_window.percent_spinbox.setValue(val)
+            self.filter_window.percent_spinbox.update()
+            self.filter_window.percent_slider.update()
+            self.preview_filter()
+
+        def handle_unsharp_percent_spinbox_change(val):
+            self.unsharp_percent = val
+            self.filter_window.percent_slider.setValue(int(val))
+            self.filter_window.percent_spinbox.update()
+            self.filter_window.percent_slider.update()
+            self.preview_filter()
+
+        def handle_unsharp_threshold_slider_change(val):
+            self.unsharp_threshold = val
+            self.filter_window.threshold_spinbox.setValue(val)
+            self.filter_window.threshold_spinbox.update()
+            self.filter_window.threshold_slider.update()
+            self.preview_filter()
+
+        def handle_unsharp_threshold_spinbox_change(val):
+            self.unsharp_threshold = val
+            self.filter_window.threshold_slider.setValue(int(val))
+            self.filter_window.threshold_spinbox.update()
+            self.filter_window.threshold_slider.update()
+            self.preview_filter()
+
+        # set the gaussian_blur_window settings values to the current settings
+        self.filter_window.radius_slider.setValue(int(self.unsharp_radius))
+        self.filter_window.radius_slider.valueChanged.connect(lambda val: handle_unsharp_radius_slider_change(val))
+        self.filter_window.radius_spinbox.setValue(self.unsharp_radius)
+        self.filter_window.radius_spinbox.valueChanged.connect(lambda val: handle_unsharp_radius_spinbox_change(val))
+
+        self.filter_window.percent_slider.setValue(int(self.unsharp_percent))
+        self.filter_window.percent_slider.valueChanged.connect(lambda val: handle_unsharp_percent_slider_change(val))
+        self.filter_window.percent_spinbox.setValue(self.unsharp_percent)
+        self.filter_window.percent_spinbox.valueChanged.connect(lambda val: handle_unsharp_percent_spinbox_change(val))
+
+        self.filter_window.threshold_slider.setValue(int(self.unsharp_threshold))
+        self.filter_window.threshold_slider.valueChanged.connect(lambda val: handle_unsharp_threshold_slider_change(val))
+        self.filter_window.threshold_spinbox.setValue(self.unsharp_threshold)
+        self.filter_window.threshold_spinbox.valueChanged.connect(lambda val: handle_unsharp_threshold_spinbox_change(val))
+        self.filter_window = self.filter_window
+        # on ok button click, apply the filter
+        self.filter_window.buttonBox.rejected.connect(self.cancel_filter)
+        self.filter_window.buttonBox.accepted.connect(self.apply_filter)
+        self.preview_filter()
+        self.parent.current_filter = self.filter
+
+        self.filter_window.exec()
diff --git a/src/airunner/filters/pixel_filter.py b/src/airunner/filters/pixel_filter.py
new file mode 100644
index 000000000..a737dd83a
--- /dev/null
+++ b/src/airunner/filters/pixel_filter.py
@@ -0,0 +1,28 @@
+from PIL import Image, ImageFilter
+
+
+class PixelFilter(ImageFilter.Filter):
+    name = "Resize Filter"
+
+    def __init__(self, number_of_colors=24, smoothing=1, base_size=16):
+        self.number_of_colors = number_of_colors
+        self.smoothing = smoothing
+        self.base_size = base_size
+
+    def filter(self, image):
+        # Downsize while maintaining aspect ratio
+        width, height = image.size
+        scale = min(self.base_size / width, self.base_size / height)
+        new_width = int(width * scale)
+        new_height = int(height * scale)
+        downsized = image.resize((new_width, new_height), Image.NEAREST)
+
+        # Upscale back to original dimensions
+        target_width = int(new_width / scale)
+        target_height = int(new_height / scale)
+        upscaled = downsized.resize((target_width, target_height), Image.NEAREST)
+
+        # Reduce number of colors
+        quantized = upscaled.quantize(self.number_of_colors)
+        quantized = quantized.convert("L")
+        return quantized
diff --git a/src/airunner/filters/saturation_filter.py b/src/airunner/filters/saturation_filter.py
new file mode 100644
index 000000000..bfbb3f46e
--- /dev/null
+++ b/src/airunner/filters/saturation_filter.py
@@ -0,0 +1,12 @@
+from PIL import ImageEnhance
+from PIL.ImageFilter import Filter
+
+
+class SaturationFilter(Filter):
+    name = "Saturation"
+
+    def __init__(self, factor=1.0):
+        self.factor = factor
+
+    def filter(self, image):
+        return ImageEnhance.Color(image).enhance(1.0 + self.factor)
diff --git a/src/airunner/history.py b/src/airunner/history.py
index 5dd2d4adf..aef207f23 100644
--- a/src/airunner/history.py
+++ b/src/airunner/history.py
@@ -4,4 +4,4 @@ class History:
 
     def add_event(self, data: dict):
         self.event_history.append(data)
-        self.undone_history = []
\ No newline at end of file
+        self.undone_history = []
diff --git a/src/airunner/main.py b/src/airunner/main.py
index bab0e3fb0..5b27a2111 100644
--- a/src/airunner/main.py
+++ b/src/airunner/main.py
@@ -3,7 +3,7 @@
 import sys
 from PyQt6 import uic, QtCore
 from PyQt6.QtWidgets import QApplication, QFileDialog
-from PyQt6.QtCore import pyqtSlot
+from PyQt6.QtCore import pyqtSlot, Qt
 from PyQt6.QtGui import QGuiApplication
 from aihandler.qtvar import TQDMVar, ImageVar, MessageHandlerVar, ErrorHandlerVar
 from aihandler.settings import LOG_LEVEL
@@ -128,9 +128,17 @@ def __init__(self, *args, **kwargs):
         self.initialize()
         self.display()
         self.settings_manager.enable_save()
+        # on window resize:
+        # self.applicationStateChanged.connect(self.on_state_changed)
         if not self.testing:
             self.exec()
 
+    def on_state_changed(self, state):
+        if state == Qt.ApplicationState.ApplicationActive:
+            self.canvas.pos_x = int(self.window.x() / 4)
+            self.canvas.pos_y = int(self.window.y() / 2)
+            self.canvas.update()
+
     def initialize(self):
         self.initialize_settings_manager()
         self.initialize_tqdm()
@@ -147,6 +155,8 @@ def initialize(self):
         self.initialize_stable_diffusion()
         ExtensionMixin.initialize(self)
         BrushesMixin.initialize(self)
+        EmbeddingMixin.initialize(self)
+        ModelMixin.initialize(self)
 
     def initialize_settings_manager(self):
         self.settings_manager = SettingsManager()
@@ -188,6 +198,9 @@ def initialize_stable_diffusion(self):
             settings_manager=self.settings_manager,
         )
 
+    def save_settings(self):
+        self.settings_manager.save_settings()
+
     def display(self):
         if not self.testing:
             self.window.show()
diff --git a/src/airunner/mixins/canvas_brushes_mixin.py b/src/airunner/mixins/canvas_brushes_mixin.py
index 5c829a821..c53e34f3b 100644
--- a/src/airunner/mixins/canvas_brushes_mixin.py
+++ b/src/airunner/mixins/canvas_brushes_mixin.py
@@ -1,10 +1,17 @@
-from PIL.ImageDraw import ImageDraw
-from PyQt6.QtCore import Qt, QPointF, QPoint
-from PyQt6.QtGui import QPainter, QPainterPath, QColor, QPen
+from PIL import ImageDraw
+from PyQt6.QtCore import Qt, QPointF, QPoint, QSize, QRect
+from PyQt6.QtGui import QPainter, QPainterPath, QColor, QPen, QImage
 from airunner.models.linedata import LineData
+from PIL import Image
 
 
 class CanvasBrushesMixin:
+    _point = None
+    active_canvas_rect = QRect(0, 0, 0, 0)
+    opacity = None
+    color = None
+    width = None
+
     @property
     def primary_color(self):
         return QColor(self.settings_manager.settings.primary_color.get())
@@ -22,17 +29,29 @@ def secondary_brush_opacity(self):
         return self.settings_manager.settings.secondary_brush_opacity.get()
 
     def draw(self, layer, index):
-        painter = QPainter(self.canvas_container)
-        painter.setBrush(self.brush)
-        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
-
-        # create a QPainterPath to hold the entire line
         path = QPainterPath()
+        painter = None
         for line in layer.lines:
             pen = line.pen
             pen.setCapStyle(Qt.PenCapStyle.RoundCap)
             pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
-
+            if self.width is None or self.width != line.width:
+                self.width = line.width
+                self.draw_path(path, painter)
+                painter = None
+            if self.color is None or self.color != line.color:
+                self.color = line.color
+                self.draw_path(path, painter)
+                painter = None
+            if self.opacity is None or self.opacity != line.opacity:
+                self.opacity = line.opacity
+                self.draw_path(path, painter)
+                painter = None
+            if not painter:
+                painter = QPainter(self.canvas_container)
+                painter.setBrush(self.brush)
+                painter.setRenderHint(QPainter.RenderHint.Antialiasing)
+                path = QPainterPath()
             painter.setPen(pen)
             painter.setOpacity(line.opacity / 255)
 
@@ -55,8 +74,12 @@ def draw(self, layer, index):
             path.cubicTo(ctrl1, ctrl2, end)
 
         # draw the entire line with a single drawPath call
-        painter.drawPath(path)
-        painter.end()
+        self.draw_path(path, painter)
+
+    def draw_path(self, path, painter):
+        if painter:
+            painter.drawPath(path)
+            painter.end()
 
     def handle_erase(self, event):
         self.is_erasing = True
@@ -94,8 +117,6 @@ def pen(self, event):
         return pen
 
     def handle_draw(self, event):
-        # Continue drawing the current line as the mouse is moved but use brush_size
-        # to control the radius of the line being drawn
         start = event.pos() - QPoint(self.pos_x, self.pos_y)
         pen = self.pen(event)
         opacity = 255
@@ -106,7 +127,22 @@ def handle_draw(self, event):
         if len(self.current_layer.lines) > 0:
             previous = LineData(self.current_layer.lines[-1].start_point, start, pen, self.current_layer_index, opacity)
             self.current_layer.lines[-1] = previous
-        end = event.pos() - QPoint(self.pos_x, self.pos_y)
+
+            if self.shift_is_pressed:  # draw a strait line by combining the line segments
+                if len(self.current_layer.lines) > self.start_drawing_line_index:
+                    start_line = self.current_layer.lines[self.start_drawing_line_index]
+                    end_line = self.current_layer.lines[self.stop_drawing_line_index - 1]
+                    new_line_data = LineData(
+                        start_line.start_point,
+                        end_line.end_point,
+                        start_line.pen,
+                        start_line.layer_index,
+                        start_line.opacity
+                    )
+                    self.current_layer.lines = self.current_layer.lines[:self.start_drawing_line_index]
+                    self.current_layer.lines.append(new_line_data)
+
+        end = event.pos() - QPoint(self.pos_x + 1, self.pos_y)
         line_data = LineData(start, end, pen, self.current_layer_index, opacity)
         self.current_layer.lines.append(line_data)
-        self.update()
+        self.update()
\ No newline at end of file
diff --git a/src/airunner/mixins/canvas_image_mixin.py b/src/airunner/mixins/canvas_image_mixin.py
index 59377f043..e61f788d7 100644
--- a/src/airunner/mixins/canvas_image_mixin.py
+++ b/src/airunner/mixins/canvas_image_mixin.py
@@ -8,6 +8,8 @@
 
 
 class CanvasImageMixin:
+    working_image = None
+
     @property
     def current_active_image(self):
         try:
@@ -15,11 +17,22 @@ def current_active_image(self):
         except IndexError:
             return None
 
-    def apply_filter(self):
-        index = 0
-        for image in self.current_layer.images:
-            self.current_layer.images[index].image = image.image.filter(self.parent.current_filter)
-            index += 1
+    def apply_filter(self, filter):
+        if self.working_image is None:
+            self.working_image = self.current_active_image.image.copy()
+        self.current_layer.images[0].image = self.working_image.filter(filter)
+        self.working_image = None
+
+    def preview_filter(self, filter):
+        if self.working_image is None:
+            self.working_image = self.current_active_image.image.copy()
+        image = self.working_image.copy()
+        self.current_active_image.image = image.filter(filter)
+        self.current_layer.images[0] = self.current_active_image
+
+    def cancel_filter(self):
+        self.current_layer.images[0].image = self.working_image
+        self.working_image = None
 
     def draw(self, layer, index):
         painter = QPainter(self.canvas_container)
@@ -136,28 +149,18 @@ def load_image(self, image_path):
         self.update()
 
     def save_image(self, image_path):
+        if self.current_layer.image is None:
+            return
         image = self.current_layer.image.image
         image = image.convert("RGBA")
+        if not "." in image_path:
+            image_path += ".png"
+        if not image_path.endswith(".png") and not image_path.endswith(".gif"):
+            image = image.convert("RGB")
         image.save(image_path)
         self.saving = False
         self.update()
 
-    def image_handler_old(self, active_img, data):
-        action = data["action"]
-        rect: QRect = data["options"]["outpaint_box_rect"]
-        rect: QRect = data["options"]["outpaint_box_rect"]
-        x = rect.x()
-        y = rect.y()
-        if len(self.current_layer.images) > 0:
-            # merge with previous image
-            image = self.current_layer.images[0].image
-            image.paste(active_img, (int(x), int(y)))
-            self.current_layer.images = [ImageData(QPoint(int(x), int(y)), image)]
-        else:
-            self.current_layer.images = [ImageData(QPoint(int(x), int(y)), active_img)]
-        self.current_layer.lines = []
-        self.update()
-
     def image_handler(self, active_img, data):
         self.update_image_canvas(data["action"], data, active_img)
         self.current_layer.lines = []
@@ -178,17 +181,92 @@ def update_image_canvas(self, section=None, data=None, processed_image: Image =
         processed_image, image_root_point, image_pivot_point = self.handle_outpaint(
             outpaint_box_rect, processed_image, section
         )
-        history_event = {
+
+        self.image_root_point = image_root_point
+        self.image_pivot_point = image_pivot_point
+        self.add_image_to_canvas(processed_image)
+
+    def handle_outpaint(self, outpaint_box_rect, outpainted_image, action):
+        if len(self.current_layer.images) == 0:
+            point = QPoint(outpaint_box_rect.x(), outpaint_box_rect.y())
+            return outpainted_image, self.image_root_point, point
+
+        # make a copy of the current canvas image
+        existing_image_copy = self.current_layer.images[0].image.copy()
+        width = existing_image_copy.width
+        height = existing_image_copy.height
+        working_width = self.settings_manager.settings.working_width.get()
+        working_height = self.settings_manager.settings.working_height.get()
+
+        is_drawing_left = outpaint_box_rect.x() < self.image_pivot_point.x()
+        is_drawing_up = outpaint_box_rect.y() < self.image_pivot_point.y()
+
+        if is_drawing_left:
+            # get the x overlap of the outpaint box and the image
+            x_overlap = min(width, outpaint_box_rect.width()) - max(0, outpaint_box_rect.x())
+        else:
+            # get the x overlap of the outpaint box and the image
+            x_overlap = min(width, outpaint_box_rect.width()) - max(0, outpaint_box_rect.x() - self.image_pivot_point.x())
+
+        if is_drawing_up:
+            # get the y overlap of the outpaint box and the image
+            y_overlap = min(height, outpaint_box_rect.height()) - max(0, outpaint_box_rect.y())
+        else:
+            # get the y overlap of the outpaint box and the image
+            y_overlap = min(height, outpaint_box_rect.height()) - max(0, outpaint_box_rect.y() - self.image_pivot_point.y())
+
+        # get the x and y overlap of the outpaint box and the image
+        new_dimensions = (int(width + working_width - x_overlap), int(height + working_height - y_overlap))
+        if new_dimensions[0] < width:
+            new_dimensions = (width, new_dimensions[1])
+        if new_dimensions[1] < height:
+            new_dimensions = (new_dimensions[0], height)
+        new_image = Image.new("RGBA", new_dimensions, (0, 0, 0, 0))
+        new_image_a = Image.new("RGBA", new_dimensions, (0, 0, 0, 0))
+        new_image_b = Image.new("RGBA", new_dimensions, (0, 0, 0, 0))
+        existing_image_pos = [0, 0]
+        image_root_point = QPoint(self.image_root_point.x(), self.image_root_point.y())
+        image_pivot_point = QPoint(self.image_pivot_point.x(), self.image_pivot_point.y())
+        if is_drawing_left:
+            current_x_pos = abs(outpaint_box_rect.x() - image_pivot_point.x())
+            left_overlap = abs(outpaint_box_rect.x()) - abs(image_root_point.x())
+            image_root_point.setX(width + left_overlap)
+            image_pivot_point.setX(int(outpaint_box_rect.x()))
+            existing_image_pos = [current_x_pos, existing_image_pos[1]]
+            pos_x = max(0, outpaint_box_rect.x() + self.image_pivot_point.x())
+        else:
+            pos_x = max(0, outpaint_box_rect.x() - self.image_pivot_point.x())
+        if is_drawing_up:
+            current_y_pos = abs(outpaint_box_rect.y() - image_pivot_point.y())
+            up_overlap = abs(outpaint_box_rect.y()) - abs(image_root_point.y())
+            image_root_point.setY(height + up_overlap)
+            image_pivot_point.setY(int(outpaint_box_rect.y()))
+            existing_image_pos = [existing_image_pos[0], current_y_pos]
+            pos_y = max(0, outpaint_box_rect.y() + self.image_pivot_point.y())
+        else:
+            pos_y = max(0, outpaint_box_rect.y() - self.image_pivot_point.y())
+
+        new_image_a.paste(outpainted_image, (int(pos_x), int(pos_y)))
+        new_image_b.paste(existing_image_copy, (int(existing_image_pos[0]), int(existing_image_pos[1])))
+
+        if action == "outpaint":
+            new_image = Image.alpha_composite(new_image, new_image_a)
+            new_image = Image.alpha_composite(new_image, new_image_b)
+        else:
+            new_image = Image.alpha_composite(new_image, new_image_b)
+            new_image = Image.alpha_composite(new_image, new_image_a)
+
+        return new_image, image_root_point, image_pivot_point
+
+    def add_image_to_canvas(self, image):
+        self.parent.history.add_event({
             "event": "set_image",
             "layer_index": self.current_layer_index,
             "images": self.current_layer.images,
             "previous_image_root_point": self.image_root_point,
             "previous_image_pivot_point": self.image_pivot_point,
-        }
-        self.parent.history.add_event(history_event)
-        self.image_root_point = image_root_point
-        self.image_pivot_point = image_pivot_point
-        self.current_layer.images = [ImageData(image_pivot_point, processed_image)]
+        })
+        self.current_layer.images = [ImageData(self.image_pivot_point, image)]
 
     def get_image_copy(self, index):
         return [ImageData(imageData.position, imageData.image.copy()) for imageData in self.layers[index].images]
\ No newline at end of file
diff --git a/src/airunner/mixins/embedding_mixin.py b/src/airunner/mixins/embedding_mixin.py
index 85daa669c..a62cf9dbe 100644
--- a/src/airunner/mixins/embedding_mixin.py
+++ b/src/airunner/mixins/embedding_mixin.py
@@ -12,6 +12,25 @@ def embedding_names(self):
             self._embedding_names = self.get_list_of_available_embedding_names()
         return self._embedding_names
 
+    def initialize(self):
+        # listen to self.settings_manager.settings.embeddings_path and update self.embedding_names on change
+        self.settings_manager.settings.embeddings_path.my_signal.connect(self.update_embedding_names)
+
+        for tab_name in self.tabs.keys():
+            tab = self.tabs[tab_name]
+            self.load_embeddings(tab)
+
+    def update_embedding_names(self, _):
+        self._embedding_names = None
+        for tab_name in self.tabs.keys():
+            tab = self.tabs[tab_name]
+            # clear embeddings
+            try:
+                tab.embeddings.widget().deleteLater()
+            except AttributeError:
+                pass
+            self.load_embeddings(tab)
+
     def load_embeddings(self, tab):
         container = QWidget()
         container.setLayout(QVBoxLayout())
@@ -23,11 +42,22 @@ def load_embeddings(self, tab):
         tab.embeddings.setWidget(container)
 
     def get_list_of_available_embedding_names(self):
-        embeddings_folder = os.path.join(self.settings_manager.settings.model_base_path.get(), "embeddings")
-        tokens = []
-        if os.path.exists(embeddings_folder):
-            for f in os.listdir(embeddings_folder):
-                loaded_learned_embeds = torch.load(os.path.join(embeddings_folder, f), map_location="cpu")
+        embeddings_path = self.settings_manager.settings.embeddings_path.get() or "embeddings"
+        if embeddings_path == "embeddings":
+            embeddings_path = os.path.join(self.settings_manager.settings.model_base_path.get(), embeddings_path)
+        return self.find_embeddings_in_path(embeddings_path)
+
+    def find_embeddings_in_path(self, embeddings_path, tokens=None):
+        if tokens is None:
+            tokens = []
+        if not os.path.exists(embeddings_path):
+            return tokens
+        if os.path.exists(embeddings_path):
+            for f in os.listdir(embeddings_path):
+                # check if f is directory
+                if os.path.isdir(os.path.join(embeddings_path, f)):
+                    return self.find_embeddings_in_path(os.path.join(embeddings_path, f), tokens)
+                loaded_learned_embeds = torch.load(os.path.join(embeddings_path, f), map_location="cpu")
                 trained_token = list(loaded_learned_embeds.keys())[0]
                 if trained_token == "string_to_token":
                     trained_token = loaded_learned_embeds["name"]
diff --git a/src/airunner/mixins/extension_mixin.py b/src/airunner/mixins/extension_mixin.py
index 8f9024c4f..3f09e8f64 100644
--- a/src/airunner/mixins/extension_mixin.py
+++ b/src/airunner/mixins/extension_mixin.py
@@ -27,15 +27,17 @@ def get_extensions_from_path(self):
         """
         extensions = []
         base_path = self.settings_manager.settings.model_base_path.get()
-        extension_path = os.path.join(base_path, "extensions")
-        if not os.path.exists(extension_path):
+        extensions_path = self.settings_manager.settings.extensions_path.get() or "extensions"
+        if extensions_path == "extensions":
+            extensions_path = os.path.join(base_path, extensions_path)
+        if not os.path.exists(extensions_path):
             return extensions
-        available_extensions = get_extensions_from_path(extension_path)
+        available_extensions = get_extensions_from_path(extensions_path)
         for extension in available_extensions:
             if extension.name.get() in self.settings_manager.settings.enabled_extensions.get():
                 repo = extension.repo.get()
                 name = repo.split("/")[-1]
-                path = os.path.join(extension_path, name)
+                path = os.path.join(extensions_path, name)
                 if os.path.exists(path):
                     for f in os.listdir(path):
                         if os.path.isfile(os.path.join(path, f)) and f == "main.py":
@@ -43,8 +45,11 @@ def get_extensions_from_path(self):
                             spec = importlib.util.spec_from_file_location("main", os.path.join(path, f))
                             module = importlib.util.module_from_spec(spec)
                             spec.loader.exec_module(module)
-                            extension_class = getattr(module, "Extension")
-                            extensions.append(extension_class(self.settings_manager))
+                            ExtensionClass = getattr(module, "Extension")
+                            try:
+                                extensions.append(ExtensionClass(self, self.settings_manager))
+                            except TypeError:
+                                pass
         self.settings_manager.settings.active_extensions.set(extensions)
 
     def do_generator_tab_injection(self, tab, tab_name):
@@ -55,12 +60,29 @@ def do_generator_tab_injection(self, tab, tab_name):
         :return:
         """
         for extension in self.settings_manager.settings.active_extensions.get():
-            extension.generator_tab_injection(tab, tab_name)
+            try:
+                extension.generator_tab_injection(tab, tab_name)
+            except AttributeError:
+                pass
 
     def do_menubar_injection(self):
         for extension in self.settings_manager.settings.active_extensions.get():
-            extension.menubar_injection(self.window.menubar)
+            try:
+                extension.menubar_injection(self.window.menubar)
+            except AttributeError:
+                pass
 
     def do_toolbar_injection(self):
         for extension in self.settings_manager.settings.active_extensions.get():
-            extension.toolbar_injection(self.window.horizontalFrame)
+            try:
+                extension.toolbar_injection(self.window.horizontalFrame)
+            except AttributeError:
+                pass
+
+    def do_preferences_injection(self, window):
+        for extension in self.settings_manager.settings.active_extensions.get():
+            try:
+                extension.preferences_injection(window)
+            except AttributeError:
+                pass
+
diff --git a/src/airunner/mixins/generator_mixin.py b/src/airunner/mixins/generator_mixin.py
index 29c5e16cb..13e09ca4c 100644
--- a/src/airunner/mixins/generator_mixin.py
+++ b/src/airunner/mixins/generator_mixin.py
@@ -11,8 +11,6 @@
 
 
 class GeneratorMixin:
-    tabs = None
-
     @property
     def width(self):
         return int(self.settings_manager.settings.working_width.get())
@@ -106,7 +104,7 @@ def scheduler(self, val):
     def initialize(self):
         sections = ["txt2img", "img2img", "depth2img", "pix2pix", "outpaint", "controlnet", "txt2vid"]
         self.tabs = {}
-        for tab in sections:
+        for tab in self.sections:
             self.tabs[tab] = uic.loadUi(os.path.join(
                 os.path.dirname(os.path.abspath(__file__)), "..", "pyqt/generate_form.ui"))
 
@@ -146,7 +144,6 @@ def initialize(self):
         # iterate over each tab and connect steps_slider with steps_spinbox
         for tab_name in self.tabs.keys():
             tab = self.tabs[tab_name]
-            self.load_embeddings(tab)
 
             tab.steps_slider.valueChanged.connect(lambda val, _tab=tab: self.handle_steps_slider_change(val, _tab))
             tab.steps_spinbox.valueChanged.connect(lambda val, _tab=tab: self.handle_steps_spinbox_change(val, _tab))
@@ -253,7 +250,7 @@ def image_handler(self, image, data, nsfw_content_detected):
         if nsfw_content_detected and self.settings_manager.settings.nsfw_filter.get():
             self.message_handler("NSFW content detected, try again.", error=True)
         else:
-            if data["action"] != "outpaint":
+            if data["action"] != "outpaint" and self.settings_manager.settings.image_to_new_layer.get():
                 self.canvas.add_layer()
             self.canvas.image_handler(image, data)
             self.message_handler("")
@@ -326,6 +323,7 @@ def generate_callback(self):
     def prep_video(self):
         pass
 
+    @property
     def active_rect(self):
         rect = QRect(
             self.canvas.active_grid_area_rect.x(),
@@ -380,7 +378,7 @@ def generate(
                 (self.settings.working_width.get(), self.settings.working_height.get()),
                 (0, 0, 0))
 
-            cropped_outpaint_box_rect = self.active_rect()
+            cropped_outpaint_box_rect = self.active_rect
             crop_location = (
                 cropped_outpaint_box_rect.x() - self.canvas.image_pivot_point.x(),
                 cropped_outpaint_box_rect.y() - self.canvas.image_pivot_point.y(),
@@ -516,7 +514,7 @@ def do_generate(self, extra_options=None):
             "model_base_path": sm.model_base_path.get(),
             "pos_x": 0,
             "pos_y": 0,
-            "outpaint_box_rect": self.active_rect(),
+            "outpaint_box_rect": self.active_rect,
             "hf_token": self.settings_manager.settings.hf_api_key.get(),
             "enable_model_cpu_offload": sm.enable_model_cpu_offload.get(),
             "use_controlnet": use_controlnet,
diff --git a/src/airunner/mixins/layer_mixin.py b/src/airunner/mixins/layer_mixin.py
index 86f3b297a..e726f3c71 100644
--- a/src/airunner/mixins/layer_mixin.py
+++ b/src/airunner/mixins/layer_mixin.py
@@ -8,8 +8,6 @@ class LayerMixin:
     """
     This is a mixin class for the main window that handles the layer manager.
     """
-    layers_as_single = False
-
     @property
     def layer_highlight_style(self):
         return f"background-color: #c7f6fc; border: 1px solid #000000; color: #000000;"
@@ -23,10 +21,6 @@ def initialize(self):
         self.window.layer_up_button.clicked.connect(self.layer_up_button)
         self.window.layer_down_button.clicked.connect(self.layer_down_button)
         self.window.delete_layer_button.clicked.connect(self.delete_layer_button)
-        self.window.singleLayerCheckbox.stateChanged.connect(self.set_layers_as_single)
-
-    def set_layers_as_single(self, state):
-        self.layers_as_single = state == 2
 
     def layer_up_button(self):
         self.canvas.move_layer_up(self.canvas.current_layer)
diff --git a/src/airunner/mixins/menubar_mixin.py b/src/airunner/mixins/menubar_mixin.py
index f282b71ee..70293549d 100644
--- a/src/airunner/mixins/menubar_mixin.py
+++ b/src/airunner/mixins/menubar_mixin.py
@@ -1,6 +1,11 @@
 from PyQt6.QtWidgets import QFileDialog
-from airunner.filters import FilterBoxBlur, FilterUnsharpMask, FilterSaturation, FilterColorBalance, FilterGaussianBlur, \
-    FilterPixelArt
+
+from airunner.filters.filter_box_blur import FilterBoxBlur
+from airunner.filters.filter_color_balance import FilterColorBalance
+from airunner.filters.filter_gaussian_blur import FilterGaussianBlur
+from airunner.filters.filter_pixel_art import FilterPixelArt
+from airunner.filters.filter_saturation import FilterSaturation
+from airunner.filters.filter_unsharp_mask import FilterUnsharpMask
 
 
 class MenubarMixin:
@@ -14,8 +19,10 @@ def initialize(self):
         self.window.actionPaste.triggered.connect(self.paste_image)
         self.window.actionCopy.triggered.connect(self.copy_image)
         self.window.actionResize_on_Paste.triggered.connect(self.toggle_resize_on_paste)
+        self.window.actionImage_to_new_layer.triggered.connect(self.toggle_image_to_new_layer)
         self.initialize_filter_actions()
         self.window.actionResize_on_Paste.setChecked(self.settings_manager.settings.resize_on_paste.get() == True)
+        self.window.actionImage_to_new_layer.setChecked(self.settings_manager.settings.image_to_new_layer.get() == True)
 
     def initialize_filter_actions(self):
         self.filter_gaussian_blur = FilterGaussianBlur(parent=self)
@@ -59,3 +66,6 @@ def copy_image(self):
 
     def toggle_resize_on_paste(self):
         self.settings_manager.settings.resize_on_paste.set(self.window.actionResize_on_Paste.isChecked())
+
+    def toggle_image_to_new_layer(self):
+        self.settings_manager.settings.image_to_new_layer.set(self.window.actionImage_to_new_layer.isChecked())
diff --git a/src/airunner/mixins/model_mixin.py b/src/airunner/mixins/model_mixin.py
index 5ba887ce9..c1456a338 100644
--- a/src/airunner/mixins/model_mixin.py
+++ b/src/airunner/mixins/model_mixin.py
@@ -3,15 +3,23 @@
 
 
 class ModelMixin:
+    def initialize(self):
+        self.settings_manager.settings.model_base_path.my_signal.connect(self.refresh_model_list)
+
     def refresh_model_list(self):
         for i in range(self.window.tabWidget.count()):
-            self.load_model_by_section(self.window.tabWidget.widget(i), self.sections[i])
+            tab = self.window.tabWidget.widget(i)
+            self.clear_model_list(tab)
+            self.load_model_by_section(tab, self.sections[i])
+
+    def clear_model_list(self, tab):
+        tab.model_dropdown.clear()
 
     def load_model_by_section(self, tab, section_name):
         if section_name in ["txt2img", "img2img"]:
             section_name = "generate"
         models = self.load_default_models(section_name)
-        models += self.load_models_from_path()
+        models += self.load_models_from_path(self.settings_manager.settings.model_base_path.get())
         self.models = models
         tab.model_dropdown.addItems(models)
 
@@ -20,8 +28,13 @@ def load_default_models(self, section_name):
             k for k in MODELS[section_name].keys()
         ]
 
-    def load_models_from_path(self):
-        path = os.path.join(self.settings_manager.settings.model_base_path.get())
+    def load_models_from_path(self, path, models = None):
+        if models is None:
+            models = []
         if os.path.exists(path):
-            return [os.path.join(path, model) for model in os.listdir(path)]
-        return []
\ No newline at end of file
+            for f in os.listdir(path):
+                if os.path.isdir(os.path.join(path, f)):
+                    models = self.load_models_from_path(os.path.join(path, f), models)
+                elif f.endswith(".pt") or f.endswith(".safetensors") or f.endswith(".ckpt"):
+                    models.append(f)
+        return models
\ No newline at end of file
diff --git a/src/airunner/mixins/toolbar_mixin.py b/src/airunner/mixins/toolbar_mixin.py
index 14f24a3c1..01e90c878 100644
--- a/src/airunner/mixins/toolbar_mixin.py
+++ b/src/airunner/mixins/toolbar_mixin.py
@@ -71,7 +71,7 @@ def show_grid_settings(self):
         GridSettings(self.settings_manager)
 
     def show_preferences(self):
-        PreferencesWindow(self.settings_manager)
+        PreferencesWindow(self.settings_manager, app=self)
 
     def show_advanced(self):
         AdvancedSettings(self.settings_manager)
diff --git a/src/airunner/pyqt/main_window.ui b/src/airunner/pyqt/main_window.ui
index 743820397..f0a887ad1 100644
--- a/src/airunner/pyqt/main_window.ui
+++ b/src/airunner/pyqt/main_window.ui
@@ -145,10 +145,40 @@
        <property name="verticalSpacing">
         <number>9</number>
        </property>
-       <item row="0" column="0">
-        <widget class="QGroupBox" name="groupBox">
+       <item row="4" column="0">
+        <widget class="QScrollArea" name="layers">
+         <property name="frameShadow">
+          <enum>QFrame::Plain</enum>
+         </property>
+         <property name="widgetResizable">
+          <bool>true</bool>
+         </property>
+         <property name="alignment">
+          <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+         </property>
+         <widget class="QWidget" name="scrollAreaWidgetContents">
+          <property name="geometry">
+           <rect>
+            <x>0</x>
+            <y>0</y>
+            <width>239</width>
+            <height>272</height>
+           </rect>
+          </property>
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <layout class="QVBoxLayout" name="verticalLayout"/>
+         </widget>
+        </widget>
+       </item>
+       <item row="5" column="0">
+        <widget class="QWidget" name="horizontalWidget" native="true">
          <property name="sizePolicy">
-          <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
+          <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
            <horstretch>0</horstretch>
            <verstretch>0</verstretch>
           </sizepolicy>
@@ -156,212 +186,243 @@
          <property name="layoutDirection">
           <enum>Qt::LeftToRight</enum>
          </property>
-         <property name="autoFillBackground">
-          <bool>false</bool>
-         </property>
-         <property name="title">
-          <string>Active grid area</string>
-         </property>
-         <layout class="QFormLayout" name="formLayout">
-          <item row="0" column="0">
-           <widget class="QLabel" name="label_2">
-            <property name="font">
-             <font>
-              <pointsize>10</pointsize>
-              <bold>false</bold>
-             </font>
-            </property>
-            <property name="text">
-             <string>Width</string>
-            </property>
-            <property name="alignment">
-             <set>Qt::AlignCenter</set>
-            </property>
-           </widget>
-          </item>
-          <item row="1" column="0">
-           <widget class="QSlider" name="width_slider">
+         <layout class="QHBoxLayout" name="horizontalLayout_8">
+          <property name="leftMargin">
+           <number>0</number>
+          </property>
+          <property name="topMargin">
+           <number>0</number>
+          </property>
+          <property name="bottomMargin">
+           <number>0</number>
+          </property>
+          <item>
+           <widget class="QPushButton" name="new_layer">
             <property name="sizePolicy">
-             <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+             <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
               <horstretch>0</horstretch>
               <verstretch>0</verstretch>
              </sizepolicy>
             </property>
             <property name="minimumSize">
              <size>
-              <width>150</width>
-              <height>0</height>
+              <width>32</width>
+              <height>32</height>
              </size>
             </property>
-            <property name="minimum">
-             <number>8</number>
-            </property>
-            <property name="maximum">
-             <number>2560</number>
-            </property>
-            <property name="singleStep">
-             <number>8</number>
-            </property>
-            <property name="pageStep">
-             <number>8</number>
+            <property name="toolTip">
+             <string>Create a new layer</string>
             </property>
-            <property name="value">
-             <number>8</number>
+            <property name="styleSheet">
+             <string notr="true">border-color: rgb(61, 56, 70);</string>
             </property>
-            <property name="orientation">
-             <enum>Qt::Horizontal</enum>
+            <property name="text">
+             <string/>
             </property>
-            <property name="tickPosition">
-             <enum>QSlider::NoTicks</enum>
+            <property name="icon">
+             <iconset>
+              <normaloff>../src/icons/file-add.png</normaloff>
+              <normalon>../src/icons/file-add.png</normalon>
+              <disabledoff>../src/icons/file-add.png</disabledoff>
+              <disabledon>../src/icons/file-add.png</disabledon>../src/icons/file-add.png</iconset>
             </property>
-            <property name="tickInterval">
-             <number>8</number>
+            <property name="iconSize">
+             <size>
+              <width>24</width>
+              <height>24</height>
+             </size>
             </property>
            </widget>
           </item>
-          <item row="1" column="1">
-           <widget class="QSpinBox" name="width_spinbox">
+          <item>
+           <widget class="QPushButton" name="layer_up_button">
             <property name="sizePolicy">
-             <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+             <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
               <horstretch>0</horstretch>
               <verstretch>0</verstretch>
              </sizepolicy>
             </property>
             <property name="minimumSize">
              <size>
-              <width>0</width>
-              <height>0</height>
-             </size>
-            </property>
-            <property name="maximumSize">
-             <size>
-              <width>75</width>
-              <height>16777215</height>
+              <width>32</width>
+              <height>32</height>
              </size>
             </property>
-            <property name="minimum">
-             <number>8</number>
-            </property>
-            <property name="maximum">
-             <number>2560</number>
+            <property name="styleSheet">
+             <string notr="true">border-color: rgb(61, 56, 70);</string>
             </property>
-            <property name="singleStep">
-             <number>8</number>
+            <property name="text">
+             <string/>
             </property>
-            <property name="value">
-             <number>8</number>
+            <property name="icon">
+             <iconset>
+              <normalon>../src/icons/arrow-up.png</normalon>
+             </iconset>
             </property>
-            <property name="displayIntegerBase">
-             <number>10</number>
+            <property name="iconSize">
+             <size>
+              <width>24</width>
+              <height>24</height>
+             </size>
             </property>
            </widget>
           </item>
-          <item row="2" column="0">
-           <widget class="QLabel" name="label">
-            <property name="font">
-             <font>
-              <pointsize>10</pointsize>
-              <bold>false</bold>
-             </font>
+          <item>
+           <widget class="QPushButton" name="layer_down_button">
+            <property name="sizePolicy">
+             <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
+              <horstretch>0</horstretch>
+              <verstretch>0</verstretch>
+             </sizepolicy>
+            </property>
+            <property name="minimumSize">
+             <size>
+              <width>32</width>
+              <height>32</height>
+             </size>
+            </property>
+            <property name="styleSheet">
+             <string notr="true">border-color: rgb(61, 56, 70);</string>
             </property>
             <property name="text">
-             <string>Height</string>
+             <string/>
             </property>
-            <property name="alignment">
-             <set>Qt::AlignCenter</set>
+            <property name="icon">
+             <iconset>
+              <normalon>../src/icons/arrow-down.png</normalon>
+             </iconset>
+            </property>
+            <property name="iconSize">
+             <size>
+              <width>24</width>
+              <height>24</height>
+             </size>
             </property>
            </widget>
           </item>
-          <item row="3" column="0">
-           <widget class="QSlider" name="height_slider">
+          <item>
+           <widget class="QPushButton" name="delete_layer_button">
             <property name="sizePolicy">
-             <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+             <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
               <horstretch>0</horstretch>
               <verstretch>0</verstretch>
              </sizepolicy>
             </property>
             <property name="minimumSize">
              <size>
-              <width>150</width>
-              <height>0</height>
-             </size>
-            </property>
-            <property name="sizeIncrement">
-             <size>
-              <width>0</width>
-              <height>0</height>
+              <width>32</width>
+              <height>32</height>
              </size>
             </property>
-            <property name="font">
-             <font>
-              <pointsize>11</pointsize>
-             </font>
+            <property name="styleSheet">
+             <string notr="true">border-color: rgb(61, 56, 70);</string>
             </property>
-            <property name="minimum">
-             <number>1</number>
+            <property name="text">
+             <string/>
             </property>
-            <property name="maximum">
-             <number>2560</number>
+            <property name="icon">
+             <iconset>
+              <normalon>../src/icons/delete.png</normalon>
+             </iconset>
             </property>
-            <property name="singleStep">
-             <number>8</number>
+            <property name="iconSize">
+             <size>
+              <width>24</width>
+              <height>24</height>
+             </size>
             </property>
-            <property name="pageStep">
-             <number>8</number>
+           </widget>
+          </item>
+         </layout>
+        </widget>
+       </item>
+       <item row="2" column="0">
+        <widget class="QGroupBox" name="groupBox_4">
+         <property name="minimumSize">
+          <size>
+           <width>0</width>
+           <height>25</height>
+          </size>
+         </property>
+         <property name="title">
+          <string>Brush color</string>
+         </property>
+         <layout class="QGridLayout" name="gridLayout_3">
+          <property name="leftMargin">
+           <number>9</number>
+          </property>
+          <property name="topMargin">
+           <number>9</number>
+          </property>
+          <property name="rightMargin">
+           <number>9</number>
+          </property>
+          <property name="bottomMargin">
+           <number>9</number>
+          </property>
+          <item row="2" column="0">
+           <widget class="QSlider" name="primary_brush_opacity_slider">
+            <property name="maximum">
+             <number>255</number>
             </property>
             <property name="value">
-             <number>8</number>
-            </property>
-            <property name="sliderPosition">
-             <number>8</number>
+             <number>255</number>
             </property>
             <property name="orientation">
              <enum>Qt::Horizontal</enum>
             </property>
-            <property name="tickInterval">
-             <number>8</number>
-            </property>
            </widget>
           </item>
-          <item row="3" column="1">
-           <widget class="QSpinBox" name="height_spinbox">
+          <item row="0" column="0">
+           <widget class="QToolButton" name="primary_color_button">
             <property name="sizePolicy">
-             <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+             <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
               <horstretch>0</horstretch>
               <verstretch>0</verstretch>
              </sizepolicy>
             </property>
-            <property name="minimumSize">
-             <size>
-              <width>0</width>
-              <height>0</height>
-             </size>
-            </property>
-            <property name="maximumSize">
-             <size>
-              <width>75</width>
-              <height>16777215</height>
-             </size>
+            <property name="text">
+             <string/>
             </property>
-            <property name="font">
-             <font>
-              <pointsize>11</pointsize>
-             </font>
+           </widget>
+          </item>
+          <item row="0" column="1">
+           <widget class="QToolButton" name="secondary_color_button">
+            <property name="sizePolicy">
+             <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+              <horstretch>0</horstretch>
+              <verstretch>0</verstretch>
+             </sizepolicy>
             </property>
-            <property name="minimum">
-             <number>8</number>
+            <property name="text">
+             <string/>
             </property>
+           </widget>
+          </item>
+          <item row="2" column="1">
+           <widget class="QSlider" name="secondary_brush_opacity_slider">
             <property name="maximum">
-             <number>2560</number>
-            </property>
-            <property name="singleStep">
-             <number>8</number>
+             <number>255</number>
             </property>
             <property name="value">
-             <number>8</number>
+             <number>255</number>
             </property>
-            <property name="displayIntegerBase">
-             <number>10</number>
+            <property name="orientation">
+             <enum>Qt::Horizontal</enum>
+            </property>
+           </widget>
+          </item>
+          <item row="3" column="1">
+           <widget class="QLabel" name="label_4">
+            <property name="text">
+             <string>Opacity 100</string>
+            </property>
+           </widget>
+          </item>
+          <item row="3" column="0">
+           <widget class="QLabel" name="label_3">
+            <property name="text">
+             <string>Opacity 100</string>
             </property>
            </widget>
           </item>
@@ -436,10 +497,10 @@
          </layout>
         </widget>
        </item>
-       <item row="6" column="0">
-        <widget class="QWidget" name="horizontalWidget" native="true">
+       <item row="0" column="0">
+        <widget class="QGroupBox" name="groupBox">
          <property name="sizePolicy">
-          <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+          <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
            <horstretch>0</horstretch>
            <verstretch>0</verstretch>
           </sizepolicy>
@@ -447,286 +508,218 @@
          <property name="layoutDirection">
           <enum>Qt::LeftToRight</enum>
          </property>
-         <layout class="QHBoxLayout" name="horizontalLayout_8">
-          <property name="leftMargin">
-           <number>0</number>
-          </property>
-          <property name="topMargin">
-           <number>0</number>
-          </property>
-          <property name="bottomMargin">
-           <number>0</number>
-          </property>
-          <item>
-           <widget class="QPushButton" name="new_layer">
-            <property name="sizePolicy">
-             <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
-              <horstretch>0</horstretch>
-              <verstretch>0</verstretch>
-             </sizepolicy>
-            </property>
-            <property name="minimumSize">
-             <size>
-              <width>32</width>
-              <height>32</height>
-             </size>
-            </property>
-            <property name="toolTip">
-             <string>Create a new layer</string>
-            </property>
-            <property name="styleSheet">
-             <string notr="true">border-color: rgb(61, 56, 70);</string>
+         <property name="autoFillBackground">
+          <bool>false</bool>
+         </property>
+         <property name="title">
+          <string>Active grid area</string>
+         </property>
+         <layout class="QFormLayout" name="formLayout">
+          <item row="0" column="0">
+           <widget class="QLabel" name="label_2">
+            <property name="font">
+             <font>
+              <pointsize>10</pointsize>
+              <bold>false</bold>
+             </font>
             </property>
             <property name="text">
-             <string/>
-            </property>
-            <property name="icon">
-             <iconset>
-              <normaloff>../src/icons/file-add.png</normaloff>
-              <normalon>../src/icons/file-add.png</normalon>
-              <disabledoff>../src/icons/file-add.png</disabledoff>
-              <disabledon>../src/icons/file-add.png</disabledon>../src/icons/file-add.png</iconset>
+             <string>Width</string>
             </property>
-            <property name="iconSize">
-             <size>
-              <width>24</width>
-              <height>24</height>
-             </size>
+            <property name="alignment">
+             <set>Qt::AlignCenter</set>
             </property>
            </widget>
           </item>
-          <item>
-           <widget class="QPushButton" name="layer_up_button">
+          <item row="1" column="0">
+           <widget class="QSlider" name="width_slider">
             <property name="sizePolicy">
-             <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
+             <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
               <horstretch>0</horstretch>
               <verstretch>0</verstretch>
              </sizepolicy>
             </property>
             <property name="minimumSize">
              <size>
-              <width>32</width>
-              <height>32</height>
+              <width>150</width>
+              <height>0</height>
              </size>
             </property>
-            <property name="styleSheet">
-             <string notr="true">border-color: rgb(61, 56, 70);</string>
-            </property>
-            <property name="text">
-             <string/>
-            </property>
-            <property name="icon">
-             <iconset>
-              <normalon>../src/icons/arrow-up.png</normalon>
-             </iconset>
+            <property name="minimum">
+             <number>8</number>
             </property>
-            <property name="iconSize">
-             <size>
-              <width>24</width>
-              <height>24</height>
-             </size>
+            <property name="maximum">
+             <number>2560</number>
             </property>
-           </widget>
-          </item>
-          <item>
-           <widget class="QPushButton" name="layer_down_button">
-            <property name="sizePolicy">
-             <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
-              <horstretch>0</horstretch>
-              <verstretch>0</verstretch>
-             </sizepolicy>
+            <property name="singleStep">
+             <number>8</number>
             </property>
-            <property name="minimumSize">
-             <size>
-              <width>32</width>
-              <height>32</height>
-             </size>
+            <property name="pageStep">
+             <number>8</number>
             </property>
-            <property name="styleSheet">
-             <string notr="true">border-color: rgb(61, 56, 70);</string>
+            <property name="value">
+             <number>8</number>
             </property>
-            <property name="text">
-             <string/>
+            <property name="orientation">
+             <enum>Qt::Horizontal</enum>
             </property>
-            <property name="icon">
-             <iconset>
-              <normalon>../src/icons/arrow-down.png</normalon>
-             </iconset>
+            <property name="tickPosition">
+             <enum>QSlider::NoTicks</enum>
             </property>
-            <property name="iconSize">
-             <size>
-              <width>24</width>
-              <height>24</height>
-             </size>
+            <property name="tickInterval">
+             <number>8</number>
             </property>
            </widget>
           </item>
-          <item>
-           <widget class="QPushButton" name="delete_layer_button">
+          <item row="1" column="1">
+           <widget class="QSpinBox" name="width_spinbox">
             <property name="sizePolicy">
-             <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+             <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
               <horstretch>0</horstretch>
               <verstretch>0</verstretch>
              </sizepolicy>
             </property>
             <property name="minimumSize">
              <size>
-              <width>32</width>
-              <height>32</height>
+              <width>0</width>
+              <height>0</height>
              </size>
             </property>
-            <property name="styleSheet">
-             <string notr="true">border-color: rgb(61, 56, 70);</string>
-            </property>
-            <property name="text">
-             <string/>
-            </property>
-            <property name="icon">
-             <iconset>
-              <normalon>../src/icons/delete.png</normalon>
-             </iconset>
-            </property>
-            <property name="iconSize">
+            <property name="maximumSize">
              <size>
-              <width>24</width>
-              <height>24</height>
+              <width>75</width>
+              <height>16777215</height>
              </size>
             </property>
-           </widget>
-          </item>
-         </layout>
-        </widget>
-       </item>
-       <item row="2" column="0">
-        <widget class="QGroupBox" name="groupBox_4">
-         <property name="minimumSize">
-          <size>
-           <width>0</width>
-           <height>25</height>
-          </size>
-         </property>
-         <property name="title">
-          <string>Brush color</string>
-         </property>
-         <layout class="QGridLayout" name="gridLayout_3">
-          <property name="leftMargin">
-           <number>9</number>
-          </property>
-          <property name="topMargin">
-           <number>9</number>
-          </property>
-          <property name="rightMargin">
-           <number>9</number>
-          </property>
-          <property name="bottomMargin">
-           <number>9</number>
-          </property>
-          <item row="2" column="0">
-           <widget class="QSlider" name="primary_brush_opacity_slider">
+            <property name="minimum">
+             <number>8</number>
+            </property>
             <property name="maximum">
-             <number>255</number>
+             <number>2560</number>
+            </property>
+            <property name="singleStep">
+             <number>8</number>
             </property>
             <property name="value">
-             <number>255</number>
+             <number>8</number>
             </property>
-            <property name="orientation">
-             <enum>Qt::Horizontal</enum>
+            <property name="displayIntegerBase">
+             <number>10</number>
             </property>
            </widget>
           </item>
-          <item row="0" column="0">
-           <widget class="QToolButton" name="primary_color_button">
-            <property name="sizePolicy">
-             <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
-              <horstretch>0</horstretch>
-              <verstretch>0</verstretch>
-             </sizepolicy>
+          <item row="2" column="0">
+           <widget class="QLabel" name="label">
+            <property name="font">
+             <font>
+              <pointsize>10</pointsize>
+              <bold>false</bold>
+             </font>
             </property>
             <property name="text">
-             <string/>
+             <string>Height</string>
+            </property>
+            <property name="alignment">
+             <set>Qt::AlignCenter</set>
             </property>
            </widget>
           </item>
-          <item row="0" column="1">
-           <widget class="QToolButton" name="secondary_color_button">
+          <item row="3" column="0">
+           <widget class="QSlider" name="height_slider">
             <property name="sizePolicy">
              <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
               <horstretch>0</horstretch>
               <verstretch>0</verstretch>
              </sizepolicy>
             </property>
-            <property name="text">
-             <string/>
+            <property name="minimumSize">
+             <size>
+              <width>150</width>
+              <height>0</height>
+             </size>
+            </property>
+            <property name="sizeIncrement">
+             <size>
+              <width>0</width>
+              <height>0</height>
+             </size>
+            </property>
+            <property name="font">
+             <font>
+              <pointsize>11</pointsize>
+             </font>
+            </property>
+            <property name="minimum">
+             <number>1</number>
             </property>
-           </widget>
-          </item>
-          <item row="2" column="1">
-           <widget class="QSlider" name="secondary_brush_opacity_slider">
             <property name="maximum">
-             <number>255</number>
+             <number>2560</number>
+            </property>
+            <property name="singleStep">
+             <number>8</number>
+            </property>
+            <property name="pageStep">
+             <number>8</number>
             </property>
             <property name="value">
-             <number>255</number>
+             <number>8</number>
+            </property>
+            <property name="sliderPosition">
+             <number>8</number>
             </property>
             <property name="orientation">
              <enum>Qt::Horizontal</enum>
             </property>
+            <property name="tickInterval">
+             <number>8</number>
+            </property>
            </widget>
           </item>
           <item row="3" column="1">
-           <widget class="QLabel" name="label_4">
-            <property name="text">
-             <string>Opacity 100</string>
+           <widget class="QSpinBox" name="height_spinbox">
+            <property name="sizePolicy">
+             <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+              <horstretch>0</horstretch>
+              <verstretch>0</verstretch>
+             </sizepolicy>
             </property>
-           </widget>
-          </item>
-          <item row="3" column="0">
-           <widget class="QLabel" name="label_3">
-            <property name="text">
-             <string>Opacity 100</string>
+            <property name="minimumSize">
+             <size>
+              <width>0</width>
+              <height>0</height>
+             </size>
+            </property>
+            <property name="maximumSize">
+             <size>
+              <width>75</width>
+              <height>16777215</height>
+             </size>
+            </property>
+            <property name="font">
+             <font>
+              <pointsize>11</pointsize>
+             </font>
+            </property>
+            <property name="minimum">
+             <number>8</number>
+            </property>
+            <property name="maximum">
+             <number>2560</number>
+            </property>
+            <property name="singleStep">
+             <number>8</number>
+            </property>
+            <property name="value">
+             <number>8</number>
+            </property>
+            <property name="displayIntegerBase">
+             <number>10</number>
             </property>
            </widget>
           </item>
          </layout>
         </widget>
        </item>
-       <item row="4" column="0">
-        <widget class="QScrollArea" name="layers">
-         <property name="frameShadow">
-          <enum>QFrame::Plain</enum>
-         </property>
-         <property name="widgetResizable">
-          <bool>true</bool>
-         </property>
-         <property name="alignment">
-          <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
-         </property>
-         <widget class="QWidget" name="scrollAreaWidgetContents">
-          <property name="geometry">
-           <rect>
-            <x>0</x>
-            <y>0</y>
-            <width>239</width>
-            <height>245</height>
-           </rect>
-          </property>
-          <property name="sizePolicy">
-           <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
-            <horstretch>0</horstretch>
-            <verstretch>0</verstretch>
-           </sizepolicy>
-          </property>
-          <layout class="QVBoxLayout" name="verticalLayout"/>
-         </widget>
-        </widget>
-       </item>
-       <item row="5" column="0">
-        <widget class="QCheckBox" name="singleLayerCheckbox">
-         <property name="text">
-          <string>Treat as single layer</string>
-         </property>
-        </widget>
-       </item>
       </layout>
      </widget>
     </item>
@@ -780,6 +773,20 @@
          </property>
         </spacer>
        </item>
+       <item>
+        <widget class="QLabel" name="debug_label">
+         <property name="text">
+          <string/>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QLabel" name="canvas_position">
+         <property name="text">
+          <string>0, 0</string>
+         </property>
+        </widget>
+       </item>
       </layout>
      </widget>
     </item>
@@ -1373,7 +1380,9 @@
     <addaction name="separator"/>
     <addaction name="actionGrid"/>
     <addaction name="actionCanvas_color"/>
+    <addaction name="separator"/>
     <addaction name="actionResize_on_Paste"/>
+    <addaction name="actionImage_to_new_layer"/>
     <addaction name="separator"/>
     <addaction name="actionAdvanced"/>
     <addaction name="separator"/>
@@ -1472,28 +1481,43 @@
    <property name="text">
     <string>Grid</string>
    </property>
+   <property name="toolTip">
+    <string>Grid settings</string>
+   </property>
   </action>
   <action name="actionPreferences">
    <property name="text">
-    <string>Preferences</string>
+    <string>Path Preferences</string>
+   </property>
+   <property name="toolTip">
+    <string>Modify model path and more</string>
    </property>
   </action>
   <action name="actionCanvas_color">
    <property name="text">
     <string>Canvas color</string>
    </property>
+   <property name="toolTip">
+    <string>Change the color of the canvas</string>
+   </property>
   </action>
   <action name="actionReset_Settings">
    <property name="text">
     <string>Reset Settings</string>
    </property>
+   <property name="toolTip">
+    <string>Reset all settings to default</string>
+   </property>
   </action>
   <action name="actionResize_on_Paste">
    <property name="checkable">
     <bool>true</bool>
    </property>
    <property name="text">
-    <string>Resize on Paste</string>
+    <string>Resize on Import</string>
+   </property>
+   <property name="toolTip">
+    <string>Resize the image to active grid area when importing</string>
    </property>
   </action>
   <action name="actionMemory">
@@ -1523,7 +1547,10 @@
   </action>
   <action name="actionAdvanced">
    <property name="text">
-    <string>Advanced</string>
+    <string>Memory Settings</string>
+   </property>
+   <property name="toolTip">
+    <string>Change memory settings</string>
    </property>
   </action>
   <action name="actionGaussian_Blur">
@@ -1618,6 +1645,28 @@
    <property name="text">
     <string>Extensions</string>
    </property>
+   <property name="toolTip">
+    <string>Install, update and delete extensions</string>
+   </property>
+  </action>
+  <action name="actionShow_image_preview">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Show image preview</string>
+   </property>
+  </action>
+  <action name="actionImage_to_new_layer">
+   <property name="checkable">
+    <bool>true</bool>
+   </property>
+   <property name="text">
+    <string>Image to new layer</string>
+   </property>
+   <property name="toolTip">
+    <string>Send generated images to new layer</string>
+   </property>
   </action>
  </widget>
  <resources>
diff --git a/src/airunner/pyqt/pixel_art.ui b/src/airunner/pyqt/pixel_art.ui
index 9b07e39e4..a34675309 100644
--- a/src/airunner/pyqt/pixel_art.ui
+++ b/src/airunner/pyqt/pixel_art.ui
@@ -108,13 +108,6 @@
     </layout>
    </item>
    <item row="1" column="0">
-    <widget class="QCheckBox" name="enabled">
-     <property name="text">
-      <string>Keep enabled</string>
-     </property>
-    </widget>
-   </item>
-   <item row="2" column="0">
     <widget class="QDialogButtonBox" name="buttonBox">
      <property name="orientation">
       <enum>Qt::Horizontal</enum>
diff --git a/src/airunner/pyqt/preferences.ui b/src/airunner/pyqt/preferences.ui
index 0707f2180..3424f7540 100644
--- a/src/airunner/pyqt/preferences.ui
+++ b/src/airunner/pyqt/preferences.ui
@@ -7,13 +7,23 @@
     <x>0</x>
     <y>0</y>
     <width>400</width>
-    <height>317</height>
+    <height>427</height>
    </rect>
   </property>
   <property name="windowTitle">
    <string>Dialog</string>
   </property>
   <layout class="QGridLayout" name="gridLayout">
+   <item row="1" column="0">
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+     </property>
+    </widget>
+   </item>
    <item row="0" column="0">
     <widget class="QFrame" name="frame">
      <property name="frameShape">
@@ -23,82 +33,128 @@
       <enum>QFrame::Raised</enum>
      </property>
      <layout class="QGridLayout" name="gridLayout_2">
-      <item row="0" column="0">
-       <widget class="QLabel" name="label">
+      <item row="4" column="0">
+       <widget class="QLabel" name="label_5">
         <property name="font">
          <font>
           <bold>true</bold>
          </font>
         </property>
         <property name="text">
-         <string>Stable Diffusion Path</string>
+         <string>Embeddings Path</string>
         </property>
        </widget>
       </item>
-      <item row="1" column="0">
-       <widget class="QLabel" name="label_2">
-        <property name="text">
-         <string>Optional folder where you store your custom Stable Diffusion models.</string>
-        </property>
-        <property name="alignment">
-         <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+      <item row="3" column="0">
+       <widget class="Line" name="line">
+        <property name="minimumSize">
+         <size>
+          <width>0</width>
+          <height>0</height>
+         </size>
         </property>
-        <property name="wordWrap">
-         <bool>true</bool>
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
         </property>
        </widget>
       </item>
       <item row="2" column="0">
-       <widget class="QLineEdit" name="sd_path"/>
+       <layout class="QHBoxLayout" name="horizontalLayout">
+        <item>
+         <widget class="QLineEdit" name="sd_path"/>
+        </item>
+        <item>
+         <widget class="QPushButton" name="browseButton">
+          <property name="text">
+           <string>Browse</string>
+          </property>
+         </widget>
+        </item>
+       </layout>
       </item>
-      <item row="3" column="0">
-       <widget class="QPushButton" name="browseButton">
+      <item row="10" column="0">
+       <layout class="QHBoxLayout" name="horizontalLayout_3">
+        <item>
+         <widget class="QLineEdit" name="extensions_path"/>
+        </item>
+        <item>
+         <widget class="QPushButton" name="extensions_browse_button">
+          <property name="text">
+           <string>Browse</string>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </item>
+      <item row="7" column="0">
+       <widget class="Line" name="line_2">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="0">
+       <widget class="QLabel" name="label_2">
         <property name="text">
-         <string>Browse</string>
+         <string>Folder that contains custom models</string>
         </property>
        </widget>
       </item>
-      <item row="4" column="0">
-       <widget class="QLabel" name="label_3">
+      <item row="6" column="0">
+       <layout class="QHBoxLayout" name="horizontalLayout_2">
+        <item>
+         <widget class="QLineEdit" name="embeddings_path"/>
+        </item>
+        <item>
+         <widget class="QPushButton" name="embeddings_browse_button">
+          <property name="text">
+           <string>Browse</string>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </item>
+      <item row="0" column="0">
+       <widget class="QLabel" name="label">
         <property name="font">
          <font>
           <bold>true</bold>
          </font>
         </property>
         <property name="text">
-         <string>Huggingface Access Token</string>
+         <string>Model Path</string>
         </property>
        </widget>
       </item>
-      <item row="5" column="0">
+      <item row="9" column="0">
        <widget class="QLabel" name="label_4">
         <property name="text">
-         <string>Used for private model access. You can get a token from https://huggingface.co/settings/token</string>
+         <string>Fold which will contain installed extensions</string>
         </property>
-        <property name="alignment">
-         <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+       </widget>
+      </item>
+      <item row="8" column="0">
+       <widget class="QLabel" name="label_3">
+        <property name="font">
+         <font>
+          <bold>true</bold>
+         </font>
         </property>
-        <property name="wordWrap">
-         <bool>true</bool>
+        <property name="text">
+         <string>Extensions Path</string>
         </property>
        </widget>
       </item>
-      <item row="6" column="0">
-       <widget class="QLineEdit" name="hf_token"/>
+      <item row="5" column="0">
+       <widget class="QLabel" name="label_6">
+        <property name="text">
+         <string>Folder that contains Textual Inversion files</string>
+        </property>
+       </widget>
       </item>
      </layout>
     </widget>
    </item>
-   <item row="1" column="0">
-    <widget class="QDialogButtonBox" name="buttonBox">
-     <property name="orientation">
-      <enum>Qt::Horizontal</enum>
-     </property>
-     <property name="standardButtons">
-      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
-     </property>
-    </widget>
-   </item>
   </layout>
  </widget>
  <resources/>
diff --git a/src/airunner/runai_client.py b/src/airunner/runai_client.py
index f74b4119a..ffb3ee3b7 100644
--- a/src/airunner/runai_client.py
+++ b/src/airunner/runai_client.py
@@ -63,6 +63,7 @@ def __init__(self, **kwargs):
         super().__init__(
             parent=kwargs.get("parent", None)
         )
+        self.app = kwargs.get("app", None)
         self.settings_manager = kwargs.get("settings_manager", None)
         self.quit_event = BooleanVar()
         self.queue = queue.Queue()
diff --git a/src/airunner/windows/base_window.py b/src/airunner/windows/base_window.py
index d4cd363c9..af6d67866 100644
--- a/src/airunner/windows/base_window.py
+++ b/src/airunner/windows/base_window.py
@@ -7,7 +7,8 @@ class BaseWindow:
     window_title = ""
     settings_manager = None
     template = None
-    def __init__(self, settings_manager):
+    def __init__(self, settings_manager, **kwargs):
+        self.app = kwargs.get("app", None)
         self.settings_manager = settings_manager
         settings_manager.disable_save()
         self.template = uic.loadUi(os.path.join(f"pyqt/{self.template_name}.ui"))
diff --git a/src/airunner/windows/extensions.py b/src/airunner/windows/extensions.py
index ce4278694..9fbba40cc 100644
--- a/src/airunner/windows/extensions.py
+++ b/src/airunner/windows/extensions.py
@@ -27,12 +27,14 @@ def initialize_window(self):
             extension_repo = extension.repo.get()
             name = extension_repo.split("/")[-1]
             base_path = self.settings_manager.settings.model_base_path.get()
-            extension_path = os.path.join(base_path, "extensions", name)
+            extensions_path = self.settings_manager.settings.extensions_path.get() or "extensions"
+            if extensions_path == "extensions":
+                extensions_path = os.path.join(base_path, extensions_path)
             widget.installButton.clicked.connect(
                 lambda x,
                 _repo=extension_repo,
                 _url=url,
-                _path=os.path.join(base_path, "extensions"),
+                _path=extensions_path,
                 _btn=widget.installButton,
                 _remove_btn=widget.removeButton:
                     self.install(_repo, _url, _path, _btn, _remove_btn)
@@ -41,10 +43,10 @@ def initialize_window(self):
                 lambda x,
                 _btn=widget.removeButton,
                 _install_btn=widget.installButton,
-                _pth=extension_path:
+                _pth=extensions_path:
                     self.remove(_pth, _btn, _install_btn)
             )
-            if os.path.exists(extension_path):
+            if os.path.exists(extensions_path):
                 # change the download button to "update"
                 widget.installButton.setText("Update")
                 # enable the remove button
diff --git a/src/airunner/windows/preferences.py b/src/airunner/windows/preferences.py
index 64a9295d5..44391df60 100644
--- a/src/airunner/windows/preferences.py
+++ b/src/airunner/windows/preferences.py
@@ -6,15 +6,44 @@ class PreferencesWindow(BaseWindow):
     template_name = "preferences"
     window_title = "Preferences"
 
+    def initialize_window(self):
+        self.template.browseButton.clicked.connect(
+            lambda: self.browse_for_model_base_path(self.template.sd_path))
+        self.template.sd_path.textChanged.connect(
+            lambda val: self.settings_manager.settings.model_base_path.set(val))
+        self.template.embeddings_path.textChanged.connect(
+            lambda val: self.settings_manager.settings.embeddings_path.set(val))
+        self.template.embeddings_browse_button.clicked.connect(
+            lambda: self.browse_for_embeddings_path(self.template.embeddings_path))
+        self.template.sd_path.setText(self.settings_manager.settings.model_base_path.get())
+        self.template.embeddings_path.setText(self.settings_manager.settings.embeddings_path.get())
+        # self.template.hf_token.textChanged.connect(
+        #     lambda val: self.settings_manager.settings.hf_api_key.set(val))
+        # self.template.hf_token.setText(self.settings_manager.settings.hf_api_key.get())
+        self.template.extensions_path.textChanged.connect(
+            lambda val: self.settings_manager.settings.extensions_path.set(val))
+        self.template.extensions_path.setText(
+            self.settings_manager.settings.extensions_path.get())
+        self.template.extensions_browse_button.clicked.connect(
+            lambda: self.browse_for_extensions_path(self.template.extensions_path))
+        self.app.do_preferences_injection(self)
+
+    def browse_for_extensions_path(self, line_edit):
+        path = QFileDialog.getExistingDirectory(None, "Select Directory")
+        line_edit.setText(path)
+        self.settings_manager.settings.extensions_path.set(path)
+
+    def browse_for_embeddings_path(self, line_edit):
+        path = QFileDialog.getExistingDirectory(None, "Select Directory")
+        line_edit.setText(path)
+        self.settings_manager.settings.embeddings_path.set(path)
+
+    def browse_for_lora_path(self, line_edit):
+        path = QFileDialog.getExistingDirectory(None, "Select Directory")
+        line_edit.setText(path)
+        self.settings_manager.settings.lora_path.set(path)
+
     def browse_for_model_base_path(self, line_edit):
         path = QFileDialog.getExistingDirectory(None, "Select Directory")
         line_edit.setText(path)
         self.settings_manager.settings.model_base_path.set(path)
-
-    def initialize_window(self):
-        self.template.sd_path.setText(self.settings_manager.settings.model_base_path.get())
-        self.template.browseButton.clicked.connect(lambda: self.browse_for_model_base_path(self.template.sd_path))
-        self.template.hf_token.setText(self.settings_manager.settings.hf_api_key.get())
-        self.template.hf_token.textChanged.connect(lambda val: self.settings_manager.settings.hf_api_key.set(val))
-        self.template.sd_path.textChanged.connect(
-            lambda val: self.settings_manager.settings.model_base_path.set(val))


Test patch:
diff --git a/src/airunner/tests/test_canvas_brushes_mixin.py b/src/airunner/tests/test_canvas_brushes_mixin.py
new file mode 100644
index 000000000..0a9ce26d4
--- /dev/null
+++ b/src/airunner/tests/test_canvas_brushes_mixin.py
@@ -0,0 +1,51 @@
+import unittest
+
+from PyQt6.QtCore import QPoint
+from PyQt6.QtGui import QPen
+
+from airunner.mixins.canvas_brushes_mixin import CanvasBrushesMixin
+from aihandler.settings_manager import SettingsManager
+from airunner.models.layerdata import LayerData
+from airunner.models.linedata import LineData
+
+
+class TestCanvasBrushesMixin(unittest.TestCase):
+    def setUp(self):
+        self.cbm = CanvasBrushesMixin()
+        self.cbm.pos_x = 0
+        self.cbm.pos_y = 0
+        self.cbm.settings_manager = SettingsManager()
+        self.cbm.settings_manager.settings.mask_brush_size.set(10)
+        self.cbm.current_layer = LayerData(0, "Untitled", True, 1.0, QPoint(0, 0))
+
+    def test_get_line_extremities(self):
+        self.cbm.current_layer.lines = [LineData(QPoint(-10, -10), QPoint(10, 10), QPen(), 0, 255)]
+        top, left, bottom, right = self.cbm.get_line_extremities()
+        self.assertEqual(top, -20)
+        self.assertEqual(left, -20)
+        self.assertEqual(bottom, 20)
+        self.assertEqual(right, 20)
+
+    # write another test for get_line_extremities with multiple lines
+    def test_get_line_extremities_multiple_lines(self):
+        self.cbm.current_layer.lines = [
+            LineData(QPoint(-10, -10), QPoint(10, 10), QPen(), 0, 255),
+            LineData(QPoint(-20, -20), QPoint(20, 20), QPen(), 0, 255)
+        ]
+        top, left, bottom, right = self.cbm.get_line_extremities()
+        self.assertEqual(top, -30)
+        self.assertEqual(left, -30)
+        self.assertEqual(bottom, 30)
+        self.assertEqual(right, 30)
+
+    # def test_get_line_extremities_multiple_lines_with_offset(self):
+    #     self.cbm.current_layer.lines = [
+    #         LineData(QPoint(-10, -10), QPoint(10, 10), QPen(), 0, 255),
+    #         LineData(QPoint(-20, -20), QPoint(20, 20), QPen(), 0, 255)
+    #     ]
+    #     self.cbm.current_layer.offset = QPoint(10, 10)
+    #     top, left, bottom, right = self.cbm.get_line_extremities()
+    #     self.assertEqual(top, -20)
+    #     self.assertEqual(left, -20)
+    #     self.assertEqual(bottom, 40)
+    #     self.assertEqual(right, 40)
\ No newline at end of file