Przeglądaj źródła

window manager

window manager

window manager

window manager

window manager

window manager

window manager

window manager
Milan Jurkulak 2 lat temu
rodzic
commit
9ea3863736
42 zmienionych plików z 3042 dodań i 342 usunięć
  1. 1 1
      build.gradle.kts
  2. 2 2
      gradle/libs.versions.toml
  3. 51 0
      src/main/java/eu/mjdev/desktop/helpers/image/filters/AbstractBufferedImageOp.java
  4. 99 0
      src/main/java/eu/mjdev/desktop/helpers/image/filters/BoxBlurFilter.java
  5. 237 0
      src/main/java/eu/mjdev/desktop/helpers/image/filters/ConvolveFilter.java
  6. 131 0
      src/main/java/eu/mjdev/desktop/helpers/image/filters/FFT.java
  7. 113 0
      src/main/java/eu/mjdev/desktop/helpers/image/filters/GaussianFilter.java
  8. 65 0
      src/main/java/eu/mjdev/desktop/helpers/image/filters/GlowFilter.java
  9. 398 0
      src/main/java/eu/mjdev/desktop/helpers/image/filters/ImageMath.java
  10. 240 0
      src/main/java/eu/mjdev/desktop/helpers/image/filters/LensBlurFilter.java
  11. 155 0
      src/main/java/eu/mjdev/desktop/helpers/image/filters/MotionBlurFilter.java
  12. 154 0
      src/main/java/eu/mjdev/desktop/helpers/image/filters/MotionBlurOp.java
  13. 211 0
      src/main/java/eu/mjdev/desktop/helpers/image/filters/PixelUtils.java
  14. 136 0
      src/main/java/eu/mjdev/desktop/helpers/image/filters/RaysFilter.java
  15. 148 0
      src/main/java/eu/mjdev/desktop/helpers/image/filters/ShadowFilter.java
  16. 125 0
      src/main/java/eu/mjdev/desktop/helpers/image/filters/SmartBlurFilter.java
  17. 90 0
      src/main/java/eu/mjdev/desktop/helpers/image/filters/UnsharpFilter.java
  18. 172 0
      src/main/java/eu/mjdev/desktop/helpers/image/filters/VariableBlurFilter.java
  19. 2 5
      src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenu.kt
  20. 8 14
      src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenuApp.kt
  21. 6 13
      src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenuCategory.kt
  22. 12 9
      src/main/kotlin/eu/mjdev/desktop/components/controlcenter/ControlCenter.kt
  23. 13 12
      src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/DesktopPanelIcon.kt
  24. 4 6
      src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/DesktopPanelText.kt
  25. 2 3
      src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/applets/DesktopPanelFavoriteApps.kt
  26. 7 19
      src/main/kotlin/eu/mjdev/desktop/components/icon/ShapedIcon.kt
  27. 0 1
      src/main/kotlin/eu/mjdev/desktop/components/main/MainWindow.kt
  28. 6 8
      src/main/kotlin/eu/mjdev/desktop/components/sliding/SlidingMenu.kt
  29. 36 14
      src/main/kotlin/eu/mjdev/desktop/data/App.kt
  30. 53 0
      src/main/kotlin/eu/mjdev/desktop/extensions/Compose.kt
  31. 12 2
      src/main/kotlin/eu/mjdev/desktop/extensions/Custom.kt
  32. 7 76
      src/main/kotlin/eu/mjdev/desktop/helpers/system/Shell.kt
  33. 3 0
      src/main/kotlin/eu/mjdev/desktop/provider/DesktopScope.kt
  34. 20 20
      src/main/kotlin/eu/mjdev/desktop/windows/ChromeWindow.kt
  35. 9 9
      src/main/kotlin/eu/mjdev/desktop/windows/DesktopWindow.kt
  36. 30 74
      src/main/kotlin/eu/mjdev/desktop/windows/Window.kt
  37. 0 54
      src/main/kotlin/eu/mjdev/desktop/windows/WindowEx.kt
  38. 37 0
      src/main/kotlin/eu/mjdev/desktop/windows/blur/WindowBlurManager.kt
  39. 85 0
      src/main/kotlin/eu/mjdev/desktop/windows/blur/base/HackedContentPane.kt
  40. 7 0
      src/main/kotlin/eu/mjdev/desktop/windows/blur/base/StubWindowBlurManager.kt
  41. 36 0
      src/main/kotlin/eu/mjdev/desktop/windows/blur/linux/BlurBackground.kt
  42. 119 0
      src/main/kotlin/eu/mjdev/desktop/windows/blur/linux/LinuxWindowBlurManager.kt

+ 1 - 1
build.gradle.kts

@@ -72,7 +72,7 @@ allprojects {
         // gemini ai
         implementation(libs.google.generativeai)
         // jna
-        implementation(libs.jna.platform)
+//        implementation(libs.jna.platform)
         // qr code
         implementation(libs.qrose)
         // tts

+ 2 - 2
gradle/libs.versions.toml

@@ -24,7 +24,7 @@ mozilla-rhino = "1.7.13"
 ini4j = "0.5.4"
 charts = "Beta-0.0.5"
 generativeai = "0.5.0-1.0.0-wasm"
-jna-platform = "5.6.0"
+#jna-platform = "5.6.0"
 qrose = "1.0.1"
 tts = "2.5.0"
 #korau = "4.0.10"
@@ -51,7 +51,7 @@ fuzzywuzzy = { module = "me.xdrop:fuzzywuzzy", version.ref = "fuzzywuzzy" }
 ffmpeg-platform = { module = "org.bytedeco:ffmpeg-platform", version.ref = "ffmpeg-platform" }
 charts = { module = "io.github.thechance101:chart", version.ref = "charts" }
 google-generativeai = { module = "dev.shreyaspatil.generativeai:generativeai-google-jvm", version.ref = "generativeai" }
-jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna-platform" }
+#jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna-platform" }
 qrose = { module = "io.github.alexzhirkevich:qrose", version.ref = "qrose" }
 tts = { module = "nl.marc-apps:tts", version.ref = "tts" }
 dbus-java-core = { module = "com.github.hypfvieh:dbus-java-core", version = "5.0.0" }

+ 51 - 0
src/main/java/eu/mjdev/desktop/helpers/image/filters/AbstractBufferedImageOp.java

@@ -0,0 +1,51 @@
+package eu.mjdev.desktop.helpers.image.filters;
+
+import java.awt.*;
+import java.awt.geom.*;
+import java.awt.image.*;
+
+@SuppressWarnings("ALL")
+public abstract class AbstractBufferedImageOp implements BufferedImageOp {
+    public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel dstCM) {
+        if (dstCM == null)
+            dstCM = src.getColorModel();
+        return new BufferedImage(
+                dstCM,
+                dstCM.createCompatibleWritableRaster(
+                        src.getWidth(),
+                        src.getHeight()),
+                dstCM.isAlphaPremultiplied(),
+                null
+        );
+    }
+
+    public Rectangle2D getBounds2D(BufferedImage src) {
+        return new Rectangle(0, 0, src.getWidth(), src.getHeight());
+    }
+
+    public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) {
+        if (dstPt == null)
+            dstPt = new Point2D.Double();
+        dstPt.setLocation(srcPt.getX(), srcPt.getY());
+        return dstPt;
+    }
+
+    public RenderingHints getRenderingHints() {
+        return null;
+    }
+
+    public int[] getRGB(BufferedImage image, int x, int y, int width, int height, int[] pixels) {
+        int type = image.getType();
+        if (type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB)
+            return (int[]) image.getRaster().getDataElements(x, y, width, height, pixels);
+        return image.getRGB(x, y, width, height, pixels, 0, width);
+    }
+
+    public void setRGB(BufferedImage image, int x, int y, int width, int height, int[] pixels) {
+        int type = image.getType();
+        if (type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB)
+            image.getRaster().setDataElements(x, y, width, height, pixels);
+        else
+            image.setRGB(x, y, width, height, pixels, 0, width);
+    }
+}

+ 99 - 0
src/main/java/eu/mjdev/desktop/helpers/image/filters/BoxBlurFilter.java

@@ -0,0 +1,99 @@
+package eu.mjdev.desktop.helpers.image.filters;
+
+import java.awt.image.*;
+
+@SuppressWarnings("ALL")
+public class BoxBlurFilter extends AbstractBufferedImageOp {
+	private int hRadius;
+	private int vRadius;
+	private int iterations = 1;
+	
+    public BufferedImage filter( BufferedImage src, BufferedImage dst ) {
+        int width = src.getWidth();
+        int height = src.getHeight();
+        if ( dst == null )
+            dst = createCompatibleDestImage( src, null );
+        int[] inPixels = new int[width*height];
+        int[] outPixels = new int[width*height];
+        getRGB( src, 0, 0, width, height, inPixels );
+        for (int i = 0; i < iterations; i++ ) {
+            blur( inPixels, outPixels, width, height, hRadius );
+            blur( outPixels, inPixels, height, width, vRadius );
+        }
+        setRGB( dst, 0, 0, width, height, inPixels );
+        return dst;
+    }
+
+    public static void blur( int[] in, int[] out, int width, int height, int radius ) {
+        int widthMinus1 = width-1;
+        int tableSize = 2*radius+1;
+        int[] divide = new int[256*tableSize];
+        for ( int i = 0; i < 256*tableSize; i++ )
+            divide[i] = i/tableSize;
+        int inIndex = 0;
+        for ( int y = 0; y < height; y++ ) {
+            int outIndex = y;
+            int ta = 0, tr = 0, tg = 0, tb = 0;
+            for ( int i = -radius; i <= radius; i++ ) {
+                int rgb = in[inIndex + ImageMath.clamp(i, 0, width-1)];
+                ta += (rgb >> 24) & 0xff;
+                tr += (rgb >> 16) & 0xff;
+                tg += (rgb >> 8) & 0xff;
+                tb += rgb & 0xff;
+            }
+            for ( int x = 0; x < width; x++ ) {
+                out[ outIndex ] = (divide[ta] << 24) | (divide[tr] << 16) | (divide[tg] << 8) | divide[tb];
+                int i1 = x+radius+1;
+                if ( i1 > widthMinus1 )
+                    i1 = widthMinus1;
+                int i2 = x-radius;
+                if ( i2 < 0 )
+                    i2 = 0;
+                int rgb1 = in[inIndex+i1];
+                int rgb2 = in[inIndex+i2];
+                ta += ((rgb1 >> 24) & 0xff)-((rgb2 >> 24) & 0xff);
+                tr += ((rgb1 & 0xff0000)-(rgb2 & 0xff0000)) >> 16;
+                tg += ((rgb1 & 0xff00)-(rgb2 & 0xff00)) >> 8;
+                tb += (rgb1 & 0xff)-(rgb2 & 0xff);
+                outIndex += height;
+            }
+            inIndex += width;
+        }
+    }
+        
+	public void setHRadius(int hRadius) {
+		this.hRadius = hRadius;
+	}
+	
+	public int getHRadius() {
+		return hRadius;
+	}
+	
+	public void setVRadius(int vRadius) {
+		this.vRadius = vRadius;
+	}
+	
+	public int getVRadius() {
+		return vRadius;
+	}
+	
+	public void setRadius(int radius) {
+		this.hRadius = this.vRadius = radius;
+	}
+	
+	public int getRadius() {
+		return hRadius;
+	}
+	
+	public void setIterations(int iterations) {
+		this.iterations = iterations;
+	}
+	
+	public int getIterations() {
+		return iterations;
+	}
+	
+	public String toString() {
+		return "Blur/Box Blur...";
+	}
+}

+ 237 - 0
src/main/java/eu/mjdev/desktop/helpers/image/filters/ConvolveFilter.java

@@ -0,0 +1,237 @@
+package eu.mjdev.desktop.helpers.image.filters;
+
+import java.awt.*;
+import java.awt.image.*;
+import java.awt.geom.*;
+
+@SuppressWarnings("ALL")
+public class ConvolveFilter extends AbstractBufferedImageOp {
+	static final long serialVersionUID = 2239251672685254626L;
+	public static int ZERO_EDGES = 0;
+	public static int CLAMP_EDGES = 1;
+	public static int WRAP_EDGES = 2;
+	protected Kernel kernel = null;
+	public boolean alpha = true;
+	private int edgeAction = CLAMP_EDGES;
+
+	public ConvolveFilter() {
+		this(new float[9]);
+	}
+
+	public ConvolveFilter(float[] matrix) {
+		this(new Kernel(3, 3, matrix));
+	}
+	
+	public ConvolveFilter(int rows, int cols, float[] matrix) {
+		this(new Kernel(cols, rows, matrix));
+	}
+	
+	public ConvolveFilter(Kernel kernel) {
+		this.kernel = kernel;	
+	}
+
+	public void setKernel(Kernel kernel) {
+		this.kernel = kernel;
+	}
+
+	public Kernel getKernel() {
+		return kernel;
+	}
+
+	public void setEdgeAction(int edgeAction) {
+		this.edgeAction = edgeAction;
+	}
+
+	public int getEdgeAction() {
+		return edgeAction;
+	}
+
+    public BufferedImage filter( BufferedImage src, BufferedImage dst ) {
+        int width = src.getWidth();
+        int height = src.getHeight();
+        if ( dst == null )
+            dst = createCompatibleDestImage( src, null );
+        int[] inPixels = new int[width*height];
+        int[] outPixels = new int[width*height];
+        getRGB( src, 0, 0, width, height, inPixels );
+		convolve(kernel, inPixels, outPixels, width, height, alpha, edgeAction);
+        setRGB( dst, 0, 0, width, height, outPixels );
+        return dst;
+    }
+
+    public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel dstCM) {
+        if ( dstCM == null )
+            dstCM = src.getColorModel();
+        return new BufferedImage(dstCM, dstCM.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), dstCM.isAlphaPremultiplied(), null);
+    }
+    
+    public Rectangle2D getBounds2D( BufferedImage src ) {
+        return new Rectangle(0, 0, src.getWidth(), src.getHeight());
+    }
+    
+    public Point2D getPoint2D( Point2D srcPt, Point2D dstPt ) {
+        if ( dstPt == null )
+            dstPt = new Point2D.Double();
+        dstPt.setLocation( srcPt.getX(), srcPt.getY() );
+        return dstPt;
+    }
+
+    public RenderingHints getRenderingHints() {
+        return null;
+    }
+
+	public static void convolve(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, int edgeAction) {
+		convolve(kernel, inPixels, outPixels, width, height, true, edgeAction);
+	}
+	
+	public static void convolve(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) {
+		if (kernel.getHeight() == 1)
+			convolveH(kernel, inPixels, outPixels, width, height, alpha, edgeAction);
+		else if (kernel.getWidth() == 1)
+			convolveV(kernel, inPixels, outPixels, width, height, alpha, edgeAction);
+		else
+			convolveHV(kernel, inPixels, outPixels, width, height, alpha, edgeAction);
+	}
+	
+	public static void convolveHV(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) {
+		int index = 0;
+		float[] matrix = kernel.getKernelData( null );
+		int rows = kernel.getHeight();
+		int cols = kernel.getWidth();
+		int rows2 = rows/2;
+		int cols2 = cols/2;
+		for (int y = 0; y < height; y++) {
+			for (int x = 0; x < width; x++) {
+				float r = 0, g = 0, b = 0, a = 0;
+				for (int row = -rows2; row <= rows2; row++) {
+					int iy = y+row;
+					int ioffset;
+					if (0 <= iy && iy < height)
+						ioffset = iy*width;
+					else if ( edgeAction == CLAMP_EDGES )
+						ioffset = y*width;
+					else if ( edgeAction == WRAP_EDGES )
+						ioffset = ((iy+height) % height) * width;
+					else
+						continue;
+					int moffset = cols*(row+rows2)+cols2;
+					for (int col = -cols2; col <= cols2; col++) {
+						float f = matrix[moffset+col];
+						if (f != 0) {
+							int ix = x+col;
+							if (!(0 <= ix && ix < width)) {
+								if ( edgeAction == CLAMP_EDGES )
+									ix = x;
+								else if ( edgeAction == WRAP_EDGES )
+									ix = (x+width) % width;
+								else
+									continue;
+							}
+							int rgb = inPixels[ioffset+ix];
+							a += f * ((rgb >> 24) & 0xff);
+							r += f * ((rgb >> 16) & 0xff);
+							g += f * ((rgb >> 8) & 0xff);
+							b += f * (rgb & 0xff);
+						}
+					}
+				}
+				int ia = alpha ? PixelUtils.clamp((int)(a+0.5)) : 0xff;
+				int ir = PixelUtils.clamp((int)(r+0.5));
+				int ig = PixelUtils.clamp((int)(g+0.5));
+				int ib = PixelUtils.clamp((int)(b+0.5));
+				outPixels[index++] = (ia << 24) | (ir << 16) | (ig << 8) | ib;
+			}
+		}
+	}
+
+	public static void convolveH(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) {
+		int index = 0;
+		float[] matrix = kernel.getKernelData( null );
+		int cols = kernel.getWidth();
+		int cols2 = cols/2;
+		for (int y = 0; y < height; y++) {
+			int ioffset = y*width;
+			for (int x = 0; x < width; x++) {
+				float r = 0, g = 0, b = 0, a = 0;
+				int moffset = cols2;
+				for (int col = -cols2; col <= cols2; col++) {
+					float f = matrix[moffset+col];
+
+					if (f != 0) {
+						int ix = x+col;
+						if ( ix < 0 ) {
+							if ( edgeAction == CLAMP_EDGES )
+								ix = 0;
+							else if ( edgeAction == WRAP_EDGES )
+								ix = (x+width) % width;
+						} else if ( ix >= width) {
+							if ( edgeAction == CLAMP_EDGES )
+								ix = width-1;
+							else if ( edgeAction == WRAP_EDGES )
+								ix = (x+width) % width;
+						}
+						int rgb = inPixels[ioffset+ix];
+						a += f * ((rgb >> 24) & 0xff);
+						r += f * ((rgb >> 16) & 0xff);
+						g += f * ((rgb >> 8) & 0xff);
+						b += f * (rgb & 0xff);
+					}
+				}
+				int ia = alpha ? PixelUtils.clamp((int)(a+0.5)) : 0xff;
+				int ir = PixelUtils.clamp((int)(r+0.5));
+				int ig = PixelUtils.clamp((int)(g+0.5));
+				int ib = PixelUtils.clamp((int)(b+0.5));
+				outPixels[index++] = (ia << 24) | (ir << 16) | (ig << 8) | ib;
+			}
+		}
+	}
+
+	public static void convolveV(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) {
+		int index = 0;
+		float[] matrix = kernel.getKernelData( null );
+		int rows = kernel.getHeight();
+		int rows2 = rows/2;
+		for (int y = 0; y < height; y++) {
+			for (int x = 0; x < width; x++) {
+				float r = 0, g = 0, b = 0, a = 0;
+				for (int row = -rows2; row <= rows2; row++) {
+					int iy = y+row;
+					int ioffset;
+					if ( iy < 0 ) {
+						if ( edgeAction == CLAMP_EDGES )
+							ioffset = 0;
+						else if ( edgeAction == WRAP_EDGES )
+							ioffset = ((y+height) % height)*width;
+						else
+							ioffset = iy*width;
+					} else if ( iy >= height) {
+						if ( edgeAction == CLAMP_EDGES )
+							ioffset = (height-1)*width;
+						else if ( edgeAction == WRAP_EDGES )
+							ioffset = ((y+height) % height)*width;
+						else
+							ioffset = iy*width;
+					} else
+						ioffset = iy*width;
+					float f = matrix[row+rows2];
+					if (f != 0) {
+						int rgb = inPixels[ioffset+x];
+						a += f * ((rgb >> 24) & 0xff);
+						r += f * ((rgb >> 16) & 0xff);
+						g += f * ((rgb >> 8) & 0xff);
+						b += f * (rgb & 0xff);
+					}
+				}
+				int ia = alpha ? PixelUtils.clamp((int)(a+0.5)) : 0xff;
+				int ir = PixelUtils.clamp((int)(r+0.5));
+				int ig = PixelUtils.clamp((int)(g+0.5));
+				int ib = PixelUtils.clamp((int)(b+0.5));
+				outPixels[index++] = (ia << 24) | (ir << 16) | (ig << 8) | ib;
+			}
+		}
+	}
+
+	public String toString() {
+		return "Blur/Convolve...";
+	}
+}

+ 131 - 0
src/main/java/eu/mjdev/desktop/helpers/image/filters/FFT.java

@@ -0,0 +1,131 @@
+/*
+** Copyright 2005 Huxtable.com. All rights reserved.
+*/
+
+package eu.mjdev.desktop.helpers.image.filters;
+
+@SuppressWarnings("ALL")
+public class FFT {
+    protected float[] w1;
+    protected float[] w2;
+    protected float[] w3;
+
+    public FFT( int logN ) {
+        // Prepare the weighting factors
+        w1 = new float[logN];
+        w2 = new float[logN];
+        w3 = new float[logN];
+        int N = 1;
+        for ( int k = 0; k < logN; k++ ) {
+            N <<= 1;
+            double angle = -2.0 * Math.PI / N;
+            w1[k] = (float)Math.sin(0.5 * angle);
+            w2[k] = -2.0f * w1[k] * w1[k];
+            w3[k] = (float)Math.sin(angle);
+        }
+    }
+
+    private void scramble( int n, float[] real, float[] imag ) {
+        int j = 0;
+        for ( int i = 0; i < n; i++ ) {
+            if ( i > j ) {
+                float t;
+                t = real[j];
+                real[j] = real[i];
+                real[i] = t;
+                t = imag[j];
+                imag[j] = imag[i];
+                imag[i] = t;
+            }
+            int m = n >> 1;
+            while (j >= m && m >= 2) {
+                j -= m;
+                m >>= 1;
+            }
+            j += m;
+        }
+    }
+
+    private void butterflies( int n, int logN, int direction, float[] real, float[] imag ) {
+        int N = 1;
+        for ( int k = 0; k < logN; k++ ) {
+            float w_re, w_im, wp_re, wp_im, temp_re, temp_im, wt;
+            int half_N = N;
+            N <<= 1;
+            wt = direction * w1[k];
+            wp_re = w2[k];
+            wp_im = direction * w3[k];
+            w_re = 1.0f;
+            w_im = 0.0f;
+            for ( int offset = 0; offset < half_N; offset++ ) {
+                for( int i = offset; i < n; i += N ) {
+                    int j = i + half_N;
+                    float re = real[j];
+                    float im = imag[j];
+                    temp_re = (w_re * re) - (w_im * im);
+                    temp_im = (w_im * re) + (w_re * im);
+                    real[j] = real[i] - temp_re;
+                    real[i] += temp_re;
+                    imag[j] = imag[i] - temp_im;
+                    imag[i] += temp_im;
+                }
+                wt = w_re;
+                w_re = wt * wp_re - w_im * wp_im + w_re;
+                w_im = w_im * wp_re + wt * wp_im + w_im;
+            }
+        }
+        if ( direction == -1 ) {
+            float nr = 1.0f / n;
+            for ( int i = 0; i < n; i++ ) {
+                real[i] *= nr;
+                imag[i] *= nr;
+            }
+        }
+    }
+
+    public void transform1D( float[] real, float[] imag, int logN, int n, boolean forward ) {
+        scramble( n, real, imag );
+        butterflies( n, logN, forward ? 1 : -1, real, imag );
+    }
+
+    public void transform2D( float[] real, float[] imag, int cols, int rows, boolean forward ) {
+        int log2cols = log2(cols);
+        int log2rows = log2(rows);
+        int n = Math.max(rows, cols);
+        float[] rtemp = new float[n];
+        float[] itemp = new float[n];
+        for ( int y = 0; y < rows; y++ ) {
+            int offset = y*cols;
+            System.arraycopy( real, offset, rtemp, 0, cols );
+            System.arraycopy( imag, offset, itemp, 0, cols );
+            transform1D(rtemp, itemp, log2cols, cols, forward);
+            System.arraycopy( rtemp, 0, real, offset, cols );
+            System.arraycopy( itemp, 0, imag, offset, cols );
+        }
+        for ( int x = 0; x < cols; x++ ) {
+            int index = x;
+            for ( int y = 0; y < rows; y++ ) {
+                rtemp[y] = real[index];
+                itemp[y] = imag[index];
+                index += cols;
+            }
+            transform1D(rtemp, itemp, log2rows, rows, forward);
+            index = x;
+            for ( int y = 0; y < rows; y++ ) {
+                real[index] = rtemp[y];
+                imag[index] = itemp[y];
+                index += cols;
+            }
+        }
+    }
+
+    private int log2( int n ) {
+        int m = 1;
+        int log2n = 0;
+        while (m < n) {
+            m *= 2;
+            log2n++;
+        }
+        return m == n ? log2n : -1;
+    }
+}

+ 113 - 0
src/main/java/eu/mjdev/desktop/helpers/image/filters/GaussianFilter.java

@@ -0,0 +1,113 @@
+package eu.mjdev.desktop.helpers.image.filters;
+
+import java.awt.image.*;
+
+@SuppressWarnings("ALL")
+public class GaussianFilter extends ConvolveFilter {
+    static final long serialVersionUID = 5377089073023183684L;
+    protected float radius;
+    protected Kernel kernel;
+
+    public GaussianFilter() {
+        this(2);
+    }
+
+    public GaussianFilter(float radius) {
+        setRadius(radius);
+    }
+
+    public void setRadius(float radius) {
+        this.radius = radius;
+        kernel = makeKernel(radius);
+    }
+
+    public float getRadius() {
+        return radius;
+    }
+
+    public BufferedImage filter(BufferedImage src, BufferedImage dst) {
+        int width = src.getWidth();
+        int height = src.getHeight();
+        if (dst == null)
+            dst = createCompatibleDestImage(src, null);
+        int[] inPixels = new int[width * height];
+        int[] outPixels = new int[width * height];
+        src.getRGB(0, 0, width, height, inPixels, 0, width);
+        convolveAndTranspose(kernel, inPixels, outPixels, width, height, alpha, CLAMP_EDGES);
+        convolveAndTranspose(kernel, outPixels, inPixels, height, width, alpha, CLAMP_EDGES);
+        dst.setRGB(0, 0, width, height, inPixels, 0, width);
+        return dst;
+    }
+
+    public static void convolveAndTranspose(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha, int edgeAction) {
+        float[] matrix = kernel.getKernelData(null);
+        int cols = kernel.getWidth();
+        int cols2 = cols / 2;
+        for (int y = 0; y < height; y++) {
+            int index = y;
+            int ioffset = y * width;
+            for (int x = 0; x < width; x++) {
+                float r = 0, g = 0, b = 0, a = 0;
+                int moffset = cols2;
+                for (int col = -cols2; col <= cols2; col++) {
+                    float f = matrix[moffset + col];
+                    if (f != 0) {
+                        int ix = x + col;
+                        if (ix < 0) {
+                            if (edgeAction == CLAMP_EDGES)
+                                ix = 0;
+                            else if (edgeAction == WRAP_EDGES)
+                                ix = (x + width) % width;
+                        } else if (ix >= width) {
+                            if (edgeAction == CLAMP_EDGES)
+                                ix = width - 1;
+                            else if (edgeAction == WRAP_EDGES)
+                                ix = (x + width) % width;
+                        }
+                        int rgb = inPixels[ioffset + ix];
+                        a += f * ((rgb >> 24) & 0xff);
+                        r += f * ((rgb >> 16) & 0xff);
+                        g += f * ((rgb >> 8) & 0xff);
+                        b += f * (rgb & 0xff);
+                    }
+                }
+                int ia = alpha ? PixelUtils.clamp((int) (a + 0.5)) : 0xff;
+                int ir = PixelUtils.clamp((int) (r + 0.5));
+                int ig = PixelUtils.clamp((int) (g + 0.5));
+                int ib = PixelUtils.clamp((int) (b + 0.5));
+                outPixels[index] = (ia << 24) | (ir << 16) | (ig << 8) | ib;
+                index += height;
+            }
+        }
+    }
+
+    public static Kernel makeKernel(float radius) {
+        int r = (int) Math.ceil(radius);
+        int rows = r * 2 + 1;
+        float[] matrix = new float[rows];
+        float sigma = radius / 3;
+        float sigma22 = 2 * sigma * sigma;
+        float sigmaPi2 = 2 * ImageMath.PI * sigma;
+        float sqrtSigmaPi2 = (float) Math.sqrt(sigmaPi2);
+        float radius2 = radius * radius;
+        float total = 0;
+        int index = 0;
+        for (int row = -r; row <= r; row++) {
+            float distance = row * row;
+            if (distance > radius2)
+                matrix[index] = 0;
+            else
+                matrix[index] = (float) Math.exp(-(distance) / sigma22) / sqrtSigmaPi2;
+            total += matrix[index];
+            index++;
+        }
+        for (int i = 0; i < rows; i++)
+            matrix[i] /= total;
+
+        return new Kernel(rows, 1, matrix);
+    }
+
+    public String toString() {
+        return "Blur/Gaussian Blur...";
+    }
+}

+ 65 - 0
src/main/java/eu/mjdev/desktop/helpers/image/filters/GlowFilter.java

@@ -0,0 +1,65 @@
+/*
+ ** Copyright 2005 Huxtable.com. All rights reserved.
+ */
+
+package eu.mjdev.desktop.helpers.image.filters;
+
+import java.awt.image.*;
+
+@SuppressWarnings("ALL")
+public class GlowFilter extends GaussianFilter {
+    static final long serialVersionUID = 5377089073023183684L;
+    private float amount = 0.5f;
+
+    public GlowFilter() {
+        radius = 2;
+    }
+
+    public void setAmount(float amount) {
+        this.amount = amount;
+    }
+
+    public float getAmount() {
+        return amount;
+    }
+
+    public BufferedImage filter(BufferedImage src, BufferedImage dst) {
+        int width = src.getWidth();
+        int height = src.getHeight();
+        if (dst == null)
+            dst = createCompatibleDestImage(src, null);
+        int[] inPixels = new int[width * height];
+        int[] outPixels = new int[width * height];
+        src.getRGB(0, 0, width, height, inPixels, 0, width);
+        if (radius > 0) {
+            convolveAndTranspose(kernel, inPixels, outPixels, width, height, alpha, CLAMP_EDGES);
+            convolveAndTranspose(kernel, outPixels, inPixels, height, width, alpha, CLAMP_EDGES);
+        }
+        src.getRGB(0, 0, width, height, outPixels, 0, width);
+        float a = 4 * amount;
+        int index = 0;
+        for (int y = 0; y < height; y++) {
+            for (int x = 0; x < width; x++) {
+                int rgb1 = outPixels[index];
+                int r1 = (rgb1 >> 16) & 0xff;
+                int g1 = (rgb1 >> 8) & 0xff;
+                int b1 = rgb1 & 0xff;
+                int rgb2 = inPixels[index];
+                int r2 = (rgb2 >> 16) & 0xff;
+                int g2 = (rgb2 >> 8) & 0xff;
+                int b2 = rgb2 & 0xff;
+                r1 = PixelUtils.clamp((int) (r1 + a * r2));
+                g1 = PixelUtils.clamp((int) (g1 + a * g2));
+                b1 = PixelUtils.clamp((int) (b1 + a * b2));
+                inPixels[index] = (rgb1 & 0xff000000) | (r1 << 16) | (g1 << 8) | b1;
+                index++;
+            }
+        }
+        dst.setRGB(0, 0, width, height, inPixels, 0, width);
+        return dst;
+    }
+
+    public String toString() {
+        return "Blur/Glow...";
+    }
+}

+ 398 - 0
src/main/java/eu/mjdev/desktop/helpers/image/filters/ImageMath.java

@@ -0,0 +1,398 @@
+package eu.mjdev.desktop.helpers.image.filters;
+
+@SuppressWarnings("ALL")
+public class ImageMath {
+	public final static float PI = (float)Math.PI;
+	public final static float HALF_PI = (float)Math.PI/2.0f;
+	public final static float QUARTER_PI = (float)Math.PI/4.0f;
+	public final static float TWO_PI = (float)Math.PI*2.0f;
+
+	public static float bias(float a, float b) {
+//		return (float)Math.pow(a, Math.log(b) / Math.log(0.5));
+		return a/((1.0f/b-2)*(1.0f-a)+1);
+	}
+
+	public static float gain(float a, float b) {
+/*
+		float p = (float)Math.log(1.0 - b) / (float)Math.log(0.5);
+
+		if (a < .001)
+			return 0.0f;
+		else if (a > .999)
+			return 1.0f;
+		if (a < 0.5)
+			return (float)Math.pow(2 * a, p) / 2;
+		else
+			return 1.0f - (float)Math.pow(2 * (1. - a), p) / 2;
+*/
+		float c = (1.0f/b-2.0f) * (1.0f-2.0f*a);
+		if (a < 0.5)
+			return a/(c+1.0f);
+		else
+			return (c-a)/(c-1.0f);
+	}
+
+	public static float step(float a, float x) {
+		return (x < a) ? 0.0f : 1.0f;
+	}
+
+	public static float pulse(float a, float b, float x) {
+		return (x < a || x >= b) ? 0.0f : 1.0f;
+	}
+
+	public static float smoothPulse(float a1, float a2, float b1, float b2, float x) {
+		if (x < a1 || x >= b2)
+			return 0;
+		if (x >= a2) {
+			if (x < b1)
+				return 1.0f;
+			x = (x - b1) / (b2 - b1);
+			return 1.0f - (x*x * (3.0f - 2.0f*x));
+		}
+		x = (x - a1) / (a2 - a1);
+		return x*x * (3.0f - 2.0f*x);
+	}
+
+	public static float smoothStep(float a, float b, float x) {
+		if (x < a)
+			return 0;
+		if (x >= b)
+			return 1;
+		x = (x - a) / (b - a);
+		return x*x * (3 - 2*x);
+	}
+
+	public static float circleUp(float x) {
+		x = 1-x;
+		return (float)Math.sqrt(1-x*x);
+	}
+
+	public static float circleDown(float x) {
+		return 1.0f-(float)Math.sqrt(1-x*x);
+	}
+
+	public static float clamp(float x, float a, float b) {
+		return (x < a) ? a : (x > b) ? b : x;
+	}
+
+	public static int clamp(int x, int a, int b) {
+		return (x < a) ? a : (x > b) ? b : x;
+	}
+
+	public static double mod(double a, double b) {
+		int n = (int)(a/b);
+		a -= n*b;
+		if (a < 0)
+			return a + b;
+		return a;
+	}
+
+	public static float mod(float a, float b) {
+		int n = (int)(a/b);
+		a -= n*b;
+		if (a < 0)
+			return a + b;
+		return a;
+	}
+
+	public static int mod(int a, int b) {
+		int n = a/b;
+		a -= n*b;
+		if (a < 0)
+			return a + b;
+		return a;
+	}
+
+	public static float triangle(float x) {
+		float r = mod(x, 1.0f);
+		return 2.0f*(r < 0.5 ? r : 1-r);
+	}
+
+	public static float lerp(float t, float a, float b) {
+		return a + t * (b - a);
+	}
+	
+	public static int lerp(float t, int a, int b) {
+		return (int)(a + t * (b - a));
+	}
+
+	public static int mixColors(float t, int rgb1, int rgb2) {
+		int a1 = (rgb1 >> 24) & 0xff;
+		int r1 = (rgb1 >> 16) & 0xff;
+		int g1 = (rgb1 >> 8) & 0xff;
+		int b1 = rgb1 & 0xff;
+		int a2 = (rgb2 >> 24) & 0xff;
+		int r2 = (rgb2 >> 16) & 0xff;
+		int g2 = (rgb2 >> 8) & 0xff;
+		int b2 = rgb2 & 0xff;
+		a1 = lerp(t, a1, a2);
+		r1 = lerp(t, r1, r2);
+		g1 = lerp(t, g1, g2);
+		b1 = lerp(t, b1, b2);
+		return (a1 << 24) | (r1 << 16) | (g1 << 8) | b1;
+	}
+
+	public static int bilinearInterpolate(float x, float y, int[] p) {
+		float m0, m1;
+		int a0 = (p[0] >> 24) & 0xff;
+		int r0 = (p[0] >> 16) & 0xff;
+		int g0 = (p[0] >> 8) & 0xff;
+		int b0 = p[0] & 0xff;
+		int a1 = (p[1] >> 24) & 0xff;
+		int r1 = (p[1] >> 16) & 0xff;
+		int g1 = (p[1] >> 8) & 0xff;
+		int b1 = p[1] & 0xff;
+		int a2 = (p[2] >> 24) & 0xff;
+		int r2 = (p[2] >> 16) & 0xff;
+		int g2 = (p[2] >> 8) & 0xff;
+		int b2 = p[2] & 0xff;
+		int a3 = (p[3] >> 24) & 0xff;
+		int r3 = (p[3] >> 16) & 0xff;
+		int g3 = (p[3] >> 8) & 0xff;
+		int b3 = p[3] & 0xff;
+		float cx = 1.0f-x;
+		float cy = 1.0f-y;
+		m0 = cx * a0 + x * a1;
+		m1 = cx * a2 + x * a3;
+		int a = (int)(cy * m0 + y * m1);
+		m0 = cx * r0 + x * r1;
+		m1 = cx * r2 + x * r3;
+		int r = (int)(cy * m0 + y * m1);
+		m0 = cx * g0 + x * g1;
+		m1 = cx * g2 + x * g3;
+		int g = (int)(cy * m0 + y * m1);
+		m0 = cx * b0 + x * b1;
+		m1 = cx * b2 + x * b3;
+		int b = (int)(cy * m0 + y * m1);
+		return (a << 24) | (r << 16) | (g << 8) | b;
+	}
+
+	public static int brightnessNTSC(int rgb) {
+		int r = (rgb >> 16) & 0xff;
+		int g = (rgb >> 8) & 0xff;
+		int b = rgb & 0xff;
+		return (int)(r*0.299f + g*0.587f + b*0.114f);
+	}
+	
+	private final static float m00 = -0.5f;
+	private final static float m01 =  1.5f;
+	private final static float m02 = -1.5f;
+	private final static float m03 =  0.5f;
+	private final static float m10 =  1.0f;
+	private final static float m11 = -2.5f;
+	private final static float m12 =  2.0f;
+	private final static float m13 = -0.5f;
+	private final static float m20 = -0.5f;
+	private final static float m21 =  0.0f;
+	private final static float m22 =  0.5f;
+	private final static float m23 =  0.0f;
+	private final static float m30 =  0.0f;
+	private final static float m31 =  1.0f;
+	private final static float m32 =  0.0f;
+	private final static float m33 =  0.0f;
+
+	public static float spline(float x, int numKnots, float[] knots) {
+		int span;
+		int numSpans = numKnots - 3;
+		float k0, k1, k2, k3;
+		float c0, c1, c2, c3;
+		if (numSpans < 1)
+			throw new IllegalArgumentException("Too few knots in spline");
+		x = clamp(x, 0, 1) * numSpans;
+		span = (int)x;
+		if (span > numKnots-4)
+			span = numKnots-4;
+		x -= span;
+		k0 = knots[span];
+		k1 = knots[span+1];
+		k2 = knots[span+2];
+		k3 = knots[span+3];
+		c3 = m00*k0 + m01*k1 + m02*k2 + m03*k3;
+		c2 = m10*k0 + m11*k1 + m12*k2 + m13*k3;
+		c1 = m20*k0 + m21*k1 + m22*k2 + m23*k3;
+		c0 = m30*k0 + m31*k1 + m32*k2 + m33*k3;
+		return ((c3*x + c2)*x + c1)*x + c0;
+	}
+	
+	public static float spline(float x, int numKnots, int[] xknots, int[] yknots) {
+		int span;
+		int numSpans = numKnots - 3;
+		float k0, k1, k2, k3;
+		float c0, c1, c2, c3;
+		if (numSpans < 1)
+			throw new IllegalArgumentException("Too few knots in spline");
+		for (span = 0; span < numSpans; span++)
+			if (xknots[span+1] > x)
+				break;
+		if (span > numKnots-3)
+			span = numKnots-3;
+		float t = (float)(x-xknots[span]) / (xknots[span+1]-xknots[span]);
+		span--;
+		if (span < 0) {
+			span = 0;
+			t = 0;
+		}
+		k0 = yknots[span];
+		k1 = yknots[span+1];
+		k2 = yknots[span+2];
+		k3 = yknots[span+3];
+		c3 = m00*k0 + m01*k1 + m02*k2 + m03*k3;
+		c2 = m10*k0 + m11*k1 + m12*k2 + m13*k3;
+		c1 = m20*k0 + m21*k1 + m22*k2 + m23*k3;
+		c0 = m30*k0 + m31*k1 + m32*k2 + m33*k3;
+		return ((c3*t + c2)*t + c1)*t + c0;
+	}
+
+	public static int colorSpline(float x, int numKnots, int[] knots) {
+		int span;
+		int numSpans = numKnots - 3;
+		float k0, k1, k2, k3;
+		float c0, c1, c2, c3;
+		if (numSpans < 1)
+			throw new IllegalArgumentException("Too few knots in spline");
+		x = clamp(x, 0, 1) * numSpans;
+		span = (int)x;
+		if (span > numKnots-4)
+			span = numKnots-4;
+		x -= span;
+		int v = 0;
+		for (int i = 0; i < 4; i++) {
+			int shift = i * 8;
+			k0 = (knots[span] >> shift) & 0xff;
+			k1 = (knots[span+1] >> shift) & 0xff;
+			k2 = (knots[span+2] >> shift) & 0xff;
+			k3 = (knots[span+3] >> shift) & 0xff;
+			c3 = m00*k0 + m01*k1 + m02*k2 + m03*k3;
+			c2 = m10*k0 + m11*k1 + m12*k2 + m13*k3;
+			c1 = m20*k0 + m21*k1 + m22*k2 + m23*k3;
+			c0 = m30*k0 + m31*k1 + m32*k2 + m33*k3;
+			int n = (int)(((c3*x + c2)*x + c1)*x + c0);
+			if (n < 0)
+				n = 0;
+			else if (n > 255)
+				n = 255;
+			v |= n << shift;
+		}
+		return v;
+	}
+
+	public static int colorSpline(int x, int numKnots, int[] xknots, int[] yknots) {
+		int span;
+		int numSpans = numKnots - 3;
+		float k0, k1, k2, k3;
+		float c0, c1, c2, c3;
+		if (numSpans < 1)
+			throw new IllegalArgumentException("Too few knots in spline");
+		for (span = 0; span < numSpans; span++)
+			if (xknots[span+1] > x)
+				break;
+		if (span > numKnots-3)
+			span = numKnots-3;
+		float t = (float)(x-xknots[span]) / (xknots[span+1]-xknots[span]);
+		span--;
+		if (span < 0) {
+			span = 0;
+			t = 0;
+		}
+		int v = 0;
+		for (int i = 0; i < 4; i++) {
+			int shift = i * 8;
+			k0 = (yknots[span] >> shift) & 0xff;
+			k1 = (yknots[span+1] >> shift) & 0xff;
+			k2 = (yknots[span+2] >> shift) & 0xff;
+			k3 = (yknots[span+3] >> shift) & 0xff;
+			c3 = m00*k0 + m01*k1 + m02*k2 + m03*k3;
+			c2 = m10*k0 + m11*k1 + m12*k2 + m13*k3;
+			c1 = m20*k0 + m21*k1 + m22*k2 + m23*k3;
+			c0 = m30*k0 + m31*k1 + m32*k2 + m33*k3;
+			int n = (int)(((c3*t + c2)*t + c1)*t + c0);
+			if (n < 0)
+				n = 0;
+			else if (n > 255)
+				n = 255;
+			v |= n << shift;
+		}
+		return v;
+	}
+
+	public static void resample(int[] source, int[] dest, int length, int offset, int stride, float[] out) {
+		int i, j;
+		float intensity;
+		float sizfac;
+		float inSegment;
+		float outSegment;
+		int a, r, g, b, nextA, nextR, nextG, nextB;
+		float aSum, rSum, gSum, bSum;
+		float[] in;
+		int srcIndex = offset;
+		int destIndex = offset;
+		int lastIndex = source.length;
+		int rgb;
+		in = new float[length+1];
+		i = 0;
+		for (j = 0; j < length; j++) {
+			while (out[i+1] < j)
+				i++;
+			in[j] = i + (float) (j - out[i]) / (out[i + 1] - out[i]);
+		}
+		in[length] = length;
+		inSegment  = 1.0f;
+		outSegment = in[1];
+		sizfac = outSegment;
+		aSum = rSum = gSum = bSum = 0.0f;
+		rgb = source[srcIndex];
+		a = (rgb >> 24) & 0xff;
+		r = (rgb >> 16) & 0xff;
+		g = (rgb >> 8) & 0xff;
+		b = rgb & 0xff;
+		srcIndex += stride;
+		rgb = source[srcIndex];
+		nextA = (rgb >> 24) & 0xff;
+		nextR = (rgb >> 16) & 0xff;
+		nextG = (rgb >> 8) & 0xff;
+		nextB = rgb & 0xff;
+		srcIndex += stride;
+		i = 1;
+		while (i < length) {
+			float aIntensity = inSegment * a + (1.0f - inSegment) * nextA;
+			float rIntensity = inSegment * r + (1.0f - inSegment) * nextR;
+			float gIntensity = inSegment * g + (1.0f - inSegment) * nextG;
+			float bIntensity = inSegment * b + (1.0f - inSegment) * nextB;
+			if (inSegment < outSegment) {
+				aSum += (aIntensity * inSegment);
+				rSum += (rIntensity * inSegment);
+				gSum += (gIntensity * inSegment);
+				bSum += (bIntensity * inSegment);
+				outSegment -= inSegment;
+				inSegment = 1.0f;
+				a = nextA;
+				r = nextR;
+				g = nextG;
+				b = nextB;
+				if (srcIndex < lastIndex)
+					rgb = source[srcIndex];
+				nextA = (rgb >> 24) & 0xff;
+				nextR = (rgb >> 16) & 0xff;
+				nextG = (rgb >> 8) & 0xff;
+				nextB = rgb & 0xff;
+				srcIndex += stride;
+			} else {
+				aSum += (aIntensity * outSegment);
+				rSum += (rIntensity * outSegment);
+				gSum += (gIntensity * outSegment);
+				bSum += (bIntensity * outSegment);
+				dest[destIndex] = 
+					((int)Math.min(aSum/sizfac, 255) << 24) |
+					((int)Math.min(rSum/sizfac, 255) << 16) |
+					((int)Math.min(gSum/sizfac, 255) << 8) |
+					(int)Math.min(bSum/sizfac, 255);
+				destIndex += stride;
+				rSum = gSum = bSum = 0.0f;
+				inSegment -= outSegment;
+				outSegment = in[i+1] - in[i];
+				sizfac = outSegment;
+				i++;
+			}
+		}
+	}
+}

+ 240 - 0
src/main/java/eu/mjdev/desktop/helpers/image/filters/LensBlurFilter.java

@@ -0,0 +1,240 @@
+/*
+** Copyright 2005 Huxtable.com. All rights reserved.
+*/
+
+package eu.mjdev.desktop.helpers.image.filters;
+
+import java.awt.image.*;
+
+public class LensBlurFilter extends AbstractBufferedImageOp {
+    private float radius = 10;
+	private float bloom = 2;
+	private float bloomThreshold = 192;
+    private float angle = 0;
+	private int sides = 5;
+
+	public void setRadius(float radius) {
+		this.radius = radius;
+	}
+	
+	public float getRadius() {
+		return radius;
+	}
+
+	public void setSides(int sides) {
+		this.sides = sides;
+	}
+	
+	public int getSides() {
+		return sides;
+	}
+
+	public void setBloom(float bloom) {
+		this.bloom = bloom;
+	}
+	
+	public float getBloom() {
+		return bloom;
+	}
+
+	public void setBloomThreshold(float bloomThreshold) {
+		this.bloomThreshold = bloomThreshold;
+	}
+	
+	public float getBloomThreshold() {
+		return bloomThreshold;
+	}
+
+    public BufferedImage filter( BufferedImage src, BufferedImage dst ) {
+        int width = src.getWidth();
+        int height = src.getHeight();
+        int rows = 1, cols = 1;
+        int log2rows = 0, log2cols = 0;
+        int iradius = (int)Math.ceil(radius);
+        int tileWidth = 128;
+        int tileHeight = tileWidth;
+        int adjustedWidth = (int)(width + iradius*2);
+        int adjustedHeight = (int)(height + iradius*2);
+		tileWidth = iradius < 32 ? Math.min(128, width+2*iradius) : Math.min(256, width+2*iradius);
+		tileHeight = iradius < 32 ? Math.min(128, height+2*iradius) : Math.min(256, height+2*iradius);
+        if ( dst == null )
+            dst = new BufferedImage( width, height, BufferedImage.TYPE_INT_ARGB );
+        while (rows < tileHeight) {
+            rows *= 2;
+            log2rows++;
+        }
+        while (cols < tileWidth) {
+            cols *= 2;
+            log2cols++;
+        }
+        int w = cols;
+        int h = rows;
+		tileWidth = w;
+		tileHeight = h;//FIXME-tileWidth, w, and cols are always all the same
+        FFT fft = new FFT( Math.max(log2rows, log2cols) );
+        int[] rgb = new int[w*h];
+        float[][] mask = new float[2][w*h];
+        float[][] gb = new float[2][w*h];
+        float[][] ar = new float[2][w*h];
+		double polyAngle = Math.PI/sides;
+		double polyScale = 1.0f / Math.cos(polyAngle);
+		double r2 = radius*radius;
+		double rangle = Math.toRadians(angle);
+		float total = 0;
+        int i = 0;
+        for ( int y = 0; y < h; y++ ) {
+            for ( int x = 0; x < w; x++ ) {
+                double dx = x-w/2f;
+                double dy = y-h/2f;
+				double r = dx*dx+dy*dy;
+				double f = r < r2 ? 1 : 0;
+				if (f != 0) {
+					r = Math.sqrt(r);
+					if ( sides != 0 ) {
+						double a = Math.atan2(dy, dx)+rangle;
+						a = ImageMath.mod(a, polyAngle*2)-polyAngle;
+						f = Math.cos(a) * polyScale;
+					} else
+						f = 1;
+					f = f*r < radius ? 1 : 0;
+				}
+				total += (float)f;
+				mask[0][i] = (float)f;
+                mask[1][i] = 0;
+                i++;
+            }
+        }
+        i = 0;
+        for ( int y = 0; y < h; y++ ) {
+            for ( int x = 0; x < w; x++ ) {
+                mask[0][i] /= total;
+                i++;
+            }
+        }
+        fft.transform2D( mask[0], mask[1], w, h, true );
+        for ( int tileY = -iradius; tileY < height; tileY += tileHeight-2*iradius ) {
+            for ( int tileX = -iradius; tileX < width; tileX += tileWidth-2*iradius ) {
+//                System.out.println("Tile: "+tileX+" "+tileY+" "+tileWidth+" "+tileHeight);
+                int tx = tileX, ty = tileY, tw = tileWidth, th = tileHeight;
+                int fx = 0, fy = 0;
+                if ( tx < 0 ) {
+                    tw += tx;
+                    fx -= tx;
+                    tx = 0;
+                }
+                if ( ty < 0 ) {
+                    th += ty;
+                    fy -= ty;
+                    ty = 0;
+                }
+                if ( tx+tw > width )
+                    tw = width-tx;
+                if ( ty+th > height )
+                    th = height-ty;
+                src.getRGB( tx, ty, tw, th, rgb, fy*w+fx, w );
+                i = 0;
+                for ( int y = 0; y < h; y++ ) {
+                    int imageY = y+tileY;
+                    int j;
+                    if ( imageY < 0 )
+                        j = fy;
+                    else if ( imageY > height )
+                        j = fy+th-1;
+                    else
+                        j = y;
+                    j *= w;
+                    for ( int x = 0; x < w; x++ ) {
+                        int imageX = x+tileX;
+                        int k;
+                        if ( imageX < 0 )
+                            k = fx;
+                        else if ( imageX > width )
+                            k = fx+tw-1;
+                        else
+                            k = x;
+                        k += j;
+                        ar[0][i] = ((rgb[k] >> 24) & 0xff);
+                        float r = ((rgb[k] >> 16) & 0xff);
+                        float g = ((rgb[k] >> 8) & 0xff);
+                        float b = (rgb[k] & 0xff);
+                        if ( r > bloomThreshold )
+							r *= bloom;
+//							r = bloomThreshold + (r-bloomThreshold) * bloom;
+                        if ( g > bloomThreshold )
+							g *= bloom;
+//							g = bloomThreshold + (g-bloomThreshold) * bloom;
+                        if ( b > bloomThreshold )
+							b *= bloom;
+//							b = bloomThreshold + (b-bloomThreshold) * bloom;
+						ar[1][i] = r;
+						gb[0][i] = g;
+						gb[1][i] = b;
+                        i++;
+                        k++;
+                    }
+                }
+                fft.transform2D( ar[0], ar[1], cols, rows, true );
+                fft.transform2D( gb[0], gb[1], cols, rows, true );
+                i = 0;
+                for ( int y = 0; y < h; y++ ) {
+                    for ( int x = 0; x < w; x++ ) {
+                        float re = ar[0][i];
+                        float im = ar[1][i];
+                        float rem = mask[0][i];
+                        float imm = mask[1][i];
+                        ar[0][i] = re*rem-im*imm;
+                        ar[1][i] = re*imm+im*rem;
+                        re = gb[0][i];
+                        im = gb[1][i];
+                        gb[0][i] = re*rem-im*imm;
+                        gb[1][i] = re*imm+im*rem;
+                        i++;
+                    }
+                }
+                fft.transform2D( ar[0], ar[1], cols, rows, false );
+                fft.transform2D( gb[0], gb[1], cols, rows, false );
+                int row_flip = w >> 1;
+                int col_flip = h >> 1;
+                int index = 0;
+                //FIXME-don't bother converting pixels off image edges
+                for ( int y = 0; y < w; y++ ) {
+                    int ym = y ^ row_flip;
+                    int yi = ym*cols;
+                    for ( int x = 0; x < w; x++ ) {
+                        int xm = yi + (x ^ col_flip);
+                        int a = (int)ar[0][xm];
+                        int r = (int)ar[1][xm];
+                        int g = (int)gb[0][xm];
+                        int b = (int)gb[1][xm];
+
+						// Clamp high pixels due to blooming
+						if ( r > 255 )
+							r = 255;
+						if ( g > 255 )
+							g = 255;
+						if ( b > 255 )
+							b = 255;
+                        int argb = (a << 24) | (r << 16) | (g << 8) | b;
+                        rgb[index++] = argb;
+                    }
+                }
+
+                // Clip to the output image
+                tx = tileX+iradius;
+                ty = tileY+iradius;
+                tw = tileWidth-2*iradius;
+                th = tileHeight-2*iradius;
+                if ( tx+tw > width )
+                    tw = width-tx;
+                if ( ty+th > height )
+                    th = height-ty;
+                dst.setRGB( tx, ty, tw, th, rgb, iradius*w+iradius, w );
+            }
+        }
+        return dst;
+    }
+
+	public String toString() {
+		return "Blur/Lens Blur...";
+	}
+}

+ 155 - 0
src/main/java/eu/mjdev/desktop/helpers/image/filters/MotionBlurFilter.java

@@ -0,0 +1,155 @@
+/*
+** Copyright 2005 Huxtable.com. All rights reserved.
+*/
+
+package eu.mjdev.desktop.helpers.image.filters;
+
+import java.awt.image.*;
+import java.awt.geom.*;
+
+public class MotionBlurFilter extends AbstractBufferedImageOp {
+
+	public final static int LINEAR = 0;
+	public final static int RADIAL = 1;
+	public final static int ZOOM = 2;
+	
+	private float angle = 0.0f;
+	private float falloff = 1.0f;
+	private float distance = 1.0f;
+	private float zoom = 0.0f;
+	private float rotation = 0.0f;
+	private boolean wrapEdges = false;
+
+	public MotionBlurFilter() {
+	}
+
+	public void setAngle( float angle ) {
+		this.angle = angle;
+	}
+
+	public float getAngle() {
+		return angle;
+	}
+	
+	public void setDistance( float distance ) {
+		this.distance = distance;
+	}
+
+	public float getDistance() {
+		return distance;
+	}
+	
+	public void setRotation( float rotation ) {
+		this.rotation = rotation;
+	}
+
+	public float getRotation() {
+		return rotation;
+	}
+	
+	public void setZoom( float zoom ) {
+		this.zoom = zoom;
+	}
+
+	public float getZoom() {
+		return zoom;
+	}
+	
+	public void setWrapEdges(boolean wrapEdges) {
+		this.wrapEdges = wrapEdges;
+	}
+
+	public boolean getWrapEdges() {
+		return wrapEdges;
+	}
+
+    public BufferedImage filter( BufferedImage src, BufferedImage dst ) {
+        int width = src.getWidth();
+        int height = src.getHeight();
+
+        if ( dst == null )
+            dst = createCompatibleDestImage( src, null );
+
+        int[] inPixels = new int[width*height];
+        int[] outPixels = new int[width*height];
+        getRGB( src, 0, 0, width, height, inPixels );
+
+		float sinAngle = (float)Math.sin(angle);
+		float cosAngle = (float)Math.cos(angle);
+
+		float total;
+		int cx = width/2;
+		int cy = height/2;
+		int index = 0;
+
+        float imageRadius = (float)Math.sqrt( cx*cx + cy*cy );
+        float translateX = (float)(distance * Math.cos( angle ));
+        float translateY = (float)(distance * -Math.sin( angle ));
+		float maxDistance = distance + Math.abs(rotation*imageRadius) + zoom*imageRadius;
+		int repetitions = (int)maxDistance;
+		AffineTransform t = new AffineTransform();
+		Point2D.Float p = new Point2D.Float();
+
+		for (int y = 0; y < height; y++) {
+			for (int x = 0; x < width; x++) {
+				int a = 0, r = 0, g = 0, b = 0;
+				int count = 0;
+				for (int i = 0; i < repetitions; i++) {
+					int newX = x, newY = y;
+					float f = (float)i/repetitions;
+
+					p.x = x;
+					p.y = y;
+					t.setToIdentity();
+					t.translate( cx+f*translateX, cy+f*translateY );
+					float s = 1-zoom*f;
+					t.scale( s, s );
+					if ( rotation != 0 )
+						t.rotate( -rotation*f );
+					t.translate( -cx, -cy );
+					t.transform( p, p );
+					newX = (int)p.x;
+					newY = (int)p.y;
+
+					if (newX < 0 || newX >= width) {
+						if ( wrapEdges )
+							newX = ImageMath.mod( newX, width );
+						else
+							break;
+					}
+					if (newY < 0 || newY >= height) {
+						if ( wrapEdges )
+							newY = ImageMath.mod( newY, height );
+						else
+							break;
+					}
+
+					count++;
+					int rgb = inPixels[newY*width+newX];
+					a += (rgb >> 24) & 0xff;
+					r += (rgb >> 16) & 0xff;
+					g += (rgb >> 8) & 0xff;
+					b += rgb & 0xff;
+				}
+				if (count == 0) {
+					outPixels[index] = inPixels[index];
+				} else {
+					a = PixelUtils.clamp((int)(a/count));
+					r = PixelUtils.clamp((int)(r/count));
+					g = PixelUtils.clamp((int)(g/count));
+					b = PixelUtils.clamp((int)(b/count));
+					outPixels[index] = (a << 24) | (r << 16) | (g << 8) | b;
+				}
+				index++;
+			}
+		}
+
+        setRGB( dst, 0, 0, width, height, inPixels );
+        return dst;
+    }
+
+	public String toString() {
+		return "Blur/Motion Blur...";
+	}
+}
+

+ 154 - 0
src/main/java/eu/mjdev/desktop/helpers/image/filters/MotionBlurOp.java

@@ -0,0 +1,154 @@
+/*
+** Copyright 2005 Huxtable.com. All rights reserved.
+*/
+
+package eu.mjdev.desktop.helpers.image.filters;
+
+import java.awt.*;
+import java.awt.geom.*;
+import java.awt.image.*;
+
+public class MotionBlurOp extends AbstractBufferedImageOp {
+    private float centreX = 0.5f, centreY = 0.5f;
+    private float distance;
+    private float angle;
+    private float rotation;
+    private float zoom;
+
+    public MotionBlurOp() {
+	}
+	
+	public MotionBlurOp( float distance, float angle, float rotation, float zoom ) {
+        this.distance = distance;
+        this.angle = angle;
+        this.rotation = rotation;
+        this.zoom = zoom;
+    }
+    
+	public void setAngle( float angle ) {
+		this.angle = angle;
+	}
+
+	public float getAngle() {
+		return angle;
+	}
+	
+	public void setDistance( float distance ) {
+		this.distance = distance;
+	}
+
+	public float getDistance() {
+		return distance;
+	}
+	
+	public void setRotation( float rotation ) {
+		this.rotation = rotation;
+	}
+
+	public float getRotation() {
+		return rotation;
+	}
+	
+	public void setZoom( float zoom ) {
+		this.zoom = zoom;
+	}
+
+	public float getZoom() {
+		return zoom;
+	}
+	
+	public void setCentreX( float centreX ) {
+		this.centreX = centreX;
+	}
+
+	public float getCentreX() {
+		return centreX;
+	}
+	
+	public void setCentreY( float centreY ) {
+		this.centreY = centreY;
+	}
+
+	public float getCentreY() {
+		return centreY;
+	}
+	
+	public void setCentre( Point2D centre ) {
+		this.centreX = (float)centre.getX();
+		this.centreY = (float)centre.getY();
+	}
+
+	public Point2D getCentre() {
+		return new Point2D.Float( centreX, centreY );
+	}
+	
+    private int log2( int n ) {
+        int m = 1;
+        int log2n = 0;
+
+        while (m < n) {
+            m *= 2;
+            log2n++;
+        }
+        return log2n;
+    }
+
+    public BufferedImage filter( BufferedImage src, BufferedImage dst ) {
+        if ( dst == null )
+            dst = createCompatibleDestImage( src, null );
+        BufferedImage tsrc = src;
+        float cx = (float)src.getWidth() * centreX;
+        float cy = (float)src.getHeight() * centreY;
+        float imageRadius = (float)Math.sqrt( cx*cx + cy*cy );
+        float translateX = (float)(distance * Math.cos( angle ));
+        float translateY = (float)(distance * -Math.sin( angle ));
+        float scale = zoom;
+        float rotate = rotation;
+        float maxDistance = distance + Math.abs(rotation*imageRadius) + zoom*imageRadius;
+        int steps = log2((int)maxDistance);
+
+		translateX /= maxDistance;
+		translateY /= maxDistance;
+		scale /= maxDistance;
+		rotate /= maxDistance;
+		
+        if ( steps == 0 ) {
+            Graphics2D g = dst.createGraphics();
+            g.drawRenderedImage( src, null );
+            g.dispose();
+            return dst;
+        }
+        
+        BufferedImage tmp = createCompatibleDestImage( src, null );
+        for ( int i = 0; i < steps; i++ ) {
+            Graphics2D g = tmp.createGraphics();
+            g.drawImage( tsrc, null, null );
+			g.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
+			g.setRenderingHint( RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR );
+			g.setComposite( AlphaComposite.getInstance( AlphaComposite.SRC_OVER, 0.5f ) );
+
+            g.translate( cx+translateX, cy+translateY );
+            g.scale( 1.0001+scale, 1.0001+scale );  // The .0001 works round a bug on Windows where drawImage throws an ArrayIndexOutofBoundException
+            if ( rotation != 0 )
+                g.rotate( rotate );
+            g.translate( -cx, -cy );
+
+            g.drawImage( dst, null, null );
+            g.dispose();
+            BufferedImage ti = dst;
+            dst = tmp;
+            tmp = ti;
+            tsrc = dst;
+
+            translateX *= 2;
+            translateY *= 2;
+            scale *= 2;
+            rotate *= 2;
+        }
+        return dst;
+    }
+    
+	public String toString() {
+		return "Blur/Motion Blur...";
+	}
+}

+ 211 - 0
src/main/java/eu/mjdev/desktop/helpers/image/filters/PixelUtils.java

@@ -0,0 +1,211 @@
+/*
+** Copyright 2005 Huxtable.com. All rights reserved.
+*/
+
+package eu.mjdev.desktop.helpers.image.filters;
+
+import java.util.*;
+import java.awt.Color;
+
+/**
+ * Some more useful math functions for image processing.
+ * These are becoming obsolete as we move to Java2D. Use MiscComposite instead.
+ */
+public class PixelUtils {
+
+	public final static int REPLACE = 0;
+	public final static int NORMAL = 1;
+	public final static int MIN = 2;
+	public final static int MAX = 3;
+	public final static int ADD = 4;
+	public final static int SUBTRACT = 5;
+	public final static int DIFFERENCE = 6;
+	public final static int MULTIPLY = 7;
+	public final static int HUE = 8;
+	public final static int SATURATION = 9;
+	public final static int VALUE = 10;
+	public final static int COLOR = 11;
+	public final static int SCREEN = 12;
+	public final static int AVERAGE = 13;
+	public final static int OVERLAY = 14;
+	public final static int CLEAR = 15;
+	public final static int EXCHANGE = 16;
+	public final static int DISSOLVE = 17;
+	public final static int DST_IN = 18;
+	public final static int ALPHA = 19;
+	public final static int ALPHA_TO_GRAY = 20;
+
+	private static Random randomGenerator = new Random();
+
+	/**
+	 * Clamp a value to the range 0..255
+	 */
+	public static int clamp(int c) {
+		if (c < 0)
+			return 0;
+		if (c > 255)
+			return 255;
+		return c;
+	}
+
+	public static int interpolate(int v1, int v2, float f) {
+		return clamp((int)(v1+f*(v2-v1)));
+	}
+	
+	public static int brightness(int rgb) {
+		int r = (rgb >> 16) & 0xff;
+		int g = (rgb >> 8) & 0xff;
+		int b = rgb & 0xff;
+		return (r+g+b)/3;
+	}
+	
+	public static boolean nearColors(int rgb1, int rgb2, int tolerance) {
+		int r1 = (rgb1 >> 16) & 0xff;
+		int g1 = (rgb1 >> 8) & 0xff;
+		int b1 = rgb1 & 0xff;
+		int r2 = (rgb2 >> 16) & 0xff;
+		int g2 = (rgb2 >> 8) & 0xff;
+		int b2 = rgb2 & 0xff;
+		return Math.abs(r1-r2) <= tolerance && Math.abs(g1-g2) <= tolerance && Math.abs(b1-b2) <= tolerance;
+	}
+	
+	private final static float hsb1[] = new float[3];//FIXME-not thread safe
+	private final static float hsb2[] = new float[3];//FIXME-not thread safe
+	
+	// Return rgb1 painted onto rgb2
+	public static int combinePixels(int rgb1, int rgb2, int op) {
+		return combinePixels(rgb1, rgb2, op, 0xff);
+	}
+	
+	public static int combinePixels(int rgb1, int rgb2, int op, int extraAlpha, int channelMask) {
+		return (rgb2 & ~channelMask) | combinePixels(rgb1 & channelMask, rgb2, op, extraAlpha);
+	}
+	
+	public static int combinePixels(int rgb1, int rgb2, int op, int extraAlpha) {
+		if (op == REPLACE)
+			return rgb1;
+		int a1 = (rgb1 >> 24) & 0xff;
+		int r1 = (rgb1 >> 16) & 0xff;
+		int g1 = (rgb1 >> 8) & 0xff;
+		int b1 = rgb1 & 0xff;
+		int a2 = (rgb2 >> 24) & 0xff;
+		int r2 = (rgb2 >> 16) & 0xff;
+		int g2 = (rgb2 >> 8) & 0xff;
+		int b2 = rgb2 & 0xff;
+
+		switch (op) {
+		case NORMAL:
+			break;
+		case MIN:
+			r1 = Math.min(r1, r2);
+			g1 = Math.min(g1, g2);
+			b1 = Math.min(b1, b2);
+			break;
+		case MAX:
+			r1 = Math.max(r1, r2);
+			g1 = Math.max(g1, g2);
+			b1 = Math.max(b1, b2);
+			break;
+		case ADD:
+			r1 = clamp(r1+r2);
+			g1 = clamp(g1+g2);
+			b1 = clamp(b1+b2);
+			break;
+		case SUBTRACT:
+			r1 = clamp(r2-r1);
+			g1 = clamp(g2-g1);
+			b1 = clamp(b2-b1);
+			break;
+		case DIFFERENCE:
+			r1 = clamp(Math.abs(r1-r2));
+			g1 = clamp(Math.abs(g1-g2));
+			b1 = clamp(Math.abs(b1-b2));
+			break;
+		case MULTIPLY:
+			r1 = clamp(r1*r2/255);
+			g1 = clamp(g1*g2/255);
+			b1 = clamp(b1*b2/255);
+			break;
+		case DISSOLVE:
+			if ((randomGenerator.nextInt() & 0xff) <= a1) {
+				r1 = r2;
+				g1 = g2;
+				b1 = b2;
+			}
+			break;
+		case AVERAGE:
+			r1 = (r1+r2)/2;
+			g1 = (g1+g2)/2;
+			b1 = (b1+b2)/2;
+			break;
+		case HUE:
+		case SATURATION:
+		case VALUE:
+		case COLOR:
+			Color.RGBtoHSB(r1, g1, b1, hsb1);
+			Color.RGBtoHSB(r2, g2, b2, hsb2);
+			switch (op) {
+			case HUE:
+				hsb2[0] = hsb1[0];
+				break;
+			case SATURATION:
+				hsb2[1] = hsb1[1];
+				break;
+			case VALUE:
+				hsb2[2] = hsb1[2];
+				break;
+			case COLOR:
+				hsb2[0] = hsb1[0];
+				hsb2[1] = hsb1[1];
+				break;
+			}
+			rgb1 = Color.HSBtoRGB(hsb2[0], hsb2[1], hsb2[2]);
+			r1 = (rgb1 >> 16) & 0xff;
+			g1 = (rgb1 >> 8) & 0xff;
+			b1 = rgb1 & 0xff;
+			break;
+		case SCREEN:
+			r1 = 255 - ((255 - r1) * (255 - r2)) / 255;
+			g1 = 255 - ((255 - g1) * (255 - g2)) / 255;
+			b1 = 255 - ((255 - b1) * (255 - b2)) / 255;
+			break;
+		case OVERLAY:
+			int m, s;
+			s = 255 - ((255 - r1) * (255 - r2)) / 255;
+			m = r1 * r2 / 255;
+			r1 = (s * r1 + m * (255 - r1)) / 255;
+			s = 255 - ((255 - g1) * (255 - g2)) / 255;
+			m = g1 * g2 / 255;
+			g1 = (s * g1 + m * (255 - g1)) / 255;
+			s = 255 - ((255 - b1) * (255 - b2)) / 255;
+			m = b1 * b2 / 255;
+			b1 = (s * b1 + m * (255 - b1)) / 255;
+			break;
+		case CLEAR:
+			r1 = g1 = b1 = 0xff;
+			break;
+		case DST_IN:
+			r1 = clamp((r2*a1)/255);
+			g1 = clamp((g2*a1)/255);
+			b1 = clamp((b2*a1)/255);
+			a1 = clamp((a2*a1)/255);
+			return (a1 << 24) | (r1 << 16) | (g1 << 8) | b1;
+		case ALPHA:
+			a1 = a1*a2/255;
+			return (a1 << 24) | (r2 << 16) | (g2 << 8) | b2;
+		case ALPHA_TO_GRAY:
+			int na = 255-a1;
+			return (a1 << 24) | (na << 16) | (na << 8) | na;
+		}
+		if (extraAlpha != 0xff || a1 != 0xff) {
+			a1 = a1*extraAlpha/255;
+			int a3 = (255-a1)*a2/255;
+			r1 = clamp((r1*a1+r2*a3)/255);
+			g1 = clamp((g1*a1+g2*a3)/255);
+			b1 = clamp((b1*a1+b2*a3)/255);
+			a1 = clamp(a1+a3);
+		}
+		return (a1 << 24) | (r1 << 16) | (g1 << 8) | b1;
+	}
+
+}

+ 136 - 0
src/main/java/eu/mjdev/desktop/helpers/image/filters/RaysFilter.java

@@ -0,0 +1,136 @@
+///*
+//** Copyright 2005 Huxtable.com. All rights reserved.
+//*/
+//
+//package eu.mjdev.desktop.helpers.image;
+//
+//import java.awt.*;
+//import java.awt.geom.*;
+//import java.awt.image.*;
+//import com.jhlabs.composite.*;
+//
+//public class RaysFilter extends MotionBlurOp {
+//
+//    private float opacity = 1.0f;
+//    private float threshold = 0.0f;
+//    private float strength = 0.5f;
+//	private boolean raysOnly = false;
+//	private Colormap colormap;
+//
+//    public RaysFilter() {
+//	}
+//
+//	public void setOpacity(float opacity) {
+//		this.opacity = opacity;
+//	}
+//
+//	public float getOpacity() {
+//		return opacity;
+//	}
+//
+//	public void setThreshold( float threshold ) {
+//		this.threshold = threshold;
+//	}
+//
+//	public float getThreshold() {
+//		return threshold;
+//	}
+//
+//	public void setStrength( float strength ) {
+//		this.strength = strength;
+//	}
+//
+//	public float getStrength() {
+//		return strength;
+//	}
+//
+//	public void setraysOnly(boolean raysOnly) {
+//		this.raysOnly = raysOnly;
+//	}
+//
+//	public boolean getraysOnly() {
+//		return raysOnly;
+//	}
+//
+////	public void setColormap(Colormap colormap) {
+////		this.colormap = colormap;
+////	}
+////
+////	public Colormap getColormap() {
+////		return colormap;
+////	}
+//
+//    public BufferedImage filter( BufferedImage src, BufferedImage dst ) {
+//        int width = src.getWidth();
+//        int height = src.getHeight();
+//		int[] pixels = new int[width];
+//		int[] srcPixels = new int[width];
+//
+//        BufferedImage rays = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+//
+//		int threshold3 = (int)(threshold*3*255);
+//		for ( int y = 0; y < height; y++ ) {
+//			getRGB( src, 0, y, width, 1, pixels );
+//			for ( int x = 0; x < width; x++ ) {
+//				int rgb = pixels[x];
+//				int a = rgb & 0xff000000;
+//				int r = (rgb >> 16) & 0xff;
+//				int g = (rgb >> 8) & 0xff;
+//				int b = rgb & 0xff;
+//				int l = r + g + b;
+//				if (l < threshold3)
+//					pixels[x] = 0xff000000;
+//				else {
+//					l /= 3;
+//					pixels[x] = a | (l << 16) | (l << 8) | l;
+//				}
+//			}
+//			setRGB( rays, 0, y, width, 1, pixels );
+//		}
+//
+//		rays = super.filter( rays, null );
+//
+//		for ( int y = 0; y < height; y++ ) {
+//			getRGB( rays, 0, y, width, 1, pixels );
+//			getRGB( src, 0, y, width, 1, srcPixels );
+//			for ( int x = 0; x < width; x++ ) {
+//				int rgb = pixels[x];
+//				int a = rgb & 0xff000000;
+//				int r = (rgb >> 16) & 0xff;
+//				int g = (rgb >> 8) & 0xff;
+//				int b = rgb & 0xff;
+//
+//				if ( colormap != null ) {
+//					int l = r + g + b;
+//					rgb = colormap.getColor( l * strength * (1/3f) );
+//				} else {
+//					r = PixelUtils.clamp((int)(r * strength));
+//					g = PixelUtils.clamp((int)(g * strength));
+//					b = PixelUtils.clamp((int)(b * strength));
+//					rgb = a | (r << 16) | (g << 8) | b;
+//				}
+//
+//				pixels[x] = rgb;
+//			}
+//			setRGB( rays, 0, y, width, 1, pixels );
+//		}
+//
+//        if ( dst == null )
+//            dst = createCompatibleDestImage( src, null );
+//
+//		Graphics2D g = dst.createGraphics();
+//		if ( !raysOnly ) {
+//			g.setComposite( AlphaComposite.SrcOver );
+//			g.drawRenderedImage( src, null );
+//		}
+//		g.setComposite( MiscComposite.getInstance( MiscComposite.ADD, opacity ) );
+//		g.drawRenderedImage( rays, null );
+//		g.dispose();
+//
+//        return dst;
+//    }
+//
+//	public String toString() {
+//		return "Stylize/Rays...";
+//	}
+//}

+ 148 - 0
src/main/java/eu/mjdev/desktop/helpers/image/filters/ShadowFilter.java

@@ -0,0 +1,148 @@
+/*
+** Copyright 2005 Huxtable.com. All rights reserved.
+*/
+
+package eu.mjdev.desktop.helpers.image.filters;
+
+import java.awt.*;
+import java.awt.geom.*;
+import java.awt.image.*;
+
+public class ShadowFilter extends AbstractBufferedImageOp {
+
+	static final long serialVersionUID = 6310370419462785691L;
+	
+	private int radius = 5;
+	private int xOffset = 5;
+	private int yOffset = 5;
+	private float opacity = 0.5f;
+	private boolean addMargins = false;
+	private boolean shadowOnly = false;
+	private int shadowColor = 0xff000000;
+
+	public ShadowFilter() {
+	}
+
+	public ShadowFilter(int radius, int xOffset, int yOffset, float opacity) {
+		this.radius = radius;
+		this.xOffset = xOffset;
+		this.yOffset = yOffset;
+		this.opacity = opacity;
+	}
+
+	public void setXOffset(int xOffset) {
+		this.xOffset = xOffset;
+	}
+
+	public int getXOffset() {
+		return xOffset;
+	}
+
+	public void setYOffset(int yOffset) {
+		this.yOffset = yOffset;
+	}
+
+	public int getYOffset() {
+		return yOffset;
+	}
+
+	/**
+	 * Set the radius of the kernel, and hence the amount of blur. The bigger the radius, the longer this filter will take.
+	 * @param radius the radius of the blur in pixels.
+	 */
+	public void setRadius(int radius) {
+		this.radius = radius;
+	}
+	
+	/**
+	 * Get the radius of the kernel.
+	 * @return the radius
+	 */
+	public int getRadius() {
+		return radius;
+	}
+
+	public void setOpacity(float opacity) {
+		this.opacity = opacity;
+	}
+
+	public float getOpacity() {
+		return opacity;
+	}
+
+	public void setShadowColor(int shadowColor) {
+		this.shadowColor = shadowColor;
+	}
+
+	public int getShadowColor() {
+		return shadowColor;
+	}
+
+	public void setAddMargins(boolean addMargins) {
+		this.addMargins = addMargins;
+	}
+
+	public boolean getAddMargins() {
+		return addMargins;
+	}
+
+	public void setShadowOnly(boolean shadowOnly) {
+		this.shadowOnly = shadowOnly;
+	}
+
+	public boolean getShadowOnly() {
+		return shadowOnly;
+	}
+
+	protected void transformSpace(Rectangle r) {
+		if ( addMargins ) {
+			r.width += Math.abs(xOffset)+2*radius;
+			r.height += Math.abs(yOffset)+2*radius;
+		}
+	}
+
+    public BufferedImage filter( BufferedImage src, BufferedImage dst ) {
+        int width = src.getWidth();
+        int height = src.getHeight();
+
+        if ( dst == null ) {
+            if ( addMargins ) {
+				ColorModel cm = src.getColorModel();
+				dst = new BufferedImage(cm, cm.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), cm.isAlphaPremultiplied(), null);
+			} else
+				dst = createCompatibleDestImage( src, null );
+		}
+
+        // Make a black mask from the image's alpha channel 
+        float[][] extractAlpha = {
+            { 0, 0, 0, 0 },
+            { 0, 0, 0, 0 },
+            { 0, 0, 0, 0 },
+            { 0, 0, 0, opacity }
+        };
+        BufferedImage shadow = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+        new BandCombineOp( extractAlpha, null ).filter( src.getRaster(), shadow.getRaster() );
+        shadow = new GaussianFilter( radius ).filter( shadow, null );
+
+		Graphics2D g = dst.createGraphics();
+		g.setComposite( AlphaComposite.getInstance( AlphaComposite.SRC_OVER, opacity ) );
+		if ( addMargins ) {
+			int radius2 = radius/2;
+			int topShadow = Math.max( 0, radius-yOffset );
+			int leftShadow = Math.max( 0, radius-xOffset );
+			g.translate( topShadow, leftShadow );
+		}
+		g.drawRenderedImage( shadow, AffineTransform.getTranslateInstance( xOffset, yOffset ) );
+		if ( !shadowOnly ) {
+			g.setComposite( AlphaComposite.SrcOver );
+			g.drawRenderedImage( src, null );
+		}
+		g.dispose();
+
+        return dst;
+	}
+
+	public String toString() {
+		return "Stylize/Drop Shadow...";
+	}
+}

+ 125 - 0
src/main/java/eu/mjdev/desktop/helpers/image/filters/SmartBlurFilter.java

@@ -0,0 +1,125 @@
+package eu.mjdev.desktop.helpers.image.filters;
+
+import java.awt.image.*;
+
+public class SmartBlurFilter extends AbstractBufferedImageOp {
+	private int hRadius = 5;
+	private int vRadius = 5;
+	private int threshold = 10;
+	
+    public BufferedImage filter( BufferedImage src, BufferedImage dst ) {
+        int width = src.getWidth();
+        int height = src.getHeight();
+        if ( dst == null )
+            dst = createCompatibleDestImage( src, null );
+        int[] inPixels = new int[width*height];
+        int[] outPixels = new int[width*height];
+        getRGB( src, 0, 0, width, height, inPixels );
+		Kernel kernel = GaussianFilter.makeKernel(hRadius);
+		thresholdBlur( kernel, inPixels, outPixels, width, height, true );
+		thresholdBlur( kernel, outPixels, inPixels, height, width, true );
+        setRGB( dst, 0, 0, width, height, inPixels );
+        return dst;
+    }
+
+	public void thresholdBlur(Kernel kernel, int[] inPixels, int[] outPixels, int width, int height, boolean alpha) {
+		int index = 0;
+		float[] matrix = kernel.getKernelData( null );
+		int cols = kernel.getWidth();
+		int cols2 = cols/2;
+		for (int y = 0; y < height; y++) {
+			int ioffset = y*width;
+            int outIndex = y;
+			for (int x = 0; x < width; x++) {
+				float r = 0, g = 0, b = 0, a = 0;
+				int moffset = cols2;
+                int rgb1 = inPixels[ioffset+x];
+                int a1 = (rgb1 >> 24) & 0xff;
+                int r1 = (rgb1 >> 16) & 0xff;
+                int g1 = (rgb1 >> 8) & 0xff;
+                int b1 = rgb1 & 0xff;
+				float af = 0, rf = 0, gf = 0, bf = 0;
+                for (int col = -cols2; col <= cols2; col++) {
+					float f = matrix[moffset+col];
+					if (f != 0) {
+						int ix = x+col;
+						if (!(0 <= ix && ix < width))
+							ix = x;
+						int rgb2 = inPixels[ioffset+ix];
+                        int a2 = (rgb2 >> 24) & 0xff;
+                        int r2 = (rgb2 >> 16) & 0xff;
+                        int g2 = (rgb2 >> 8) & 0xff;
+                        int b2 = rgb2 & 0xff;
+						int d;
+                        d = a1-a2;
+                        if ( d >= -threshold && d <= threshold ) {
+                            a += f * a2;
+                            af += f;
+                        }
+                        d = r1-r2;
+                        if ( d >= -threshold && d <= threshold ) {
+                            r += f * r2;
+                            rf += f;
+                        }
+                        d = g1-g2;
+                        if ( d >= -threshold && d <= threshold ) {
+                            g += f * g2;
+                            gf += f;
+                        }
+                        d = b1-b2;
+                        if ( d >= -threshold && d <= threshold ) {
+                            b += f * b2;
+                            bf += f;
+                        }
+					}
+				}
+                a = af == 0 ? a1 : a/af;
+                r = rf == 0 ? r1 : r/rf;
+                g = gf == 0 ? g1 : g/gf;
+                b = bf == 0 ? b1 : b/bf;
+				int ia = alpha ? PixelUtils.clamp((int)(a+0.5)) : 0xff;
+				int ir = PixelUtils.clamp((int)(r+0.5));
+				int ig = PixelUtils.clamp((int)(g+0.5));
+				int ib = PixelUtils.clamp((int)(b+0.5));
+				outPixels[outIndex] = (ia << 24) | (ir << 16) | (ig << 8) | ib;
+                outIndex += height;
+			}
+		}
+	}
+
+	public void setHRadius(int hRadius) {
+		this.hRadius = hRadius;
+	}
+	
+	public int getHRadius() {
+		return hRadius;
+	}
+	
+	public void setVRadius(int vRadius) {
+		this.vRadius = vRadius;
+	}
+	
+	public int getVRadius() {
+		return vRadius;
+	}
+	
+	public void setRadius(int radius) {
+		this.hRadius = this.vRadius = radius;
+	}
+	
+	public int getRadius() {
+		return hRadius;
+	}
+	
+	public void setThreshold(int threshold) {
+		this.threshold = threshold;
+	}
+	
+	public int getThreshold() {
+		return threshold;
+	}
+	
+	public String toString() {
+		return "Blur/Smart Blur...";
+	}
+}

+ 90 - 0
src/main/java/eu/mjdev/desktop/helpers/image/filters/UnsharpFilter.java

@@ -0,0 +1,90 @@
+/*
+** Copyright 2005 Huxtable.com. All rights reserved.
+*/
+
+package eu.mjdev.desktop.helpers.image.filters;
+
+import java.awt.image.*;
+
+/**
+ * A filter which subtracts Gaussian blur from an image, sharpening it.
+ * @author Jerry Huxtable
+ */
+public class UnsharpFilter extends GaussianFilter {
+
+	static final long serialVersionUID = 5377089073023183684L;
+
+	private float amount = 0.5f;
+	private int threshold = 1;
+	
+	public UnsharpFilter() {
+		radius = 2;
+	}
+	
+	public void setThreshold( int threshold ) {
+		this.threshold = threshold;
+	}
+	
+	public int getThreshold() {
+		return threshold;
+	}
+	
+	public void setAmount( float amount ) {
+		this.amount = amount;
+	}
+	
+	public float getAmount() {
+		return amount;
+	}
+	
+    public BufferedImage filter( BufferedImage src, BufferedImage dst ) {
+        int width = src.getWidth();
+        int height = src.getHeight();
+
+        if ( dst == null )
+            dst = createCompatibleDestImage( src, null );
+
+        int[] inPixels = new int[width*height];
+        int[] outPixels = new int[width*height];
+        src.getRGB( 0, 0, width, height, inPixels, 0, width );
+
+		convolveAndTranspose(kernel, inPixels, outPixels, width, height, alpha, CLAMP_EDGES);
+		convolveAndTranspose(kernel, outPixels, inPixels, height, width, alpha, CLAMP_EDGES);
+
+        src.getRGB( 0, 0, width, height, outPixels, 0, width );
+
+		float a = 4*amount;
+
+		int index = 0;
+		for ( int y = 0; y < height; y++ ) {
+			for ( int x = 0; x < width; x++ ) {
+				int rgb1 = outPixels[index];
+				int r1 = (rgb1 >> 16) & 0xff;
+				int g1 = (rgb1 >> 8) & 0xff;
+				int b1 = rgb1 & 0xff;
+
+				int rgb2 = inPixels[index];
+				int r2 = (rgb2 >> 16) & 0xff;
+				int g2 = (rgb2 >> 8) & 0xff;
+				int b2 = rgb2 & 0xff;
+
+				if ( Math.abs( r1 -  r2 ) >= threshold )
+					r1 = PixelUtils.clamp( (int)((a+1) * (r1-r2) + r2) );
+				if ( Math.abs( g1 -  g2 ) >= threshold )
+					g1 = PixelUtils.clamp( (int)((a+1) * (g1-g2) + g2) );
+				if ( Math.abs( b1 -  b2 ) >= threshold )
+					b1 = PixelUtils.clamp( (int)((a+1) * (b1-b2) + b2) );
+
+				inPixels[index] = (rgb1 & 0xff000000) | (r1 << 16) | (g1 << 8) | b1;
+				index++;
+			}
+		}
+
+        dst.setRGB( 0, 0, width, height, inPixels, 0, width );
+        return dst;
+    }
+
+	public String toString() {
+		return "Blur/Unsharp Mask...";
+	}
+}

+ 172 - 0
src/main/java/eu/mjdev/desktop/helpers/image/filters/VariableBlurFilter.java

@@ -0,0 +1,172 @@
+package eu.mjdev.desktop.helpers.image.filters;
+
+import java.awt.*;
+import java.awt.image.*;
+import java.awt.geom.*;
+
+@SuppressWarnings("ALL")
+public class VariableBlurFilter extends AbstractBufferedImageOp {
+	private int hRadius = 1;
+	private int vRadius = 1;
+	private int iterations = 1;
+	private BufferedImage blurMask;
+	
+    public BufferedImage filter( BufferedImage src, BufferedImage dst ) {
+		int width = src.getWidth();
+        int height = src.getHeight();
+        if ( dst == null )
+            dst = new BufferedImage( width, height, BufferedImage.TYPE_INT_ARGB );
+        int[] inPixels = new int[width*height];
+        int[] outPixels = new int[width*height];
+        getRGB( src, 0, 0, width, height, inPixels );
+        for (int i = 0; i < iterations; i++ ) {
+            blur( inPixels, outPixels, width, height, hRadius, 1 );
+            blur( outPixels, inPixels, height, width, vRadius, 2 );
+        }
+        setRGB( dst, 0, 0, width, height, inPixels );
+        return dst;
+    }
+
+    public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel dstCM) {
+        if ( dstCM == null )
+            dstCM = src.getColorModel();
+        return new BufferedImage(dstCM, dstCM.createCompatibleWritableRaster(src.getWidth(), src.getHeight()), dstCM.isAlphaPremultiplied(), null);
+    }
+    
+    public Rectangle2D getBounds2D( BufferedImage src ) {
+        return new Rectangle(0, 0, src.getWidth(), src.getHeight());
+    }
+    
+    public Point2D getPoint2D( Point2D srcPt, Point2D dstPt ) {
+        if ( dstPt == null )
+            dstPt = new Point2D.Double();
+        dstPt.setLocation( srcPt.getX(), srcPt.getY() );
+        return dstPt;
+    }
+
+    public RenderingHints getRenderingHints() {
+        return null;
+    }
+
+    public void blur( int[] in, int[] out, int width, int height, int radius, int pass ) {
+        int widthMinus1 = width-1;
+        int[] r = new int[width];
+        int[] g = new int[width];
+        int[] b = new int[width];
+        int[] a = new int[width];
+        int[] mask = new int[width];
+		int inIndex = 0;
+        for ( int y = 0; y < height; y++ ) {
+            int outIndex = y;
+			if ( blurMask != null ) {
+				if ( pass == 1 )
+					getRGB( blurMask, 0, y, width, 1, mask );
+				else
+					getRGB( blurMask, y, 0, 1, width, mask );
+			}
+            for ( int x = 0; x < width; x++ ) {
+				int argb = in[inIndex+x];
+				a[x] = (argb >> 24) & 0xff;
+                r[x] = (argb >> 16) & 0xff;
+                g[x] = (argb >> 8) & 0xff;
+                b[x] = argb & 0xff;
+                if ( x != 0 ) {
+                    a[x] += a[x-1];
+                    r[x] += r[x-1];
+                    g[x] += g[x-1];
+                    b[x] += b[x-1];
+                }
+			}
+            for ( int x = 0; x < width; x++ ) {
+				int ra;
+				if ( blurMask != null ) {
+					if ( pass == 1 )
+						ra = (int)((mask[x] & 0xff)*hRadius/255f);
+					else
+						ra = (int)((mask[x] & 0xff)*vRadius/255f);
+				} else {
+					if ( pass == 1 )
+						ra = (int)(blurRadiusAt( x, y, width, height ) * hRadius);
+					else
+						ra = (int)(blurRadiusAt( y, x, height, width ) * vRadius);
+				}
+                int divisor = 2*ra+1;
+                int ta = 0, tr = 0, tg = 0, tb = 0;
+				int i1 = x+ra;
+                if ( i1 > widthMinus1 ) {
+                    int f = i1-widthMinus1;
+					int l = widthMinus1;
+					ta += (a[l]-a[l-1]) * f;
+					tr += (r[l]-r[l-1]) * f;
+					tg += (g[l]-g[l-1]) * f;
+					tb += (b[l]-b[l-1]) * f;
+					i1 = widthMinus1;
+                }
+				int i2 = x-ra-1;
+                if ( i2 < 0 ) {
+					ta -= a[0] * i2;
+					tr -= r[0] * i2;
+					tg -= g[0] * i2;
+					tb -= b[0] * i2;
+                    i2 = 0;
+				}
+                ta += a[i1] - a[i2];
+                tr += r[i1] - r[i2];
+                tg += g[i1] - g[i2];
+                tb += b[i1] - b[i2];
+//if ( y == 0 && pass == 1 )System.out.println(x+": "+i1+" "+i2+" "+r[i1]+" "+r[i2]+" "+tr);
+                out[ outIndex ] = ((ta/divisor) << 24) | ((tr/divisor) << 16) | ((tg/divisor) << 8) | (tb/divisor);
+                outIndex += height;
+            }
+			inIndex += width;
+        }
+    }
+    
+	protected float blurRadiusAt( int x, int y, int width, int height ) {
+		return (float)x/width;
+	}
+
+	public void setHRadius(int hRadius) {
+		this.hRadius = hRadius;
+	}
+	
+	public int getHRadius() {
+		return hRadius;
+	}
+	
+	public void setVRadius(int vRadius) {
+		this.vRadius = vRadius;
+	}
+	
+	public int getVRadius() {
+		return vRadius;
+	}
+	
+	public void setRadius(int radius) {
+		this.hRadius = this.vRadius = radius;
+	}
+	
+	public int getRadius() {
+		return hRadius;
+	}
+	
+	public void setIterations(int iterations) {
+		this.iterations = iterations;
+	}
+	
+	public int getIterations() {
+		return iterations;
+	}
+	
+	public void setBlurMask(BufferedImage blurMask) {
+		this.blurMask = blurMask;
+	}
+	
+	public BufferedImage getBlurMask() {
+		return blurMask;
+	}
+	
+	public String toString() {
+		return "Blur/Variable Blur...";
+	}
+}

+ 2 - 5
src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenu.kt

@@ -13,11 +13,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.Divider
 import androidx.compose.runtime.*
 import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
-import androidx.compose.ui.input.pointer.PointerEventType
-import androidx.compose.ui.input.pointer.onPointerEvent
 import androidx.compose.ui.layout.onPlaced
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.window.WindowPosition
@@ -28,6 +25,7 @@ import eu.mjdev.desktop.data.App
 import eu.mjdev.desktop.data.Category
 import eu.mjdev.desktop.extensions.ColorUtils.alpha
 import eu.mjdev.desktop.extensions.Compose.launchedEffect
+import eu.mjdev.desktop.extensions.Compose.onMouseEnter
 import eu.mjdev.desktop.extensions.Compose.rememberCalculated
 import eu.mjdev.desktop.extensions.Modifier.dropShadow
 import eu.mjdev.desktop.helpers.animation.Animations.AppsMenuEnterAnimation
@@ -38,7 +36,6 @@ import eu.mjdev.desktop.windows.ChromeWindow
 import eu.mjdev.desktop.windows.ChromeWindowState
 import eu.mjdev.desktop.windows.ChromeWindowState.Companion.rememberChromeWindowState
 
-@OptIn(ExperimentalComposeUiApi::class)
 @Suppress("FunctionName", "LocalVariableName")
 @Composable
 fun AppsMenu(
@@ -104,7 +101,7 @@ fun AppsMenu(
                     0.dp
                 )
                 .onPlaced(menuState::onPlaced)
-                .onPointerEvent(PointerEventType.Enter) {
+                .onMouseEnter {
                     windowState.requestFocus()
                 }
         ) {

+ 8 - 14
src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenuApp.kt

@@ -1,28 +1,27 @@
 package eu.mjdev.desktop.components.appsmenu
 
 import androidx.compose.desktop.ui.tooling.preview.Preview
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.mouseClickable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.pointer.isSecondaryPressed
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
 import eu.mjdev.desktop.components.fonticon.FontIcon
 import eu.mjdev.desktop.components.text.TextAny
 import eu.mjdev.desktop.data.App
+import eu.mjdev.desktop.extensions.Compose.onLeftClick
+import eu.mjdev.desktop.extensions.Compose.onMousePress
+import eu.mjdev.desktop.extensions.Compose.onRigntClick
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
 
-@Suppress("DEPRECATION", "FunctionName")
-@OptIn(ExperimentalFoundationApi::class)
+@Suppress("FunctionName")
 @Composable
 fun AppsMenuApp(
     modifier: Modifier = Modifier,
@@ -32,16 +31,13 @@ fun AppsMenuApp(
     iconTint: Color = Color.Black,
     textColor: Color = Color.White,
     backgroundColor: Color = Color.White,
+    onContextMenuClick: () -> Unit = {},
     onClick: () -> Unit = {},
-    onContextMenuClick: () -> Unit = {}
 ) = withDesktopScope {
     Row(
-        modifier = modifier.mouseClickable {
-            if (buttons.isSecondaryPressed) {
-                onContextMenuClick()
-            } else {
-                onClick()
-            }
+        modifier = modifier.onMousePress {
+            onLeftClick { onClick() }
+            onRigntClick { onContextMenuClick() }
         },
         verticalAlignment = Alignment.CenterVertically,
     ) {
@@ -54,8 +50,6 @@ fun AppsMenuApp(
             iconBackgroundColor = backgroundColor,
             outerPadding = PaddingValues(2.dp),
             innerPadding = PaddingValues(0.dp),
-            onClick = onClick,
-            onRightClick = onContextMenuClick
         )
         TextAny(
             modifier = Modifier.padding(start = 4.dp).fillMaxWidth(),

+ 6 - 13
src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenuCategory.kt

@@ -1,27 +1,25 @@
 package eu.mjdev.desktop.components.appsmenu
 
 import androidx.compose.desktop.ui.tooling.preview.Preview
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.mouseClickable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.pointer.isSecondaryPressed
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
 import eu.mjdev.desktop.components.fonticon.FontIcon
 import eu.mjdev.desktop.components.text.TextAny
 import eu.mjdev.desktop.data.Category
+import eu.mjdev.desktop.extensions.Compose.onLeftClick
+import eu.mjdev.desktop.extensions.Compose.onMousePress
+import eu.mjdev.desktop.extensions.Compose.onRigntClick
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
 
-@Suppress("DEPRECATION")
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 fun AppsMenuCategory(
     modifier: Modifier = Modifier,
@@ -31,12 +29,9 @@ fun AppsMenuCategory(
     onContextMenuClick: (category: Category) -> Unit = {}
 ) = withDesktopScope {
     Row(
-        modifier = modifier.mouseClickable {
-            if (buttons.isSecondaryPressed) {
-                onContextMenuClick(category)
-            } else {
-                onClick(category)
-            }
+        modifier = modifier.onMousePress {
+            onLeftClick { onClick(category) }
+            onRigntClick { onContextMenuClick(category) }
         },
         verticalAlignment = Alignment.CenterVertically
     ) {
@@ -49,8 +44,6 @@ fun AppsMenuCategory(
             iconBackgroundColor = backgroundColor,
             outerPadding = PaddingValues(2.dp),
             innerPadding = PaddingValues(0.dp),
-            onClick = { onClick(category) },
-            onRightClick = { onContextMenuClick(category) }
         )
         TextAny(
             modifier = Modifier.padding(start = 4.dp).fillMaxWidth(),

+ 12 - 9
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/ControlCenter.kt

@@ -3,14 +3,12 @@ package eu.mjdev.desktop.components.controlcenter
 import androidx.compose.animation.EnterTransition
 import androidx.compose.animation.ExitTransition
 import androidx.compose.desktop.ui.tooling.preview.Preview
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.gestures.Orientation.Horizontal
 import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.foundation.layout.*
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.foundation.mouseClickable
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.Divider
 import androidx.compose.material.Icon
@@ -18,7 +16,6 @@ import androidx.compose.runtime.*
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.pointer.isSecondaryPressed
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
@@ -26,6 +23,9 @@ import androidx.compose.ui.window.WindowPosition
 import eu.mjdev.desktop.components.sliding.SlidingMenu
 import eu.mjdev.desktop.components.sliding.base.VisibilityState
 import eu.mjdev.desktop.components.sliding.base.VisibilityState.Companion.rememberVisibilityState
+import eu.mjdev.desktop.extensions.Compose.onLeftClick
+import eu.mjdev.desktop.extensions.Compose.onMousePress
+import eu.mjdev.desktop.extensions.Compose.onRigntClick
 import eu.mjdev.desktop.extensions.Compose.rememberCalculated
 import eu.mjdev.desktop.extensions.Compose.rememberState
 import eu.mjdev.desktop.helpers.animation.Animations.ControlCenterEnterAnimation
@@ -37,8 +37,7 @@ import eu.mjdev.desktop.windows.ChromeWindowState.Companion.rememberChromeWindow
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 
-@OptIn(ExperimentalFoundationApi::class)
-@Suppress("FunctionName", "DEPRECATION")
+@Suppress("FunctionName")
 @Composable
 fun ControlCenter(
     pagerState: MutableState<Int> = rememberState(0),
@@ -135,13 +134,17 @@ fun ControlCenter(
                                                     }
                                                 )
                                             }
-                                            .mouseClickable {
-                                                if (buttons.isSecondaryPressed) {
-                                                    onContextMenuClick()
-                                                } else {
+                                            .onMousePress {
+                                                onLeftClick {
+                                                    scope.launch {
+                                                        pagerState.value = idx
+                                                    }
+                                                }
+                                                onRigntClick {
                                                     scope.launch {
                                                         pagerState.value = idx
                                                     }
+                                                    onContextMenuClick()
                                                 }
                                             },
                                         imageVector = page.icon,

+ 13 - 12
src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/DesktopPanelIcon.kt

@@ -8,22 +8,23 @@ import androidx.compose.material.CircularProgressIndicator
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.remember
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.pointer.PointerEventType
-import androidx.compose.ui.input.pointer.onPointerEvent
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
 import eu.mjdev.desktop.components.fonticon.FontIcon
 import eu.mjdev.desktop.data.App
 import eu.mjdev.desktop.extensions.Compose.color
 import eu.mjdev.desktop.extensions.Compose.noElevation
+import eu.mjdev.desktop.extensions.Compose.onLeftClick
+import eu.mjdev.desktop.extensions.Compose.onMouseEnter
+import eu.mjdev.desktop.extensions.Compose.onMouseLeave
+import eu.mjdev.desktop.extensions.Compose.onMousePress
+import eu.mjdev.desktop.extensions.Compose.onRigntClick
 import eu.mjdev.desktop.extensions.Compose.rememberState
 import eu.mjdev.desktop.extensions.Modifier.clipRect
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
 
-@OptIn(ExperimentalComposeUiApi::class)
 @Composable
 fun DesktopPanelIcon(
     app: App? = null,
@@ -40,8 +41,8 @@ fun DesktopPanelIcon(
     iconState: MutableState<Boolean> = rememberState(false),
     contentDescription: String? = null,
     onToolTip: (item: Any?) -> Unit = {},
+    onContextMenuClick: () -> Unit = {},
     onClick: () -> Unit = {},
-    onContextMenuClick: () -> Unit = {}
 ) = withDesktopScope {
     val materialIcon = remember(app, icon) {
         val iconName = app?.name ?: icon // todo better guess
@@ -55,23 +56,25 @@ fun DesktopPanelIcon(
         modifier = Modifier
             .size(iconSize)
             .clipRect()
-            .onPointerEvent(PointerEventType.Enter) {
+            .onMouseEnter {
                 iconState.value = true
                 onToolTip(app ?: contentDescription)
             }
-            .onPointerEvent(PointerEventType.Exit) {
+            .onMouseLeave {
                 iconState.value = false
             }
-            .onPointerEvent(PointerEventType.Press) {
+            .onMousePress {
                 iconState.value = false
+                onLeftClick { onClick() }
+                onRigntClick { onContextMenuClick() }
             }
     ) {
         Button(
             modifier = Modifier.fillMaxSize(),
             contentPadding = PaddingValues(0.dp),
-            onClick = onClick,
             colors = ButtonDefaults.color(background),
-            elevation = ButtonDefaults.noElevation()
+            elevation = ButtonDefaults.noElevation(),
+            onClick = {}
         ) {
             FontIcon(
                 iconId = materialIcon,
@@ -80,8 +83,6 @@ fun DesktopPanelIcon(
                 iconSize = iconSize,
                 innerPadding = iconPadding,
                 outerPadding = iconOuterPadding,
-                onClick = onClick,
-                onRightClick = onContextMenuClick
             )
         }
         if (isStarting || isRunning) {

+ 4 - 6
src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/DesktopPanelText.kt

@@ -10,18 +10,16 @@ import androidx.compose.material.ButtonDefaults
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.pointer.PointerEventType
-import androidx.compose.ui.input.pointer.onPointerEvent
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
 import eu.mjdev.desktop.extensions.Compose.color
 import eu.mjdev.desktop.extensions.Compose.noElevation
+import eu.mjdev.desktop.extensions.Compose.onMouseEnter
+import eu.mjdev.desktop.extensions.Compose.onMouseLeave
 import eu.mjdev.desktop.extensions.Compose.rememberState
 
-@OptIn(ExperimentalComposeUiApi::class)
 @Preview
 @Composable
 fun DesktopPanelText(
@@ -36,11 +34,11 @@ fun DesktopPanelText(
 ) = Box(
     modifier = modifier
         .wrapContentSize()
-        .onPointerEvent(PointerEventType.Enter) {
+        .onMouseEnter {
             buttonState.value = true
             onToolTip(text)
         }
-        .onPointerEvent(PointerEventType.Exit) {
+        .onMouseLeave {
             buttonState.value = false
         }
 ) {

+ 2 - 3
src/main/kotlin/eu/mjdev/desktop/components/desktoppanel/applets/DesktopPanelFavoriteApps.kt

@@ -8,7 +8,7 @@ import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.foundation.lazy.LazyRow
 import androidx.compose.foundation.lazy.items
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -16,7 +16,6 @@ import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
 import eu.mjdev.desktop.components.desktoppanel.DesktopPanelIcon
 import eu.mjdev.desktop.data.App
-import eu.mjdev.desktop.extensions.Compose.rememberCalculated
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
 
 @Suppress("FunctionName")
@@ -33,7 +32,7 @@ fun DesktopPanelFavoriteApps(
     onAppClick: (app: App) -> Unit = {},
     onContextMenuClick: (app: App) -> Unit = {}
 ) = withDesktopScope {
-    val apps by rememberCalculated(api.appsProvider.favoriteApps.size) { api.appsProvider.favoriteApps }
+    val apps = remember(favoriteApps.size) { favoriteApps }
     Box(
         modifier = modifier
     ) {

+ 7 - 19
src/main/kotlin/eu/mjdev/desktop/components/icon/ShapedIcon.kt

@@ -1,10 +1,8 @@
 package eu.mjdev.desktop.components.icon
 
 import androidx.compose.desktop.ui.tooling.preview.Preview
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.mouseClickable
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.material.Icon
 import androidx.compose.material.icons.Icons
@@ -15,13 +13,13 @@ import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.input.pointer.isSecondaryPressed
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
+import eu.mjdev.desktop.extensions.Compose.onLeftClick
+import eu.mjdev.desktop.extensions.Compose.onMousePress
+import eu.mjdev.desktop.extensions.Compose.onRigntClick
 import eu.mjdev.desktop.extensions.Compose.size
 
-@Suppress("DEPRECATION")
-@OptIn(ExperimentalFoundationApi::class)
 @Composable
 fun ShapedIcon(
     modifier: Modifier = Modifier,
@@ -42,12 +40,9 @@ fun ShapedIcon(
         .size(iconSize + innerPadding.size + outerPadding.size)
         .background(iconBackgroundColor, iconShape)
         .clip(iconShape)
-        .mouseClickable {
-            if (buttons.isSecondaryPressed) {
-                onRightClick()
-            } else {
-                onClick()
-            }
+        .onMousePress {
+            onLeftClick { onClick() }
+            onRigntClick { onRightClick() }
         }
 ) {
     Box(
@@ -59,14 +54,7 @@ fun ShapedIcon(
             customContent()
         } else {
             Icon(
-                modifier = Modifier.fillMaxSize()
-                    .mouseClickable {
-                        if (buttons.isSecondaryPressed) {
-                            onRightClick()
-                        } else {
-                            onClick()
-                        }
-                    },
+                modifier = Modifier.fillMaxSize(),
                 imageVector = imageVector,
                 contentDescription = contentDescription,
                 tint = iconColor

+ 0 - 1
src/main/kotlin/eu/mjdev/desktop/components/main/MainWindow.kt

@@ -10,7 +10,6 @@ import eu.mjdev.desktop.components.controlcenter.ControlCenter
 import eu.mjdev.desktop.components.desktop.Desktop
 import eu.mjdev.desktop.components.desktoppanel.DesktopPanel
 import eu.mjdev.desktop.components.greeter.Greeter
-import eu.mjdev.desktop.components.info.InfoWindow
 import eu.mjdev.desktop.components.sliding.base.VisibilityState.Companion.rememberVisibilityState
 import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
 import eu.mjdev.desktop.windows.DesktopWindow

+ 6 - 8
src/main/kotlin/eu/mjdev/desktop/components/sliding/SlidingMenu.kt

@@ -9,14 +9,12 @@ import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.pointer.PointerEventType
-import androidx.compose.ui.input.pointer.onPointerEvent
 import eu.mjdev.desktop.components.sliding.base.VisibilityState
 import eu.mjdev.desktop.components.sliding.base.VisibilityState.Companion.rememberVisibilityState
+import eu.mjdev.desktop.extensions.Compose.onMouseEnter
+import eu.mjdev.desktop.extensions.Compose.onMouseLeave
 
-@OptIn(ExperimentalComposeUiApi::class)
 @Suppress("FunctionName")
 @Composable
 fun SlidingMenu(
@@ -35,8 +33,8 @@ fun SlidingMenu(
         Horizontal -> Box(
             modifier = modifier
                 .fillMaxHeight()
-                .onPointerEvent(PointerEventType.Enter) { onPointerEnter() }
-                .onPointerEvent(PointerEventType.Exit) { onPointerLeave() }
+                .onMouseEnter { onPointerEnter() }
+                .onMouseLeave { onPointerLeave() }
         ) {
             content(state.isVisible)
         }
@@ -44,8 +42,8 @@ fun SlidingMenu(
         Vertical -> Box(
             modifier = modifier
                 .fillMaxWidth()
-                .onPointerEvent(PointerEventType.Enter) { onPointerEnter() }
-                .onPointerEvent(PointerEventType.Exit) { onPointerLeave() }
+                .onMouseEnter { onPointerEnter() }
+                .onMouseLeave { onPointerLeave() }
         ) {
             content(state.isVisible)
         }

+ 36 - 14
src/main/kotlin/eu/mjdev/desktop/data/App.kt

@@ -6,6 +6,9 @@ import androidx.compose.ui.graphics.Color
 import eu.mjdev.desktop.helpers.exception.EmptyException.Companion.EmptyException
 import eu.mjdev.desktop.helpers.system.Shell
 import eu.mjdev.desktop.provider.DesktopProvider
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
 import java.io.File
 
 @Suppress("unused", "MemberVisibilityCanBePrivate")
@@ -46,6 +49,14 @@ class App(
 
     var process: Shell? = null
 
+    var subprocessId: Long? = null
+    val subProcess
+        get() = subprocessId?.let { spId ->
+            process?.allProcesses?.firstOrNull { p ->
+                p.pid() == spId
+            }
+        }
+
     var isStartingState = mutableStateOf(false)
     var isStarting: Boolean
         get() = isStartingState.value
@@ -71,21 +82,32 @@ class App(
         get() = process?.pids ?: emptyList()
 
     fun start(
-        api: DesktopProvider
+        api: DesktopProvider,
+        scope: CoroutineScope = api.scope
     ) = runCatching {
-        println("Starting app: $name [$fileName]")
-        triggerStart()
-        process = Shell(api)
-            .writeCommand("export DBUS_SESSION_BUS_ADDRESS=\"unix:path=\$XDG_RUNTIME_DIR/bus\"")
-            .writeCommand("gtk-launch", fileName)
-            .apply {
-                triggerStarted()
-                println("app started")
-            }
-            .onExit {
-                triggerStop()
-                println("app stopped")
-            }
+        scope.launch {
+            println("Starting app: $name [$fileName].")
+            triggerStart()
+            process = Shell(api)
+                .writeCommand("export DBUS_SESSION_BUS_ADDRESS=\"unix:path=\$XDG_RUNTIME_DIR/bus\"")
+//                .writeCommand("gtk-launch", fileName, "&")
+                .writeCommand("$cmd  &")
+                .writeCommand( "echo \$\$")
+                .writeCommand( "exit")
+                .apply {
+                    triggerStarted()
+                    println("app started.")
+                    val subProcessString = readOutput().trim()
+                    println("shell out: $subProcessString")
+                    subprocessId = runCatching { subProcessString.toLong() }.getOrNull()
+                    println("subprocess id : $subprocessId")
+                    while (subProcess?.isAlive == true) {
+                        delay(100)
+                    }
+                    triggerStop()
+                    println("app stopped")
+                }
+        }
     }.onFailure { error ->
         triggerStop(error)
     }

+ 53 - 0
src/main/kotlin/eu/mjdev/desktop/extensions/Compose.kt

@@ -5,7 +5,10 @@ package eu.mjdev.desktop.extensions
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.material.ButtonDefaults
 import androidx.compose.runtime.*
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.*
 import androidx.compose.ui.unit.*
 import coil3.ImageLoader
 import coil3.PlatformContext
@@ -126,6 +129,56 @@ object Compose {
         calculation: () -> T
     ) = remember(*key) { derivedStateOf { calculation() } }
 
+    @OptIn(ExperimentalComposeUiApi::class)
+    fun Modifier.onMouseEnter(
+        pass: PointerEventPass = PointerEventPass.Main,
+        onEvent: AwaitPointerEventScope.(event: PointerEvent) -> Unit
+    ): Modifier = onPointerEvent(
+        eventType = PointerEventType.Enter,
+        pass = pass,
+        onEvent = onEvent
+    )
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    fun Modifier.onMouseLeave(
+        pass: PointerEventPass = PointerEventPass.Main,
+        onEvent: AwaitPointerEventScope.(event: PointerEvent) -> Unit
+    ): Modifier = onPointerEvent(
+        eventType = PointerEventType.Exit,
+        pass = pass,
+        onEvent = onEvent
+    )
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    fun Modifier.onMousePress(
+        pass: PointerEventPass = PointerEventPass.Main,
+        onEvent: AwaitPointerEventScope.(event: PointerEvent) -> Unit
+    ): Modifier = onPointerEvent(
+        eventType = PointerEventType.Press,
+        pass = pass,
+        onEvent = onEvent
+    )
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    val AwaitPointerEventScope.isPrimary
+        get() = currentEvent.button.isPrimary
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    val AwaitPointerEventScope.isSecondary
+        get() = currentEvent.button.isSecondary
+
+    fun AwaitPointerEventScope.onLeftClick(
+        block: AwaitPointerEventScope.() -> Unit
+    ) {
+        if (isPrimary) block()
+    }
+
+    fun AwaitPointerEventScope.onRigntClick(
+        block: AwaitPointerEventScope.() -> Unit
+    ) {
+        if (isSecondary) block()
+    }
+
 //    Toolkit.getDefaultToolkit().addAWTEventListener({ event ->
 //        }, AWTEvent.MOUSE_EVENT_MASK or AWTEvent.FOCUS_EVENT_MASK
 //    )

+ 12 - 2
src/main/kotlin/eu/mjdev/desktop/extensions/Custom.kt

@@ -11,6 +11,7 @@ import eu.mjdev.desktop.helpers.streams.ResourceStream
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.channelFlow
 import kotlinx.coroutines.launch
+import java.io.BufferedReader
 import java.text.DateFormat
 import java.util.*
 import kotlin.jvm.optionals.getOrNull
@@ -18,6 +19,14 @@ import kotlin.jvm.optionals.getOrNull
 @Suppress("FunctionName")
 object Custom {
 
+    fun BufferedReader.readAvailable(): String = StringBuilder().apply {
+        var ch = read()
+        while (ch != -1) {
+            append(Char(ch))
+            ch = read()
+        }
+    }.toString()
+
     fun ParsedList(
         value: String?,
         delimiter: String = ";"
@@ -76,9 +85,10 @@ object Custom {
         }.collectAsState(initial = "")
 
     // todo better solution
-    fun <T> SnapshotStateList<T>.invalidate() = toList().also {
+    fun <T> SnapshotStateList<T>.invalidate() = apply {
+        val old = toList()
         clear()
-        addAll(it)
+        addAll(old)
     }
 
     fun loadKey(key: String): String = runCatching {

+ 7 - 76
src/main/kotlin/eu/mjdev/desktop/helpers/system/Shell.kt

@@ -1,6 +1,7 @@
 package eu.mjdev.desktop.helpers.system
 
 import eu.mjdev.desktop.extensions.Custom.command
+import eu.mjdev.desktop.extensions.Custom.readAvailable
 import eu.mjdev.desktop.provider.DesktopProvider
 import java.io.BufferedReader
 import java.io.BufferedWriter
@@ -9,7 +10,7 @@ import java.io.BufferedWriter
 class Shell(
     val api: DesktopProvider,
     val shellType: ShellType = ShellType.SH,
-) : AutoCloseable {
+) {
     val environment: Environment = Environment()
     val process: Process = Runtime.getRuntime().exec(
         arrayOf(shellType.cmd),
@@ -39,66 +40,6 @@ class Shell(
     val childProcesses: List<ProcessHandle>
         get() = process.descendants().toList()
 
-//    val allWindows
-//        get() = api.windows.allSystemWindows
-
-//    val allWindowPids
-//        get() = allWindows.map { it.pid }.toList()
-
-//    val processWindows
-//        get() = allWindows.filter { pids.contains(it.pid) }
-
-//    val processWindowsPids
-//        get() = processWindows.map { it.pid }
-
-//    val hasWindow: Boolean
-//        get() = processWindows.isNotEmpty()
-
-//    val isRunning: Boolean = hasWindow
-
-//    fun isWindowFocus(api: DesktopProvider): Boolean =
-//        api.windows.isWindowActive(pids)
-
-//    fun hasWindow(api: DesktopProvider) =
-//        api.windows.getWindowsByPids(pids).isNotEmpty()
-
-//    fun requestWindowFocus(api: DesktopProvider) {
-//        val windows = api.windows.getWindowsByPids(pids)
-//        if (windows.isNotEmpty()) {
-//            windows.forEach { w -> w.toFront() }
-//        }
-//    }
-
-//    fun minimizeWindow(api: DesktopProvider) {
-//        val windows = api.windows.getWindowsByPids(pids)
-//        if (windows.isNotEmpty()) {
-//            windows.forEach { w -> w.minimize() }
-//        }
-//    }
-
-//    fun maximizeWindow(api: DesktopProvider) {
-//        val windows = api.windows.getWindowsByPids(pids)
-//        if (windows.isNotEmpty()) {
-//            windows.forEach { w -> w.maximize() }
-//        }
-//    }
-
-//    fun closeWindow(api: DesktopProvider) {
-//        val windows = api.windows.getWindowsByPids(pids)
-//        if (windows.isNotEmpty()) {
-//            windows.forEach { w -> w.close() }
-//        } else {
-//            close()
-//        }
-//    }
-
-    fun onExit(
-        block: (Shell) -> Unit
-    ): Shell {
-        waitFor(block)
-        return this
-    }
-
     fun getProcessPids(): List<Long> = mutableListOf<Long>().apply {
         add(process.pid())
         addAll(process.descendants().map { it.pid() }.toList())
@@ -107,18 +48,6 @@ class Shell(
     fun toHandle(): ProcessHandle =
         process.toHandle()
 
-    fun waitFor(
-        block: (Shell) -> Unit
-    ): Shell {
-//        while (hasWindow) {
-//            process.waitFor()
-//        }
-        return apply {
-            block(this)
-            close()
-        }
-    }
-
     fun writeCommand(cmd: String): Shell {
         output.write(cmd + "\n")
         output.flush()
@@ -132,7 +61,7 @@ class Shell(
     }
 
     fun readOutput(): String =
-        input.readText()
+        input.readAvailable()
 
     fun readOutputLines(): List<String> = input.readLines().filter {
         it.isNotEmpty()
@@ -143,8 +72,10 @@ class Shell(
         return this
     }
 
-    override fun close() =
+    fun close(): Shell {
         process.destroy()
+        return this
+    }
 
     companion object {
         fun executeAndRead(cmd: String, vararg args: String): String =
@@ -174,4 +105,4 @@ class Shell(
         SH("/bin/sh"),
         BASH("/bin/bash")
     }
-}
+}

+ 3 - 0
src/main/kotlin/eu/mjdev/desktop/provider/DesktopScope.kt

@@ -81,6 +81,9 @@ open class DesktopScope(
     val appCategoriesAndApps
         get() = appsProvider.categoriesAndApps
 
+    val favoriteApps
+        get() = appsProvider.favoriteApps
+
     val palette
         get() = api.palette
 

+ 20 - 20
src/main/kotlin/eu/mjdev/desktop/windows/ChromeWindow.kt

@@ -29,7 +29,7 @@ fun ChromeWindow(
     enabled: Boolean = true,
     focusable: Boolean = true,
     alwaysOnTop: Boolean = true,
-//    alwaysOnBottom: Boolean = false,
+    alwaysOnBottom: Boolean = false,
     enterAnimation: EnterTransition = fadeIn() + slideInVertically(initialOffsetY = { it }),
     exitAnimation: ExitTransition = slideOutVertically(targetOffsetY = { it }) + fadeOut(),
     onCreate: ChromeWindowState.() -> Unit = {},
@@ -48,7 +48,7 @@ fun ChromeWindow(
     enabled = enabled,
     focusable = focusable,
     alwaysOnTop = alwaysOnTop,
-//    alwaysOnBottom = alwaysOnBottom,
+    alwaysOnBottom = alwaysOnBottom,
     enterAnimation = enterAnimation,
     exitAnimation = exitAnimation,
     onCreate = onCreate,
@@ -73,7 +73,7 @@ fun ChromeWindow(
     enabled: Boolean = true,
     focusable: Boolean = true,
     alwaysOnTop: Boolean = true,
-//    alwaysOnBottom: Boolean = false,
+    alwaysOnBottom: Boolean = false,
     enterAnimation: EnterTransition = fadeIn() + slideInVertically(initialOffsetY = { it }),
     exitAnimation: ExitTransition = slideOutVertically(targetOffsetY = { it }) + fadeOut(),
     onCreate: ChromeWindowState.() -> Unit = {},
@@ -92,7 +92,7 @@ fun ChromeWindow(
     enabled = enabled,
     focusable = focusable,
     alwaysOnTop = alwaysOnTop,
-//    alwaysOnBottom = alwaysOnBottom,
+    alwaysOnBottom = alwaysOnBottom,
     enterAnimation = enterAnimation,
     exitAnimation = exitAnimation,
     onCreate = onCreate,
@@ -116,7 +116,7 @@ fun ChromeWindow(
     enabled: Boolean = true,
     focusable: Boolean = true,
     alwaysOnTop: Boolean = true,
-//    alwaysOnBottom: Boolean = false,
+    alwaysOnBottom: Boolean = false,
     enterAnimation: EnterTransition = fadeIn() + slideInVertically(initialOffsetY = { it }),
     exitAnimation: ExitTransition = slideOutVertically(targetOffsetY = { it }) + fadeOut(),
     onCloseRequest: () -> Unit = {},
@@ -151,7 +151,7 @@ fun ChromeWindow(
         }
         windowState.onClosed.add(onClosed)
         windowState.onFocusChange.add(onFocusChange)
-        WindowEx(
+        Window(
             onCreate = { window ->
                 windowState.window = window
                 onCreate(windowState)
@@ -159,20 +159,20 @@ fun ChromeWindow(
 //        onOpen = {
 //            onOpened(windowState)
 //        },
-            onCloseRequest,
-            windowState,
-            wnState.value,
-            "",
-            null,
-            true,
-            transparent,
-            resizable,
-            enabled,
-            focusable,
-            alwaysOnTop,
-//        alwaysOnBottom,
-            onPreviewKeyEvent,
-            onKeyEvent,
+            onCloseRequest = onCloseRequest,
+            state = windowState,
+            visible = wnState.value,
+            title = "",
+            icon = null,
+            undecorated = true,
+            transparent = transparent,
+            resizable = resizable,
+            enabled = enabled,
+            focusable = focusable,
+            alwaysOnTop = alwaysOnTop,
+            alwaysOnBottom = alwaysOnBottom,
+            onPreviewKeyEvent = onPreviewKeyEvent,
+            onKeyEvent = onKeyEvent,
             stateHelper = windowState.stateHelper,
             focusHelper = windowState.focusHelper,
             content = {

+ 9 - 9
src/main/kotlin/eu/mjdev/desktop/windows/DesktopWindow.kt

@@ -1,21 +1,20 @@
 package eu.mjdev.desktop.windows
 
 import androidx.compose.desktop.ui.tooling.preview.Preview
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.mouseClickable
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.input.pointer.isSecondaryPressed
 import eu.mjdev.desktop.components.background.BackgroundImage
 import eu.mjdev.desktop.components.sliding.base.VisibilityState
+import eu.mjdev.desktop.extensions.Compose.onLeftClick
+import eu.mjdev.desktop.extensions.Compose.onMousePress
+import eu.mjdev.desktop.extensions.Compose.onRigntClick
 import eu.mjdev.desktop.provider.DesktopProvider
 import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
 
-@OptIn(ExperimentalFoundationApi::class)
-@Suppress("FunctionName", "DEPRECATION")
+@Suppress("FunctionName")
 @Preview
 @Composable
 fun DesktopWindow(
@@ -59,14 +58,15 @@ fun DesktopWindow(
         BackgroundImage(
             modifier = Modifier
                 .fillMaxSize()
-                .mouseClickable {
-                    if (buttons.isSecondaryPressed) {
-//                        contextMenuState.show()
-                    } else {
+                .onMousePress {
+                    onLeftClick {
                         panelState.hide()
                         menuState.hide()
                         controlCenterState.hide()
                     }
+                    onRigntClick {
+//                        contextMenuState.show()
+                    }
                 },
             onChange = { src ->
                 api.palette.apply {

+ 30 - 74
src/main/kotlin/eu/mjdev/desktop/windows/Window.kt

@@ -15,19 +15,14 @@ import androidx.compose.ui.window.*
 import eu.mjdev.desktop.windows.ChromeWindowState.Companion.rememberChromeWindowState
 import java.awt.*
 import java.awt.event.*
-import java.awt.image.BufferedImage
-import java.awt.image.ConvolveOp
-import java.awt.image.Kernel
 import java.util.*
 import javax.swing.JFrame
-import javax.swing.JPanel
 import kotlin.math.roundToInt
 
 @Suppress("FunctionName")
 @Preview
 @Composable
 fun Window(
-//    onOpen: (window: ComposeWindow) -> Unit = {},
     onCreate: (window: ComposeWindow) -> Unit = {},
     onOpen: (window: ComposeWindow) -> Unit = {},
     onClosing: (window: ComposeWindow) -> Unit = {},
@@ -50,8 +45,11 @@ fun Window(
     enabled: Boolean = true,
     focusable: Boolean = true,
     alwaysOnTop: Boolean = false,
-//    alwaysOnBottom: Boolean = false,
+    alwaysOnBottom: Boolean = false,
     closeAction: Int = JFrame.DO_NOTHING_ON_CLOSE,
+//    blurEnabled: Boolean = true,
+    focusHelper: WindowFocusHelper,
+    stateHelper: WindowStateHelper,
     onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
     onKeyEvent: (KeyEvent) -> Boolean = { false },
     content: @Composable FrameWindowScope.() -> Unit
@@ -65,7 +63,7 @@ fun Window(
     val currentEnabled by rememberUpdatedState(enabled)
     val currentFocusable by rememberUpdatedState(focusable)
     val currentAlwaysOnTop by rememberUpdatedState(alwaysOnTop)
-//    val currentAlwaysOnBottom by rememberUpdatedState(alwaysOnBottom)
+    val currentAlwaysOnBottom by rememberUpdatedState(alwaysOnBottom)
     val currentOnCloseRequest by rememberUpdatedState(onCloseRequest)
     val updater = remember(::ComponentUpdater)
     val appliedState = remember {
@@ -94,8 +92,9 @@ fun Window(
         onPreviewKeyEvent = onPreviewKeyEvent,
         onKeyEvent = onKeyEvent,
         create = {
-            val graphicsConfiguration = WindowLocationTracker.lastActiveGraphicsConfiguration
-            ComposeWindow(graphicsConfiguration = graphicsConfiguration).apply {
+            ComposeWindow(
+                graphicsConfiguration = WindowLocationTracker.lastActiveGraphicsConfiguration
+            ).apply {
                 defaultCloseOperation = closeAction
                 listeners.windowListenerRef.registerWithAndSet(
                     this,
@@ -153,14 +152,14 @@ fun Window(
                 set(currentTransparent, window::isTransparent::set)
                 set(currentResizable, window::setResizable)
                 set(currentEnabled, window::setEnabled)
-//                if (alwaysOnBottom) {
-//                    set(false, window::setFocusableWindowState)
-//                    set(false, window::setAlwaysOnTop)
-//                    set(false, window::setAutoRequestFocus)
-//                } else {
-                set(currentFocusable, window::setFocusableWindowState)
-                set(currentAlwaysOnTop, window::setAlwaysOnTop)
-//                }
+                if (currentAlwaysOnBottom) {
+                    set(false, window::setFocusableWindowState)
+                    set(false, window::setAlwaysOnTop)
+                    set(false, window::setAutoRequestFocus)
+                } else {
+                    set(currentFocusable, window::setFocusableWindowState)
+                    set(currentAlwaysOnTop, window::setAlwaysOnTop)
+                }
             }
             if (state.size != appliedState.size) {
                 window.setSizeSafely(state.size, state.placement)
@@ -183,7 +182,12 @@ fun Window(
                 appliedState.isMinimized = state.isMinimized
             }
         },
-        content = content
+        content = {
+//            windowBlur(blurEnabled)
+            stateHelper.register(window)
+            focusHelper.register(window)
+            content()
+        }
     )
 }
 
@@ -370,22 +374,20 @@ fun Window(
             create().apply {
                 this.compositionLocalContext = compositionLocalContext
                 this.exceptionHandler = windowExceptionHandlerFactory.exceptionHandler(this)
-//                BlurBackground.applyBlurBackground(this)
                 setContent(onPreviewKeyEvent, onKeyEvent, content)
             }
         },
         dispose = {
             dispose(it)
         },
-        update = { component ->
-            component.compositionLocalContext = compositionLocalContext
-            component.exceptionHandler = windowExceptionHandlerFactory.exceptionHandler(component)
-            component.componentOrientation = layoutDirection.componentOrientation
-            val wasDisplayable = component.isDisplayable
-            update(component)
-//            BlurBackground.applyBlurBackground(component)
-            if (!wasDisplayable && component.isDisplayable) {
-                component.contentPane.paint(component.contentPane.graphics)
+        update = { window ->
+            window.compositionLocalContext = compositionLocalContext
+            window.exceptionHandler = windowExceptionHandlerFactory.exceptionHandler(window)
+            window.componentOrientation = layoutDirection.componentOrientation
+            val wasDisplayable = window.isDisplayable
+            update(window)
+            if (!wasDisplayable && window.isDisplayable) {
+                window.contentPane.paint(window.contentPane.graphics)
             }
         },
     )
@@ -452,49 +454,3 @@ class WindowEventsAdapter(
         currentOnCloseRequest()
     }
 }
-
-@Suppress("unused")
-object BlurBackground {
-    private val blurKernel = floatArrayOf(
-        0.0625f, 0.125f, 0.0625f,
-        0.125f, 0.25f, 0.125f,
-        0.0625f, 0.125f, 0.0625f
-    )
-    private val kernel = Kernel(3, 3, blurKernel)
-    private val blurOp = ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null)
-
-    fun applyBlurBackground(frame: JFrame) {
-        try {
-            BufferedImage(
-                frame.width,
-                frame.height,
-                BufferedImage.TYPE_INT_ARGB
-            ).let { image ->
-                image.graphics.also { graphics ->
-                    frame.contentPane.paint(graphics)
-//                    graphics.dispose()
-                    blurOp.filter(image, null).let { blurredImage ->
-                        frame.contentPane = BackgroundPanel(blurredImage)
-//                        frame.contentPane.layout = null
-                    }
-                }
-            }
-        } catch (e: Throwable) {
-            e.printStackTrace()
-        }
-    }
-
-    private class BackgroundPanel(
-        private val image: BufferedImage
-    ) : JPanel() {
-        init {
-            isOpaque = false
-        }
-
-        override fun paintComponent(g: Graphics) {
-            super.paintComponent(g)
-            val r = g.clipBounds
-            g.drawImage(image, r.x, r.y, r.width, r.height, null)
-        }
-    }
-}

+ 0 - 54
src/main/kotlin/eu/mjdev/desktop/windows/WindowEx.kt

@@ -1,54 +0,0 @@
-package eu.mjdev.desktop.windows
-
-import androidx.compose.desktop.ui.tooling.preview.Preview
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.awt.ComposeWindow
-import androidx.compose.ui.graphics.painter.Painter
-import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.window.FrameWindowScope
-import eu.mjdev.desktop.windows.ChromeWindowState.Companion.rememberChromeWindowState
-
-@Suppress("FunctionName")
-@Preview
-@Composable
-fun WindowEx(
-    onCreate: (window: ComposeWindow) -> Unit = {},
-    onCloseRequest: () -> Unit,
-    state: ChromeWindowState = rememberChromeWindowState(),
-    visible: Boolean = true,
-    title: String = "Untitled",
-    icon: Painter? = null,
-    undecorated: Boolean = true,
-    transparent: Boolean = true,
-    resizable: Boolean = true,
-    enabled: Boolean = true,
-    focusable: Boolean = true,
-    alwaysOnTop: Boolean = false,
-//    alwaysOnBottom: Boolean = false,
-    onPreviewKeyEvent: (KeyEvent) -> Boolean = { false },
-    onKeyEvent: (KeyEvent) -> Boolean = { false },
-    focusHelper: WindowFocusHelper,
-    stateHelper: WindowStateHelper,
-    content: @Composable FrameWindowScope.() -> Unit = {}
-) = Window(
-    onCreate = onCreate,
-    onCloseRequest = onCloseRequest,
-    state = state,
-    visible = visible,
-    title = title,
-    icon = icon,
-    undecorated = undecorated,
-    transparent = transparent,
-    resizable = resizable,
-    enabled = enabled,
-    focusable = focusable,
-    alwaysOnTop = alwaysOnTop,
-//    alwaysOnBottom = alwaysOnBottom,
-    onPreviewKeyEvent = onPreviewKeyEvent,
-    onKeyEvent = onKeyEvent,
-    content = {
-        stateHelper.register(window)
-        focusHelper.register(window)
-        content()
-    }
-)

+ 37 - 0
src/main/kotlin/eu/mjdev/desktop/windows/blur/WindowBlurManager.kt

@@ -0,0 +1,37 @@
+package eu.mjdev.desktop.windows.blur
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.window.WindowScope
+import eu.mjdev.desktop.windows.blur.base.StubWindowBlurManager
+import eu.mjdev.desktop.windows.blur.linux.LinuxWindowBlurManager
+import org.jetbrains.skiko.OS
+import org.jetbrains.skiko.hostOs
+import java.awt.Window
+
+@Suppress("unused")
+interface WindowBlurManager {
+    var blurEnabled: Boolean
+
+    companion object {
+        private fun WindowBlurManager(
+            window: Window,
+            blurEnabled: Boolean = false,
+        ) = when (hostOs) {
+            // todo other platforms
+            OS.Linux -> LinuxWindowBlurManager(window, blurEnabled)
+            else -> StubWindowBlurManager(blurEnabled)
+        }
+
+        @Composable
+        fun WindowScope.windowBlur(
+            blurEnabled: Boolean = false,
+        ) {
+            val manager = remember { WindowBlurManager(window, blurEnabled) }
+            LaunchedEffect(blurEnabled) {
+                manager.blurEnabled = blurEnabled
+            }
+        }
+    }
+}

+ 85 - 0
src/main/kotlin/eu/mjdev/desktop/windows/blur/base/HackedContentPane.kt

@@ -0,0 +1,85 @@
+package eu.mjdev.desktop.windows.blur.base
+
+import java.awt.AlphaComposite
+import java.awt.Graphics
+import java.awt.Graphics2D
+import java.awt.image.BufferedImage
+import javax.swing.JPanel
+
+@Suppress("MemberVisibilityCanBePrivate", "unused")
+class HackedContentPane : JPanel() {
+    private var image: BufferedImage? = null
+
+//    var jlayer: JLayer<JComponent> = JLayer(JPanel(), BlurLayerUI())
+
+    init {
+        isOpaque = false
+//        add(jlayer)
+    }
+
+    fun setImage(image: BufferedImage?) {
+        this.image = image
+    }
+
+    fun clear() = setImage(null)
+
+    // todo not painting nothing somehow
+    override fun paint(g: Graphics) {
+        if ((width <= 0) || (height <= 0)) {
+            return
+        }
+        val gg = g.create()
+        try {
+            if (background.alpha != 255) {
+                gg.color = background
+                if (gg is Graphics2D) {
+                    gg.composite = AlphaComposite.getInstance(AlphaComposite.SRC)
+                }
+                gg.fillRect(0, 0, width, height)
+            }
+            if (image != null) {
+                g.drawImage(image, 0, 0, width, height, null)
+            }
+        } finally {
+            gg.dispose()
+        }
+        super.paint(g)
+    }
+
+//    class BlurLayerUI : LayerUI<JComponent>() {
+//        private var mOffscreenImage: BufferedImage? = null
+//        private val mOperation: BufferedImageOp
+//
+//        init {
+//            val ninth = 1.0f / 9.0f
+//            val blurKernel = floatArrayOf(
+//                ninth, ninth, ninth, ninth, ninth, ninth,
+//                ninth, ninth, ninth
+//            )
+//            mOperation = ConvolveOp(
+//                Kernel(3, 3, blurKernel),
+//                ConvolveOp.EDGE_NO_OP, null
+//            )
+//        }
+//
+//        override fun paint(g: Graphics, c: JComponent) {
+//            val w = c.width
+//            val h = c.height
+//            if (w == 0 || h == 0) {
+//                return
+//            }
+//            if (mOffscreenImage == null || mOffscreenImage!!.width != w || mOffscreenImage!!.height != h) {
+//                mOffscreenImage = BufferedImage(
+//                    w, h,
+//                    BufferedImage.TYPE_INT_RGB
+//                )
+//            }
+//            val ig2 = mOffscreenImage!!.createGraphics()
+//            ig2.clip = g.clip
+//            super.paint(ig2, c)
+//            ig2.dispose()
+//            val g2 = g as Graphics2D
+//            g2.drawImage(mOffscreenImage, mOperation, 0, 0)
+//        }
+//    }
+}

+ 7 - 0
src/main/kotlin/eu/mjdev/desktop/windows/blur/base/StubWindowBlurManager.kt

@@ -0,0 +1,7 @@
+package eu.mjdev.desktop.windows.blur.base
+
+import eu.mjdev.desktop.windows.blur.WindowBlurManager
+
+class StubWindowBlurManager(
+    override var blurEnabled: Boolean
+) : WindowBlurManager

+ 36 - 0
src/main/kotlin/eu/mjdev/desktop/windows/blur/linux/BlurBackground.kt

@@ -0,0 +1,36 @@
+package eu.mjdev.desktop.windows.blur.linux
+
+import eu.mjdev.desktop.helpers.image.filters.GaussianFilter
+import eu.mjdev.desktop.windows.blur.base.HackedContentPane
+import eu.mjdev.desktop.windows.blur.linux.LinuxWindowBlurManager.Companion.contentPane
+import java.awt.Container
+import java.awt.Window
+import java.awt.image.BufferedImage
+
+object BlurBackground {
+    fun applyBlurBackground(
+        window: Window,
+        blurRadius: Float = 3f,
+        contentPane: Container? = window.contentPane
+    ) {
+        val hackedPane = contentPane as? HackedContentPane
+        val isHacked = hackedPane != null
+        val width = window.width
+        val height = window.height
+        if (isHacked && (width > 0) && (height > 0)) {
+            try {
+                BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB).let { image ->
+                    image.graphics.also { graphics ->
+                        hackedPane?.paint(graphics)
+                        graphics.dispose()
+                        GaussianFilter(blurRadius).filter(image, null).let { blurredImage ->
+                            hackedPane?.setImage(blurredImage)
+                        }
+                    }
+                }
+            } catch (e: Throwable) {
+                e.printStackTrace()
+            }
+        }
+    }
+}

+ 119 - 0
src/main/kotlin/eu/mjdev/desktop/windows/blur/linux/LinuxWindowBlurManager.kt

@@ -0,0 +1,119 @@
+package eu.mjdev.desktop.windows.blur.linux
+
+import androidx.compose.ui.awt.ComposeWindow
+import eu.mjdev.desktop.windows.blur.WindowBlurManager
+import eu.mjdev.desktop.windows.blur.base.HackedContentPane
+import org.jetbrains.skiko.SkiaLayer
+import java.awt.AWTEvent
+import java.awt.BorderLayout
+import java.awt.Component
+import java.awt.Window
+import java.awt.event.ComponentAdapter
+import java.awt.event.ComponentEvent
+import java.lang.reflect.Field
+import java.lang.reflect.Method
+import javax.swing.*
+
+@Suppress("MemberVisibilityCanBePrivate")
+class LinuxWindowBlurManager(
+    val window: Window,
+    blurEnabled: Boolean = false,
+) : WindowBlurManager {
+    override var blurEnabled: Boolean = blurEnabled
+        set(value) {
+            if (field != value) {
+                field = value
+                updateBlur()
+            }
+        }
+
+    init {
+        SwingUtilities.invokeLater {
+                if (window is ComposeWindow) window.setComposeLayerTransparency(true)
+                window.hackContentPane()
+            updateBlur()
+        }
+    }
+
+    private fun updateBlur() {
+        BlurBackground.applyBlurBackground(window)
+    }
+
+    val componentAdapter = object : ComponentAdapter() {
+        override fun componentMoved(e: ComponentEvent?) = resetTransparent(e)
+        override fun componentResized(e: ComponentEvent?) = resetTransparent(e)
+        override fun componentShown(e: ComponentEvent?) = resetTransparent(e)
+    }
+
+    @Suppress("UNUSED_PARAMETER")
+    private fun resetTransparent(e: AWTEvent?) {
+        if (blurEnabled) updateBlur()
+    }
+
+    init {
+        window.addComponentListener(componentAdapter)
+    }
+
+    companion object {
+//        val Window.isTransparent
+//            get() = when (this) {
+//                is ComposeWindow -> skiaLayer?.transparency
+//                else -> background.alpha != 255
+//            }
+
+        var Window.contentPane
+            get() = when (this) {
+                is JFrame -> contentPane
+                is JDialog -> contentPane
+                is JWindow -> contentPane
+                else -> null
+            }
+            set(value) = when (this) {
+                is JFrame -> contentPane = value
+                is JDialog -> contentPane = value
+                is JWindow -> contentPane = value
+                else -> throw IllegalStateException()
+            }
+
+        val delegateField: Field?
+            get() = runCatching {
+                ComposeWindow::class.java.getDeclaredField("delegate").apply { isAccessible = true }
+            }.getOrNull()
+
+        val getLayerMethod: Method?
+            get() = runCatching {
+                delegateField?.type?.getDeclaredMethod("getLayer")?.apply { isAccessible = true }
+            }.getOrNull()
+
+        val getComponentMethod: Method?
+            get() = runCatching {
+                getLayerMethod?.returnType?.getDeclaredMethod("getComponent")
+            }.getOrNull()
+
+        val ComposeWindow.skiaLayer: SkiaLayer?
+            get() {
+                val delegate = delegateField?.get(this)
+                val layer = getLayerMethod?.invoke(delegate)
+                return getComponentMethod?.invoke(layer) as? SkiaLayer
+            }
+
+        fun ComposeWindow.setComposeLayerTransparency(isTransparent: Boolean) {
+            skiaLayer?.transparency = isTransparent
+        }
+
+        fun Window.hackContentPane() {
+            println("hacking window: ${this.name}")
+            val oldContentPane = contentPane ?: return
+            val newContentPane: JComponent = HackedContentPane()
+            newContentPane.name = "$name.contentPane"
+            newContentPane.layout = object : BorderLayout() {
+                override fun addLayoutComponent(comp: Component, constraints: Any?) {
+                    super.addLayoutComponent(comp, constraints ?: CENTER)
+                }
+            }
+            newContentPane.background = java.awt.Color(0, 0, 0, 0)
+            oldContentPane.components.forEach { newContentPane.add(it) }
+            contentPane = newContentPane
+        }
+    }
+}