diff --git a/launcher/pom.xml b/launcher/pom.xml index e56cb8d91ee7..105b02141544 100644 --- a/launcher/pom.xml +++ b/launcher/pom.xml @@ -72,6 +72,20 @@ false + + zip-file-linux + install + + single + + + + src/assembly/bin-linux.xml + + triggevent-linux + false + + @@ -164,6 +178,24 @@ + + copy-linux-scripts + + package + + copy-resources + + + ${basedir}/target/windows + + + ${project.basedir}/src/linux + *.sh + + + + + diff --git a/launcher/src/assembly/bin-linux.xml b/launcher/src/assembly/bin-linux.xml new file mode 100644 index 000000000000..4452d7379005 --- /dev/null +++ b/launcher/src/assembly/bin-linux.xml @@ -0,0 +1,19 @@ + + zip + + zip + + ${project.basedir} + + + ${project.basedir}/target/windows + triggevent + + **/src.zip + jre/ + + + + \ No newline at end of file diff --git a/launcher/src/assembly/bin-no-jre.xml b/launcher/src/assembly/bin-no-jre.xml index 4452d7379005..afd227d8aa14 100644 --- a/launcher/src/assembly/bin-no-jre.xml +++ b/launcher/src/assembly/bin-no-jre.xml @@ -13,6 +13,7 @@ **/src.zip jre/ + *.sh diff --git a/launcher/src/assembly/bin.xml b/launcher/src/assembly/bin.xml index 7679f51b9f11..7c1fe1f2a3c0 100644 --- a/launcher/src/assembly/bin.xml +++ b/launcher/src/assembly/bin.xml @@ -12,6 +12,7 @@ triggevent **/src.zip + *.sh diff --git a/launcher/src/linux/setup-java.sh b/launcher/src/linux/setup-java.sh new file mode 100644 index 000000000000..63bee07b6191 --- /dev/null +++ b/launcher/src/linux/setup-java.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +if wget -O /tmp/java.tar.gz https://download.oracle.com/java/17/archive/jdk-17.0.4_linux-x64_bin.tar.gz +then + tar -xzf /tmp/java.tar.gz + mv jdk-* jre +else + echo "Download failed" +fi diff --git a/launcher/src/linux/triggevent-import.sh b/launcher/src/linux/triggevent-import.sh new file mode 100644 index 000000000000..65406e4bb808 --- /dev/null +++ b/launcher/src/linux/triggevent-import.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +if [ -x jre/bin/java ] +then + java=jre/bin/java + echo "Using directory-supplied Java" +else + java=$(command -v java) + if [ $? -ne 0 ] + then + echo "Found neither a 'jre' dir nor a built in 'java' executable. Please do one of the following to fix it:" + echo "1. Install Java 17 from your distro's package manager, and make sure the 'java' program on your PATH points to the correct java installation." + echo "2. Download Java 17 from https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html and place it in a directory called 'jre' (rename it from jdk-x.y.z)" + echo "3. Run setup-java.sh (requires wget)" + exit 1; + fi +fi + +$java -cp './userdata:./preload/*:./deps/*:./user/*:launcher-1.0-SNAPSHOT.jar' @args.txt gg.xp.xivsupport.gui.GuiImportLaunch \ No newline at end of file diff --git a/launcher/src/linux/triggevent-upd.sh b/launcher/src/linux/triggevent-upd.sh new file mode 100644 index 000000000000..0a0075b60b8e --- /dev/null +++ b/launcher/src/linux/triggevent-upd.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +if [ -x jre/bin/java ] +then + java=jre/bin/java + echo "Using directory-supplied Java" +else + java=$(command -v java) + if [ $? -ne 0 ] + then + echo "Found neither a 'jre' dir nor a built in 'java' executable. Please do one of the following to fix it:" + echo "1. Install Java 17 from your distro's package manager, and make sure the 'java' program on your PATH points to the correct java installation." + echo "2. Download Java 17 from https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html and place it in a directory called 'jre' (rename it from jdk-x.y.z)" + echo "3. Run setup-java.sh (requires wget)" + exit 1; + fi +fi + +$java -cp 'launcher-1.0-SNAPSHOT.jar' @args.txt gg.xp.xivsupport.gui.Update \ No newline at end of file diff --git a/launcher/src/linux/triggevent.sh b/launcher/src/linux/triggevent.sh new file mode 100644 index 000000000000..0e9b0cfbf46f --- /dev/null +++ b/launcher/src/linux/triggevent.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +if [ -x jre/bin/java ] +then + java=jre/bin/java + echo "Using directory-supplied Java" +else + java=$(command -v java) + if [ $? -ne 0 ] + then + echo "Found neither a 'jre' dir nor a built in 'java' executable. Please do one of the following to fix it:" + echo "1. Install Java 17 from your distro's package manager, and make sure the 'java' program on your PATH points to the correct java installation." + echo "2. Download Java 17 from https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html and place it in a directory called 'jre' (rename it from jdk-x.y.z)" + echo "3. Run setup-java.sh (requires wget)" + exit 1; + fi +fi + +$java -cp './userdata:./preload/*:./deps/*:./user/*:launcher-1.0-SNAPSHOT.jar' @args.txt gg.xp.xivsupport.gui.GuiLaunch \ No newline at end of file diff --git a/launcher/src/main/java/gg/xp/xivsupport/gui/Update.java b/launcher/src/main/java/gg/xp/xivsupport/gui/Update.java index f3ae0dced8c5..9c23a635ad32 100644 --- a/launcher/src/main/java/gg/xp/xivsupport/gui/Update.java +++ b/launcher/src/main/java/gg/xp/xivsupport/gui/Update.java @@ -336,7 +336,12 @@ else if (depsFiles == null) { if (!updateTheUpdaterItself) { Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { - Runtime.getRuntime().exec(Paths.get(installDir.toString(), "triggevent.exe").toString()); + if (isWindows()) { + Runtime.getRuntime().exec(Paths.get(installDir.toString(), "triggevent.exe").toString()); + } + else { + Runtime.getRuntime().exec(new String[]{"sh", Paths.get(installDir.toString(), "triggevent.sh").toString()}); + } } catch (IOException e) { e.printStackTrace(); @@ -400,4 +405,8 @@ private static String getStackTrace(final Throwable throwable) { return sw.getBuffer().toString(); } + private static boolean isWindows() { + return System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("windows"); + } + } diff --git a/libjawt.so b/libjawt.so new file mode 100644 index 000000000000..80f0b74b01e0 Binary files /dev/null and b/libjawt.so differ diff --git a/pom.xml b/pom.xml index 8f58e0b8b016..ae5d5d1298a1 100644 --- a/pom.xml +++ b/pom.xml @@ -56,6 +56,7 @@ 1 true -Duser.country=US -Duser.language=en -DsequentialTriggerCycleTime=1000 + false diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/EnhancedWindowUtils.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/EnhancedWindowUtils.java new file mode 100644 index 000000000000..cf1946657933 --- /dev/null +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/EnhancedWindowUtils.java @@ -0,0 +1,2107 @@ +/* Copyright (c) 2007-2008 Timothy Wall, All Rights Reserved + * Copyright (c) 2007 Olivier Chafik + * + * The contents of this file is dual-licensed under 2 + * alternative Open Source/Free licenses: LGPL 2.1 or later and + * Apache License 2.0. (starting with JNA version 4.0.0). + * + * You can freely decide which license you want to apply to + * the project. + * + * You may obtain a copy of the LGPL License at: + * + * http://www.gnu.org/licenses/licenses.html + * + * A copy is also included in the downloadable source code package + * containing JNA, in file "LGPL2.1". + * + * You may obtain a copy of the Apache License at: + * + * http://www.apache.org/licenses/ + * + * A copy is also included in the downloadable source code package + * containing JNA, in file "AL2.0". + */ +package gg.xp.xivsupport.gui.overlay; + +import java.awt.AWTEvent; +import java.awt.AlphaComposite; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Container; +import java.awt.Dialog; +import java.awt.Dimension; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.Toolkit; +import java.awt.Window; +import java.awt.event.AWTEventListener; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.ContainerEvent; +import java.awt.event.HierarchyEvent; +import java.awt.event.HierarchyListener; +import java.awt.event.MouseEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.geom.Area; +import java.awt.geom.PathIterator; +import java.awt.image.BufferedImage; +import java.awt.image.Raster; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import javax.swing.Icon; +import javax.swing.JComponent; +import javax.swing.JLayeredPane; +import javax.swing.JPanel; +import javax.swing.JRootPane; +import javax.swing.PopupFactory; +import javax.swing.RootPaneContainer; +import javax.swing.SwingUtilities; + +import com.sun.jna.Memory; +import com.sun.jna.Native; +import com.sun.jna.NativeLong; +import com.sun.jna.Platform; +import com.sun.jna.Pointer; +import com.sun.jna.platform.DesktopWindow; +import com.sun.jna.platform.RasterRangesUtils; +import com.sun.jna.platform.unix.X11; +import com.sun.jna.platform.unix.X11.Display; +import com.sun.jna.platform.unix.X11.GC; +import com.sun.jna.platform.unix.X11.Pixmap; +import com.sun.jna.platform.unix.X11.XVisualInfo; +import com.sun.jna.platform.unix.X11.Xext; +import com.sun.jna.platform.unix.X11.Xrender.XRenderPictFormat; +import com.sun.jna.platform.win32.GDI32; +import com.sun.jna.platform.win32.Kernel32; +import com.sun.jna.platform.win32.Kernel32Util; +import com.sun.jna.platform.win32.PsapiUtil; +import com.sun.jna.platform.win32.User32; +import com.sun.jna.platform.win32.Win32Exception; +import com.sun.jna.platform.win32.WinBase; +import com.sun.jna.platform.win32.WinDef.DWORDByReference; +import com.sun.jna.platform.win32.WinDef.HBITMAP; +import com.sun.jna.platform.win32.WinDef.HDC; +import com.sun.jna.platform.win32.WinDef.HICON; +import com.sun.jna.platform.win32.WinDef.HRGN; +import com.sun.jna.platform.win32.WinDef.HWND; +import com.sun.jna.platform.win32.WinDef.LPARAM; +import com.sun.jna.platform.win32.WinDef.LRESULT; +import com.sun.jna.platform.win32.WinDef.POINT; +import com.sun.jna.platform.win32.WinDef.RECT; +import com.sun.jna.platform.win32.WinDef.WPARAM; +import com.sun.jna.platform.win32.WinError; +import com.sun.jna.platform.win32.WinGDI; +import com.sun.jna.platform.win32.WinGDI.BITMAP; +import com.sun.jna.platform.win32.WinGDI.BITMAPINFO; +import com.sun.jna.platform.win32.WinGDI.BITMAPINFOHEADER; +import com.sun.jna.platform.win32.WinGDI.ICONINFO; +import com.sun.jna.platform.win32.WinNT; +import com.sun.jna.platform.win32.WinNT.HANDLE; +import com.sun.jna.platform.win32.WinUser; +import com.sun.jna.platform.win32.WinUser.BLENDFUNCTION; +import com.sun.jna.platform.win32.WinUser.SIZE; +import com.sun.jna.platform.win32.WinUser.WNDENUMPROC; +import com.sun.jna.ptr.ByteByReference; +import com.sun.jna.ptr.IntByReference; +import com.sun.jna.ptr.PointerByReference; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Provides additional features on a Java {@link Window}. + * + * NOTE: since there is no explicit way to force PopupFactory to use a + * heavyweight popup, and anything but a heavyweight popup will be + * clipped by a window mask, an additional subwindow is added to all + * masked windows to implicitly force PopupFactory to use a heavyweight + * window and avoid clipping. + *

+ * NOTE: Neither shaped windows nor transparency + * currently works with Java 1.4 under X11. This is at least partly due + * to 1.4 using multiple X11 windows for a single given Java window. It + * *might* be possible to remedy by applying the window + * region/transparency to all descendants, but I haven't tried it. In + * addition, windows must be both displayable and visible + * before the corresponding native Drawable may be obtained; in later + * Java versions, the window need only be displayable. + *

+ * NOTE: If you use {@link EnhancedWindowUtils#setWindowMask(Window, Shape, int)} and override {@link + * Window#paint(Graphics)} on OS X, you'll need to explicitly set the clip + * mask on the Graphics object with the window mask; only the + * content pane of the window and below have the window mask automatically + * applied.

+ * NOTE: On OSX, the property + * apple.awt.draggableWindowBackground is set automatically when + * a window's background color has an alpha component. That property must be + * set to its final value before the heavyweight peer for the Window + * is created. Once {@link Component#addNotify} has been called on the + * component, causing creation of the heavyweight peer, changing this + * property has no effect. + * @see Apple Technote 2007 + * + * @author Andreas "PAX" Lück, onkelpax-git[at]yahoo.de + */ +// TODO: setWindowMask() should accept a threshold; some cases want a +// 50% threshold, some might want zero/non-zero +public class EnhancedWindowUtils { + + private static final Logger LOG = Logger.getLogger(EnhancedWindowUtils.class.getName()); + + private static final String TRANSPARENT_OLD_BG = "transparent-old-bg"; + private static final String TRANSPARENT_OLD_OPAQUE = "transparent-old-opaque"; + private static final String TRANSPARENT_ALPHA = "transparent-alpha"; + + /** Use this to clear a window mask. */ + public static final Shape MASK_NONE = null; + + /** + * This class forces a heavyweight popup on the parent + * {@link Window}. See the implementation of {@link PopupFactory}; + * a heavyweight is forced if there is an occluding subwindow on the + * target window. + *

+ * Ideally we'd have more control over {@link PopupFactory} but this + * is a fairly simple, lightweight workaround. Note that, at least as of + * JDK 1.6, the following do not have the desired effect:
+ *


+	 * ToolTipManager.sharedInstance().setLightWeightPopupEnabled(false);
+	 * JPopupMenu.setDefaultLightWeightPopupEnabled(false);
+	 * System.setProperty("JPopupMenu.defaultLWPopupEnabledKey", "false");
+	 * 
+ */ + private static class HeavyweightForcer extends Window { + private static final long serialVersionUID = 1L; + private final boolean packed; + + public HeavyweightForcer(Window parent) { + super(parent); + pack(); + packed = true; + } + + @Override + public boolean isVisible() { + // Only want to be 'visible' once the peer is instantiated + // via pack; this tricks PopupFactory into using a heavyweight + // popup to avoid being obscured by this window + return packed; + } + + @Override + public Rectangle getBounds() { + return getOwner().getBounds(); + } + } + /** + * This can be installed over a {@link JLayeredPane} in order to + * listen for repaint requests. The content's repaint method will be + * invoked whenever any part of the ancestor window is repainted. + */ + protected static class RepaintTrigger extends JComponent { + private static final long serialVersionUID = 1L; + + protected class Listener + extends WindowAdapter + implements ComponentListener, HierarchyListener, AWTEventListener { + @Override + public void windowOpened(WindowEvent e) { + repaint(); + } + + @Override + public void componentHidden(ComponentEvent e) {} + + @Override + public void componentMoved(ComponentEvent e) {} + + @Override + public void componentResized(ComponentEvent e) { + setSize(getParent().getSize()); + repaint(); + } + + @Override + public void componentShown(ComponentEvent e) { + repaint(); + } + + @Override + public void hierarchyChanged(HierarchyEvent e) { + repaint(); + } + + @Override + public void eventDispatched(AWTEvent e) { + if (e instanceof MouseEvent) { + Component src = ((MouseEvent)e).getComponent(); + if (src != null + && SwingUtilities.isDescendingFrom(src, content)) { + MouseEvent me = SwingUtilities.convertMouseEvent(src, (MouseEvent)e, content); + Component c = SwingUtilities.getDeepestComponentAt(content, me.getX(), me.getY()); + if (c != null) { + setCursor(c.getCursor()); + } + } + } + } + } + + private final Listener listener = createListener(); + private final JComponent content; + + public RepaintTrigger(JComponent content) { + this.content = content; + } + + @Override + public void addNotify() { + super.addNotify(); + Window w = SwingUtilities.getWindowAncestor(this); + setSize(getParent().getSize()); + w.addComponentListener(listener); + w.addWindowListener(listener); + Toolkit.getDefaultToolkit().addAWTEventListener(listener, AWTEvent.MOUSE_EVENT_MASK|AWTEvent.MOUSE_MOTION_EVENT_MASK); + } + + @Override + public void removeNotify() { + Toolkit.getDefaultToolkit().removeAWTEventListener(listener); + Window w = SwingUtilities.getWindowAncestor(this); + w.removeComponentListener(listener); + w.removeWindowListener(listener); + super.removeNotify(); + } + + private Rectangle dirty; + @Override + protected void paintComponent(Graphics g) { + Rectangle bounds = g.getClipBounds(); + if (dirty == null || !dirty.contains(bounds)) { + if (dirty == null) { + dirty = bounds; + } + else { + dirty = dirty.union(bounds); + } + content.repaint(dirty); + } + else { + dirty = null; + } + } + + protected Listener createListener() { + return new Listener(); + } + }; + + /** Window utilities with differing native implementations. */ + public static abstract class NativeWindowUtils { + protected void setMask(Component w, Raster raster, int whichShape) { + throw new UnsupportedOperationException("Not supported"); + }; + + protected abstract class TransparentContentPane + extends JPanel implements AWTEventListener { + private static final long serialVersionUID = 1L; + private boolean transparent; + public TransparentContentPane(Container oldContent) { + super(new BorderLayout()); + add(oldContent, BorderLayout.CENTER); + setTransparent(true); + if (oldContent instanceof JPanel) { + ((JComponent)oldContent).setOpaque(false); + } + } + @Override + public void addNotify() { + super.addNotify(); + Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.CONTAINER_EVENT_MASK); + } + @Override + public void removeNotify() { + Toolkit.getDefaultToolkit().removeAWTEventListener(this); + super.removeNotify(); + } + public void setTransparent(boolean transparent) { + this.transparent = transparent; + setOpaque(!transparent); + setDoubleBuffered(!transparent); + repaint(); + } + @Override + public void eventDispatched(AWTEvent e) { + if (e.getID() == ContainerEvent.COMPONENT_ADDED + && SwingUtilities.isDescendingFrom(((ContainerEvent)e).getChild(), this)) { + Component child = ((ContainerEvent)e).getChild(); + NativeWindowUtils.this.setDoubleBuffered(child, false); + } + } + @Override + public void paint(Graphics gr) { + if (transparent) { + Rectangle r = gr.getClipBounds(); + final int w = r.width; + final int h = r.height; + if (getWidth() > 0 && getHeight() > 0) { + final BufferedImage buf = + new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE); + + Graphics2D g = buf.createGraphics(); + g.setComposite(AlphaComposite.Clear); + g.fillRect(0, 0, w, h); + g.dispose(); + + g = buf.createGraphics(); + g.translate(-r.x, -r.y); + super.paint(g); + g.dispose(); + + paintDirect(buf, r); + } + } + else { + super.paint(gr); + } + } + /** Use the contents of the given BufferedImage to paint directly + * on this component's ancestor window. + */ + protected abstract void paintDirect(BufferedImage buf, Rectangle bounds); + } + + protected Window getWindow(Component c) { + return c instanceof Window + ? (Window)c : SwingUtilities.getWindowAncestor(c); + } + /** + * Execute the given action when the given window becomes + * displayable. + */ + protected void whenDisplayable(Component w, final Runnable action) { + if (w.isDisplayable() && (!Holder.requiresVisible || w.isVisible())) { + action.run(); + } + else if (Holder.requiresVisible) { + getWindow(w).addWindowListener(new WindowAdapter() { + @Override + public void windowOpened(WindowEvent e) { + e.getWindow().removeWindowListener(this); + action.run(); + } + @Override + public void windowClosed(WindowEvent e) { + e.getWindow().removeWindowListener(this); + } + }); + } + else { + // Hierarchy events are fired in direct response to + // displayability changes + w.addHierarchyListener(new HierarchyListener() { + @Override + public void hierarchyChanged(HierarchyEvent e) { + if ((e.getChangeFlags() & HierarchyEvent.DISPLAYABILITY_CHANGED) != 0 + && e.getComponent().isDisplayable()) { + e.getComponent().removeHierarchyListener(this); + action.run(); + } + } + }); + } + } + + protected Raster toRaster(Shape mask) { + Raster raster = null; + if (mask != MASK_NONE) { + Rectangle bounds = mask.getBounds(); + if (bounds.width > 0 && bounds.height > 0) { + BufferedImage clip = + new BufferedImage(bounds.x + bounds.width, + bounds.y + bounds.height, + BufferedImage.TYPE_BYTE_BINARY); + Graphics2D g = clip.createGraphics(); + g.setColor(Color.black); + g.fillRect(0, 0, bounds.x + bounds.width, bounds.y + bounds.height); + g.setColor(Color.white); + g.fill(mask); + raster = clip.getRaster(); + } + } + return raster; + } + + protected Raster toRaster(Component c, Icon mask) { + Raster raster = null; + if (mask != null) { + Rectangle bounds = new Rectangle(0, 0, mask.getIconWidth(), + mask.getIconHeight()); + BufferedImage clip = new BufferedImage(bounds.width, + bounds.height, + BufferedImage.TYPE_INT_ARGB); + Graphics2D g = clip.createGraphics(); + g.setComposite(AlphaComposite.Clear); + g.fillRect(0, 0, bounds.width, bounds.height); + g.setComposite(AlphaComposite.SrcOver); + mask.paintIcon(c, g, 0, 0); + raster = clip.getAlphaRaster(); + } + return raster; + } + + protected Shape toShape(Raster raster) { + final Area area = new Area(new Rectangle(0, 0, 0, 0)); + RasterRangesUtils.outputOccupiedRanges(raster, new RasterRangesUtils.RangesOutput() { + @Override + public boolean outputRange(int x, int y, int w, int h) { + area.add(new Area(new Rectangle(x, y, w, h))); + return true; + } + }); + return area; + } + + /** + * Set the overall alpha transparency of the window. An alpha of + * 1.0 is fully opaque, 0.0 is fully transparent. + */ + public void setWindowAlpha(Window w, float alpha) { + // do nothing + } + + /** Default: no support. */ + public boolean isWindowAlphaSupported() { + return false; + } + + /** Return the default graphics configuration. */ + public GraphicsConfiguration getAlphaCompatibleGraphicsConfiguration() { + GraphicsEnvironment env = GraphicsEnvironment + .getLocalGraphicsEnvironment(); + GraphicsDevice dev = env.getDefaultScreenDevice(); + return dev.getDefaultConfiguration(); + } + + /** + * Set the window to be transparent. Only explicitly painted + * pixels will be non-transparent. All pixels will be composited + * with whatever is under the window using their alpha values. + */ + public void setWindowTransparent(Window w, boolean transparent) { + // do nothing + } + + protected void setDoubleBuffered(Component root, boolean buffered) { + if (root instanceof JComponent) { + ((JComponent)root).setDoubleBuffered(buffered); + } + if (root instanceof JRootPane && buffered) { + ((JRootPane)root).setDoubleBuffered(true); + } + else if (root instanceof Container) { + Component[] kids = ((Container)root).getComponents(); + for (int i=0;i < kids.length;i++) { + setDoubleBuffered(kids[i], buffered); + } + } + } + + protected void setLayersTransparent(Window w, boolean transparent) { + + Color bg = transparent ? new Color(0, 0, 0, 0) : null; + if (w instanceof RootPaneContainer) { + RootPaneContainer rpc = (RootPaneContainer)w; + JRootPane root = rpc.getRootPane(); + JLayeredPane lp = root.getLayeredPane(); + Container c = root.getContentPane(); + JComponent content = + (c instanceof JComponent) ? (JComponent)c : null; + if (transparent) { + lp.putClientProperty(TRANSPARENT_OLD_OPAQUE, Boolean.valueOf(lp.isOpaque())); + lp.setOpaque(false); + root.putClientProperty(TRANSPARENT_OLD_OPAQUE, Boolean.valueOf(root.isOpaque())); + root.setOpaque(false); + if (content != null) { + content.putClientProperty(TRANSPARENT_OLD_OPAQUE, Boolean.valueOf(content.isOpaque())); + content.setOpaque(false); + } + root.putClientProperty(TRANSPARENT_OLD_BG, + root.getParent().getBackground()); + } + else { + lp.setOpaque(Boolean.TRUE.equals(lp.getClientProperty(TRANSPARENT_OLD_OPAQUE))); + lp.putClientProperty(TRANSPARENT_OLD_OPAQUE, null); + root.setOpaque(Boolean.TRUE.equals(root.getClientProperty(TRANSPARENT_OLD_OPAQUE))); + root.putClientProperty(TRANSPARENT_OLD_OPAQUE, null); + if (content != null) { + content.setOpaque(Boolean.TRUE.equals(content.getClientProperty(TRANSPARENT_OLD_OPAQUE))); + content.putClientProperty(TRANSPARENT_OLD_OPAQUE, null); + } + bg = (Color)root.getClientProperty(TRANSPARENT_OLD_BG); + root.putClientProperty(TRANSPARENT_OLD_BG, null); + } + } + w.setBackground(bg); + } + + /** Override this method to provide bitmap masking of the given + * heavyweight component. + */ + protected void setMask(Component c, Raster raster) { + throw new UnsupportedOperationException("Window masking is not available"); + } + + /** + * Set the window mask based on the given Raster, which should + * be treated as a bitmap (zero/nonzero values only). A value of + * null means to remove the mask. + */ + protected void setWindowMask(Component w, Raster raster, int which) { + if (w.isLightweight()) + throw new IllegalArgumentException("Component must be heavyweight: " + w); + setMask(w, raster, which); + } + + /** Set the window mask based on a {@link Shape}. */ + public void setWindowMask(Component w, Shape mask, int which) { + setWindowMask(w, toRaster(mask), which); + } + + /** + * Set the window mask based on an Icon. All non-transparent + * pixels will be included in the mask. + */ + public void setWindowMask(Component w, Icon mask) { + setWindowMask(w, toRaster(w, mask), Xext.ShapeBounding); + } + + /** + * Use this method to ensure heavyweight popups are used in + * conjunction with a given window. This prevents the window's + * alpha setting or mask region from being applied to the popup. + */ + protected void setForceHeavyweightPopups(Window w, boolean force) { + if (!(w instanceof HeavyweightForcer)) { + Window[] owned = w.getOwnedWindows(); + for (int i = 0; i < owned.length; i++) { + if (owned[i] instanceof HeavyweightForcer) { + if (force) + return; + owned[i].dispose(); + } + } + Boolean b = Boolean.valueOf(System.getProperty("jna.force_hw_popups", "true")); + if (force && b.booleanValue()) { + new HeavyweightForcer(w); + } + } + } + + /** + * Obtains the set icon for the window associated with the specified + * window handle. + * + * @param hwnd + * The concerning window handle. + * @return Either the window's icon or {@code null} if an error + * occurred. + * + * @throws UnsupportedOperationException + * Thrown if this method wasn't yet implemented for the + * current platform. + */ + protected BufferedImage getWindowIcon(final HWND hwnd) { + throw new UnsupportedOperationException("This platform is not supported, yet."); + } + + /** + * Detects the size of an icon. + * + * @param hIcon + * The icon handle type. + * @return Either the requested icon's dimension or an {@link Dimension} + * instance of {@code (0, 0)}. + * + * @throws UnsupportedOperationException + * Thrown if this method wasn't yet implemented for the + * current platform. + */ + protected Dimension getIconSize(final HICON hIcon) { + throw new UnsupportedOperationException("This platform is not supported, yet."); + } + + /** + * Requests a list of all currently available Desktop windows. + * + * @param onlyVisibleWindows + * Specifies whether only currently visible windows will be + * considered ({@code true}). That are windows which are not + * minimized. The {@code WS_VISIBLE} flag will be checked + * (see: User32.IsWindowVisible(HWND)). + * + * @return A list with all windows and some detailed information. + * + * @throws UnsupportedOperationException + * Thrown if this method wasn't yet implemented for the + * current platform. + */ + protected List getAllWindows(final boolean onlyVisibleWindows) { + throw new UnsupportedOperationException("This platform is not supported, yet."); + } + + /** + * Tries to obtain the Window's title which belongs to the specified + * window handle. + * + * @param hwnd + * The concerning window handle. + * @return Either the title or an empty string of no title was found or + * an error occurred. + * + * @throws UnsupportedOperationException + * Thrown if this method wasn't yet implemented for the + */ + protected String getWindowTitle(final HWND hwnd) { + throw new UnsupportedOperationException("This platform is not supported, yet."); + } + + /** + * Detects the full file path of the process associated with the specified + * window handle. + * + * @param hwnd + * The concerning window handle for which the PE file path is + * required. + * @return The full file path of the PE file that is associated with the + * specified window handle. + * + * @throws UnsupportedOperationException + * Thrown if this method wasn't yet implemented for the + */ + protected String getProcessFilePath(final HWND hwnd){ + throw new UnsupportedOperationException("This platform is not supported, yet."); + } + + /** + * Requests the location and size of the window associated with the + * specified window handle. + * + * @param hwnd + * The concerning window handle. + * @return The location and size of the window. + * + * @throws UnsupportedOperationException + * Thrown if this method wasn't yet implemented for the + */ + protected Rectangle getWindowLocationAndSize(final HWND hwnd) { + throw new UnsupportedOperationException("This platform is not supported, yet."); + } + } + /** Canonical lazy loading of a singleton. */ + private static class Holder { + /** + * Indicates whether a window must be visible before its native + * handle can be obtained. This wart is caused by the Java + * 1.4/X11 implementation. + */ + public static boolean requiresVisible; + public static final NativeWindowUtils INSTANCE; + static { + if (Platform.isWindows()) { + INSTANCE = new W32WindowUtils(); + } + else if (Platform.isMac()) { + INSTANCE = new MacWindowUtils(); + } + else if (Platform.isX11()) { + INSTANCE = new X11WindowUtils(); + requiresVisible = System.getProperty("java.version") + .matches("^1\\.4\\..*"); + } + else { + String os = System.getProperty("os.name"); + throw new UnsupportedOperationException("No support for " + os); + } + } + } + + private static NativeWindowUtils getInstance() { + return Holder.INSTANCE; + } + + private static class W32WindowUtils extends NativeWindowUtils { + private HWND getHWnd(Component w) { + HWND hwnd = new HWND(); + hwnd.setPointer(Native.getComponentPointer(w)); + return hwnd; + } + + /** + * W32 alpha will only work if sun.java2d.noddraw + * is set + */ + @Override + public boolean isWindowAlphaSupported() { + return Boolean.getBoolean("sun.java2d.noddraw"); + } + + /** Indicates whether UpdateLayeredWindow is in use. */ + private boolean usingUpdateLayeredWindow(Window w) { + if (w instanceof RootPaneContainer) { + JRootPane root = ((RootPaneContainer)w).getRootPane(); + return root.getClientProperty(TRANSPARENT_OLD_BG) != null; + } + return false; + } + + /** Keep track of the alpha level, since we can't read it from + * the window itself. + */ + private void storeAlpha(Window w, byte alpha) { + if (w instanceof RootPaneContainer) { + JRootPane root = ((RootPaneContainer)w).getRootPane(); + Byte b = alpha == (byte)0xFF ? null : Byte.valueOf(alpha); + root.putClientProperty(TRANSPARENT_ALPHA, b); + } + } + + /** Return the last alpha level we set on the window. */ + private byte getAlpha(Window w) { + if (w instanceof RootPaneContainer) { + JRootPane root = ((RootPaneContainer)w).getRootPane(); + Byte b = (Byte)root.getClientProperty(TRANSPARENT_ALPHA); + if (b != null) { + return b.byteValue(); + } + } + return (byte)0xFF; + } + + @Override + public void setWindowAlpha(final Window w, final float alpha) { + if (!isWindowAlphaSupported()) { + throw new UnsupportedOperationException("Set sun.java2d.noddraw=true to enable transparent windows"); + } + whenDisplayable(w, new Runnable() { + @Override + public void run() { + HWND hWnd = getHWnd(w); + User32 user = User32.INSTANCE; + int flags = user.GetWindowLong(hWnd, WinUser.GWL_EXSTYLE); + byte level = (byte)((int)(255 * alpha) & 0xFF); + if (usingUpdateLayeredWindow(w)) { + // If already using UpdateLayeredWindow, continue to + // do so + BLENDFUNCTION blend = new BLENDFUNCTION(); + blend.SourceConstantAlpha = level; + blend.AlphaFormat = WinUser.AC_SRC_ALPHA; + user.UpdateLayeredWindow(hWnd, null, null, null, null, + null, 0, blend, + WinUser.ULW_ALPHA); + } + else if (alpha == 1f) { + flags &= ~WinUser.WS_EX_LAYERED; + user.SetWindowLong(hWnd, WinUser.GWL_EXSTYLE, flags); + } + else { + flags |= WinUser.WS_EX_LAYERED; + user.SetWindowLong(hWnd, WinUser.GWL_EXSTYLE, flags); + user.SetLayeredWindowAttributes(hWnd, 0, level, + WinUser.LWA_ALPHA); + } + setForceHeavyweightPopups(w, alpha != 1f); + storeAlpha(w, level); + } + }); + } + + /** W32 makes the client responsible for repainting the entire + * window on any change. It also does not paint window decorations + * when the window is transparent. + */ + private class W32TransparentContentPane extends TransparentContentPane { + private static final long serialVersionUID = 1L; + private HDC memDC; + private HBITMAP hBitmap; + private Pointer pbits; + private Dimension bitmapSize; + public W32TransparentContentPane(Container content) { + super(content); + } + private void disposeBackingStore() { + GDI32 gdi = GDI32.INSTANCE; + if (hBitmap != null) { + gdi.DeleteObject(hBitmap); + hBitmap = null; + } + if (memDC != null) { + gdi.DeleteDC(memDC); + memDC = null; + } + } + @Override + public void removeNotify() { + super.removeNotify(); + disposeBackingStore(); + } + @Override + public void setTransparent(boolean transparent) { + super.setTransparent(transparent); + if (!transparent) { + disposeBackingStore(); + } + } + @Override + protected void paintDirect(BufferedImage buf, Rectangle bounds) { + // TODO: paint frame decoration if window is decorated + Window win = SwingUtilities.getWindowAncestor(this); + GDI32 gdi = GDI32.INSTANCE; + User32 user = User32.INSTANCE; + int x = bounds.x; + int y = bounds.y; + Point origin = SwingUtilities.convertPoint(this, x, y, win); + int w = bounds.width; + int h = bounds.height; + int ww = win.getWidth(); + int wh = win.getHeight(); + HDC screenDC = user.GetDC(null); + HANDLE oldBitmap = null; + try { + if (memDC == null) { + memDC = gdi.CreateCompatibleDC(screenDC); + } + if (hBitmap == null || !win.getSize().equals(bitmapSize)) { + if (hBitmap != null) { + gdi.DeleteObject(hBitmap); + hBitmap = null; + } + BITMAPINFO bmi = new BITMAPINFO(); + bmi.bmiHeader.biWidth = ww; + bmi.bmiHeader.biHeight = wh; + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; + bmi.bmiHeader.biCompression = WinGDI.BI_RGB; + bmi.bmiHeader.biSizeImage = ww * wh * 4; + PointerByReference ppbits = new PointerByReference(); + hBitmap = gdi.CreateDIBSection(memDC, bmi, + WinGDI.DIB_RGB_COLORS, + ppbits, null, 0); + pbits = ppbits.getValue(); + bitmapSize = new Dimension(ww, wh); + } + oldBitmap = gdi.SelectObject(memDC, hBitmap); + Raster raster = buf.getData(); + int[] pixel = new int[4]; + int[] bits = new int[w]; + for (int row = 0; row < h; row++) { + for (int col = 0; col < w; col++) { + raster.getPixel(col, row, pixel); + int alpha = (pixel[3] & 0xFF) << 24; + int red = (pixel[2] & 0xFF); + int green = (pixel[1] & 0xFF) << 8; + int blue = (pixel[0] & 0xFF) << 16; + bits[col] = alpha | red | green | blue; + } + int v = wh - (origin.y + row) - 1; + pbits.write((v*ww+origin.x)*4, bits, 0, bits.length); + } + SIZE winSize = new SIZE(); + winSize.cx = win.getWidth(); + winSize.cy = win.getHeight(); + POINT winLoc = new POINT(); + winLoc.x = win.getX(); + winLoc.y = win.getY(); + POINT srcLoc = new POINT(); + BLENDFUNCTION blend = new BLENDFUNCTION(); + HWND hWnd = getHWnd(win); + // extract current constant alpha setting, if possible + ByteByReference bref = new ByteByReference(); + IntByReference iref = new IntByReference(); + byte level = getAlpha(win); + try { + // GetLayeredwindowAttributes supported WinXP and later + if (user.GetLayeredWindowAttributes(hWnd, null, bref, iref) + && (iref.getValue() & WinUser.LWA_ALPHA) != 0) { + level = bref.getValue(); + } + } + catch(UnsatisfiedLinkError e) { + } + blend.SourceConstantAlpha = level; + blend.AlphaFormat = WinUser.AC_SRC_ALPHA; + user.UpdateLayeredWindow(hWnd, screenDC, winLoc, winSize, memDC, + srcLoc, 0, blend, WinUser.ULW_ALPHA); + } finally { + user.ReleaseDC(null, screenDC); + if (memDC != null && oldBitmap != null) { + gdi.SelectObject(memDC, oldBitmap); + } + } + } + } + + /** Note that w32 does not paint window decorations when + * the window is transparent. + */ + @Override + public void setWindowTransparent(final Window w, + final boolean transparent) { + if (!(w instanceof RootPaneContainer)) { + throw new IllegalArgumentException("Window must be a RootPaneContainer"); + } + if (!isWindowAlphaSupported()) { + throw new UnsupportedOperationException("Set sun.java2d.noddraw=true to enable transparent windows"); + } + boolean isTransparent = w.getBackground() != null + && w.getBackground().getAlpha() == 0; + if (transparent == isTransparent) + return; + whenDisplayable(w, new Runnable() { + @Override + public void run() { + User32 user = User32.INSTANCE; + HWND hWnd = getHWnd(w); + int flags = user.GetWindowLong(hWnd, WinUser.GWL_EXSTYLE); + JRootPane root = ((RootPaneContainer)w).getRootPane(); + JLayeredPane lp = root.getLayeredPane(); + Container content = root.getContentPane(); + if (content instanceof W32TransparentContentPane) { + ((W32TransparentContentPane)content).setTransparent(transparent); + } + else if (transparent) { + W32TransparentContentPane w32content = + new W32TransparentContentPane(content); + root.setContentPane(w32content); + lp.add(new RepaintTrigger(w32content), + JLayeredPane.DRAG_LAYER); + } + if (transparent && !usingUpdateLayeredWindow(w)) { + flags |= WinUser.WS_EX_LAYERED; + user.SetWindowLong(hWnd, WinUser.GWL_EXSTYLE, flags); + } + else if (!transparent && usingUpdateLayeredWindow(w)) { + flags &= ~WinUser.WS_EX_LAYERED; + user.SetWindowLong(hWnd, WinUser.GWL_EXSTYLE, flags); + } + setLayersTransparent(w, transparent); + setForceHeavyweightPopups(w, transparent); + setDoubleBuffered(w, !transparent); + } + }); + } + + @Override + public void setWindowMask(final Component w, final Shape mask, int which) { + if (mask instanceof Area && ((Area)mask).isPolygonal()) { + setMask(w, (Area)mask); + } + else { + super.setWindowMask(w, mask, which); + } + } + + // NOTE: Deletes hrgn after setting the window region + private void setWindowRegion(final Component w, final HRGN hrgn) { + whenDisplayable(w, new Runnable() { + @Override + public void run() { + GDI32 gdi = GDI32.INSTANCE; + User32 user = User32.INSTANCE; + HWND hWnd = getHWnd(w); + try { + user.SetWindowRgn(hWnd, hrgn, true); + setForceHeavyweightPopups(getWindow(w), hrgn != null); + } + finally { + gdi.DeleteObject(hrgn); + } + } + }); + } + + // Take advantage of CreatePolyPolygonalRgn on w32 + private void setMask(final Component w, final Area area) { + GDI32 gdi = GDI32.INSTANCE; + PathIterator pi = area.getPathIterator(null); + int mode = pi.getWindingRule() == PathIterator.WIND_NON_ZERO + ? WinGDI.WINDING: WinGDI.ALTERNATE; + float[] coords = new float[6]; + List points = new ArrayList(); + int size = 0; + List sizes = new ArrayList(); + while (!pi.isDone()) { + int type = pi.currentSegment(coords); + if (type == PathIterator.SEG_MOVETO) { + size = 1; + points.add(new POINT((int)coords[0], (int)coords[1])); + } + else if (type == PathIterator.SEG_LINETO) { + ++size; + points.add(new POINT((int)coords[0], (int)coords[1])); + } + else if (type == PathIterator.SEG_CLOSE) { + sizes.add(Integer.valueOf(size)); + } + else { + throw new RuntimeException("Area is not polygonal: " + area); + } + pi.next(); + } + POINT[] lppt = (POINT[])new POINT().toArray(points.size()); + POINT[] pts = points.toArray(new POINT[points.size()]); + for (int i=0;i < lppt.length;i++) { + lppt[i].x = pts[i].x; + lppt[i].y = pts[i].y; + } + int[] counts = new int[sizes.size()]; + for (int i=0;i < counts.length;i++) { + counts[i] = sizes.get(i).intValue(); + } + HRGN hrgn = gdi.CreatePolyPolygonRgn(lppt, counts, counts.length, mode); + setWindowRegion(w, hrgn); + } + + @Override + protected void setMask(final Component w, final Raster raster) { + GDI32 gdi = GDI32.INSTANCE; + final HRGN region = raster != null + ? gdi.CreateRectRgn(0, 0, 0, 0) : null; + if (region != null) { + final HRGN tempRgn = gdi.CreateRectRgn(0, 0, 0, 0); + try { + RasterRangesUtils.outputOccupiedRanges(raster, new RasterRangesUtils.RangesOutput() { + @Override + public boolean outputRange(int x, int y, int w, int h) { + GDI32 gdi = GDI32.INSTANCE; + gdi.SetRectRgn(tempRgn, x, y, x + w, y + h); + return gdi.CombineRgn(region, region, tempRgn, WinGDI.RGN_OR) != WinGDI.ERROR; + } + }); + } + finally { + gdi.DeleteObject(tempRgn); + } + } + setWindowRegion(w, region); + } + + @Override + public BufferedImage getWindowIcon(final HWND hwnd) { + // request different kind of icons if any solution fails + final DWORDByReference hIconNumber = new DWORDByReference(); + LRESULT result = User32.INSTANCE + .SendMessageTimeout(hwnd, + WinUser.WM_GETICON, + new WPARAM(WinUser.ICON_BIG), + new LPARAM(0), + WinUser.SMTO_ABORTIFHUNG, 500, hIconNumber); + if (result.intValue() == 0) + result = User32.INSTANCE + .SendMessageTimeout(hwnd, + WinUser.WM_GETICON, + new WPARAM(WinUser.ICON_SMALL), + new LPARAM(0), + WinUser.SMTO_ABORTIFHUNG, 500, hIconNumber); + if (result.intValue() == 0) + result = User32.INSTANCE + .SendMessageTimeout(hwnd, + WinUser.WM_GETICON, + new WPARAM(WinUser.ICON_SMALL2), + new LPARAM(0), + WinUser.SMTO_ABORTIFHUNG, 500, hIconNumber); + if (result.intValue() == 0) { + result = new LRESULT(User32.INSTANCE + .GetClassLongPtr(hwnd, + WinUser.GCLP_HICON).intValue()); + hIconNumber.getValue().setValue(result.intValue()); + } + if (result.intValue() == 0) { + result = new LRESULT(User32.INSTANCE + .GetClassLongPtr(hwnd, + WinUser.GCLP_HICONSM).intValue()); + hIconNumber.getValue().setValue(result.intValue()); + } + if (result.intValue() == 0) + return null; + + // draw native icon into Java image + final HICON hIcon = new HICON(new Pointer(hIconNumber.getValue() + .longValue())); + final Dimension iconSize = getIconSize(hIcon); + if (iconSize.width == 0 || iconSize.height == 0) + return null; + + final int width = iconSize.width; + final int height = iconSize.height; + final short depth = 24; + + final byte[] lpBitsColor = new byte[width * height * depth / 8]; + final Pointer lpBitsColorPtr = new Memory(lpBitsColor.length); + final byte[] lpBitsMask = new byte[width * height * depth / 8]; + final Pointer lpBitsMaskPtr = new Memory(lpBitsMask.length); + final BITMAPINFO bitmapInfo = new BITMAPINFO(); + final BITMAPINFOHEADER hdr = new BITMAPINFOHEADER(); + + bitmapInfo.bmiHeader = hdr; + hdr.biWidth = width; + hdr.biHeight = height; + hdr.biPlanes = 1; + hdr.biBitCount = depth; + hdr.biCompression = 0; + hdr.write(); + bitmapInfo.write(); + + final HDC hDC = User32.INSTANCE.GetDC(null); + final ICONINFO iconInfo = new ICONINFO(); + User32.INSTANCE.GetIconInfo(hIcon, iconInfo); + iconInfo.read(); + GDI32.INSTANCE.GetDIBits(hDC, iconInfo.hbmColor, 0, height, + lpBitsColorPtr, bitmapInfo, 0); + lpBitsColorPtr.read(0, lpBitsColor, 0, lpBitsColor.length); + GDI32.INSTANCE.GetDIBits(hDC, iconInfo.hbmMask, 0, height, + lpBitsMaskPtr, bitmapInfo, 0); + lpBitsMaskPtr.read(0, lpBitsMask, 0, lpBitsMask.length); + final BufferedImage image = new BufferedImage(width, height, + BufferedImage.TYPE_INT_ARGB); + + int r, g, b, a, argb; + int x = 0, y = height - 1; + for (int i = 0; i < lpBitsColor.length; i = i + 3) { + b = lpBitsColor[i] & 0xFF; + g = lpBitsColor[i + 1] & 0xFF; + r = lpBitsColor[i + 2] & 0xFF; + a = 0xFF - lpBitsMask[i] & 0xFF; + argb = (a << 24) | (r << 16) | (g << 8) | b; + image.setRGB(x, y, argb); + x = (x + 1) % width; + if (x == 0) + y--; + } + + User32.INSTANCE.ReleaseDC(null, hDC); + + return image; + } + + @Override + public Dimension getIconSize(final HICON hIcon) { + final ICONINFO iconInfo = new ICONINFO(); + try { + if (!User32.INSTANCE.GetIconInfo(hIcon, iconInfo)) + return new Dimension(); + iconInfo.read(); + + final BITMAP bmp = new BITMAP(); + if (iconInfo.hbmColor != null + && iconInfo.hbmColor.getPointer() != Pointer.NULL) { + final int nWrittenBytes = GDI32.INSTANCE.GetObject( + iconInfo.hbmColor, bmp.size(), bmp.getPointer()); + bmp.read(); + if (nWrittenBytes > 0) + return new Dimension(bmp.bmWidth.intValue(), + bmp.bmHeight.intValue()); + } else if (iconInfo.hbmMask != null + && iconInfo.hbmMask.getPointer() != Pointer.NULL) { + final int nWrittenBytes = GDI32.INSTANCE.GetObject( + iconInfo.hbmMask, bmp.size(), bmp.getPointer()); + bmp.read(); + if (nWrittenBytes > 0) + return new Dimension(bmp.bmWidth.intValue(), bmp.bmHeight.intValue() / 2); + } + } finally { + if (iconInfo.hbmColor != null + && iconInfo.hbmColor.getPointer() != Pointer.NULL) + GDI32.INSTANCE.DeleteObject(iconInfo.hbmColor); + if (iconInfo.hbmMask != null + && iconInfo.hbmMask.getPointer() != Pointer.NULL) + GDI32.INSTANCE.DeleteObject(iconInfo.hbmMask); + } + + return new Dimension(); + } + + @Override + public List getAllWindows(final boolean onlyVisibleWindows) { + final List result = new LinkedList(); + + final WNDENUMPROC lpEnumFunc = new WNDENUMPROC() { + @Override + public boolean callback(final HWND hwnd, final Pointer arg1) { + try { + final boolean visible = !onlyVisibleWindows + || User32.INSTANCE.IsWindowVisible(hwnd); + if (visible) { + final String title = getWindowTitle(hwnd); + final String filePath = getProcessFilePath(hwnd); + final Rectangle locAndSize = getWindowLocationAndSize(hwnd); + result.add(new DesktopWindow(hwnd, title, filePath, + locAndSize)); + } + } catch (final Exception e) { + // FIXME properly handle whatever error is raised + e.printStackTrace(); + } + + return true; + } + }; + + if (!User32.INSTANCE.EnumWindows(lpEnumFunc, null)) + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + + return result; + } + + @Override + public String getWindowTitle(final HWND hwnd) { + final int requiredLength = User32.INSTANCE + .GetWindowTextLength(hwnd) + 1; + final char[] title = new char[requiredLength]; + final int length = User32.INSTANCE.GetWindowText(hwnd, title, + title.length); + + return Native.toString(Arrays.copyOfRange(title, 0, length)); + } + + @Override + public String getProcessFilePath(final HWND hwnd) { + final IntByReference pid = new IntByReference(); + User32.INSTANCE.GetWindowThreadProcessId(hwnd, pid); + + // GetProcessImageFileName requires PROCESS_QUERY_INFORMATION on + // older windows versions so try that first. If we fail to get + // access to that information fallback to + // PROCESS_QUERY_LIMITED_INFORMATION. This allows reading image + // paths from processes running with elevated privileges (at least + // worked successfully for a setup program started from a network + // share) + HANDLE process = Kernel32.INSTANCE.OpenProcess( + WinNT.PROCESS_QUERY_INFORMATION, + false, + pid.getValue()); + + if (process == null) { + if(Kernel32.INSTANCE.GetLastError() != WinNT.ERROR_ACCESS_DENIED) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } else { + process = Kernel32.INSTANCE.OpenProcess( + WinNT.PROCESS_QUERY_LIMITED_INFORMATION, + false, + pid.getValue()); + + if (process == null) { + if (Kernel32.INSTANCE.GetLastError() != WinNT.ERROR_ACCESS_DENIED) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } else { + // Ignore windows, that can't be accessed + return ""; + } + } + } + } + + try { + String processImagePath = PsapiUtil.GetProcessImageFileName(process); + + // GetProcessImageFileName returns the file name as a device + // filename in the form \Device\Harddisk5\. To make this more + // familiar and keep compatibility with QueryModuleName try to + // map back to known path (DOS path or UNC path) + + // Map Mup to UNC path + if(processImagePath.startsWith("\\Device\\Mup\\")) { + return "\\" + processImagePath.substring(11); + } + + // Format of FindFirstVolume is + // \\?\Volume{00000000-0000-0000-0000-000000000000}\ + char[] volumeUUID = new char[50]; + HANDLE h = Kernel32.INSTANCE.FindFirstVolume(volumeUUID, 50); + if (h == null || h.equals(WinBase.INVALID_HANDLE_VALUE)) { + throw new Win32Exception(Native.getLastError()); + } + try { + do { + String volumePath = Native.toString(volumeUUID); + for (String s : Kernel32Util.getVolumePathNamesForVolumeName(volumePath)) { + if (s.matches("[a-zA-Z]:\\\\")) { + for (String path : Kernel32Util.queryDosDevice(s.substring(0, 2), 1024)) { + if(processImagePath.startsWith(path)) { + return s + processImagePath.substring(path.length() + 1); + } + } + } + } + } while (Kernel32.INSTANCE.FindNextVolume(h, volumeUUID, 50)); + if (Native.getLastError() != WinError.ERROR_NO_MORE_FILES) { + throw new Win32Exception(Native.getLastError()); + } + } finally { + Kernel32.INSTANCE.FindVolumeClose(h); + } + return processImagePath; + } finally { + Kernel32.INSTANCE.CloseHandle(process); + } + } + + @Override + public Rectangle getWindowLocationAndSize(final HWND hwnd) { + final RECT lpRect = new RECT(); + if (!User32.INSTANCE.GetWindowRect(hwnd, lpRect)) + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + + return new Rectangle(lpRect.left, lpRect.top, Math.abs(lpRect.right + - lpRect.left), Math.abs(lpRect.bottom - lpRect.top)); + } + } + + private static class MacWindowUtils extends NativeWindowUtils { + @Override + public boolean isWindowAlphaSupported() { + return true; + } + + private OSXMaskingContentPane installMaskingPane(Window w) { + OSXMaskingContentPane content; + if (w instanceof RootPaneContainer) { + // TODO: replace layered pane instead? + final RootPaneContainer rpc = (RootPaneContainer)w; + Container oldContent = rpc.getContentPane(); + if (oldContent instanceof OSXMaskingContentPane) { + content = (OSXMaskingContentPane)oldContent; + } + else { + content = new OSXMaskingContentPane(oldContent); + // TODO: listen for content pane changes + rpc.setContentPane(content); + } + } + else { + Component oldContent = w.getComponentCount() > 0 ? w.getComponent(0) : null; + if (oldContent instanceof OSXMaskingContentPane) { + content = (OSXMaskingContentPane)oldContent; + } + else { + content = new OSXMaskingContentPane(oldContent); + w.add(content); + } + } + return content; + } + + /** Note that the property + * apple.awt.draggableWindowBackground must be set to its + * final value before the heavyweight peer for the Window is + * created. Once {@link Component#addNotify} has been called on the + * component, causing creation of the heavyweight peer, changing this + * property has no effect. + * @see Apple Technote 2007 + */ + @Override + public void setWindowTransparent(Window w, boolean transparent) { + boolean isTransparent = w.getBackground() != null + && w.getBackground().getAlpha() == 0; + if (transparent != isTransparent) { + setBackgroundTransparent(w, transparent, "setWindowTransparent"); + } + } + + /** Setting this false restores the original setting. */ + private static final String WDRAG = "apple.awt.draggableWindowBackground"; + private void fixWindowDragging(Window w, String context) { + if (w instanceof RootPaneContainer) { + JRootPane p = ((RootPaneContainer)w).getRootPane(); + Boolean oldDraggable = (Boolean)p.getClientProperty(WDRAG); + if (oldDraggable == null) { + p.putClientProperty(WDRAG, Boolean.FALSE); + if (w.isDisplayable()) { + LOG.log(Level.WARNING, "{0}(): To avoid content dragging, {1}() must be called before the window is realized, or " + WDRAG + " must be set to Boolean.FALSE before the window is realized. If you really want content dragging, set " + WDRAG + " on the window''s root pane to Boolean.TRUE before calling {2}() to hide this message.", + new Object[]{context, context, context}); + } + } + } + } + + /** Note that the property + * apple.awt.draggableWindowBackground must be set to its + * final value before the heavyweight peer for the Window is + * created. Once {@link Component#addNotify} has been called on the + * component, causing creation of the heavyweight peer, changing this + * property has no effect. + * @see Apple Technote 2007 + */ + @Override + public void setWindowAlpha(final Window w, final float alpha) { + if (w instanceof RootPaneContainer) { + JRootPane p = ((RootPaneContainer)w).getRootPane(); + p.putClientProperty("Window.alpha", Float.valueOf(alpha)); + fixWindowDragging(w, "setWindowAlpha"); + } + whenDisplayable(w, new Runnable() { + @Override + public void run() { + try { + // This will work with old Apple AWT implementations and + // not with openjdk + Method getPeer = w.getClass().getMethod("getPeer"); + Object peer = getPeer.invoke(w); + Method setAlpha = peer.getClass().getMethod("setAlpha", new Class[]{ float.class }); + setAlpha.invoke(peer, Float.valueOf(alpha)); + } + catch (Exception e) { + } + } + }); + } + + @Override + protected void setWindowMask(Component w, Raster raster, int which) { + if (raster != null) { + setWindowMask(w, toShape(raster), which); + } + else { + setWindowMask(w, new Rectangle(0, 0, w.getWidth(), + w.getHeight()), which); + } + } + + @Override + public void setWindowMask(Component c, final Shape shape, int which) { + if (c instanceof Window) { + Window w = (Window)c; + OSXMaskingContentPane content = installMaskingPane(w); + content.setMask(shape); + setBackgroundTransparent(w, shape != MASK_NONE, "setWindowMask"); + } + else { + // not yet implemented + } + } + + /** Mask out unwanted pixels and ensure background gets cleared. + * @author Olivier Chafik + */ + private static class OSXMaskingContentPane extends JPanel { + private static final long serialVersionUID = 1L; + private Shape shape; + + public OSXMaskingContentPane(Component oldContent) { + super(new BorderLayout()); + if (oldContent != null) { + add(oldContent, BorderLayout.CENTER); + } + } + + public void setMask(Shape shape) { + this.shape = shape; + repaint(); + } + + @Override + public void paint(Graphics graphics) { + Graphics2D g = (Graphics2D)graphics.create(); + g.setComposite(AlphaComposite.Clear); + g.fillRect(0, 0, getWidth(), getHeight()); + g.dispose(); + if (shape != null) { + g = (Graphics2D)graphics.create(); + g.setClip(shape); + super.paint(g); + g.dispose(); + } + else { + super.paint(graphics); + } + } + } + + private void setBackgroundTransparent(Window w, boolean transparent, String context) { + JRootPane rp = w instanceof RootPaneContainer + ? ((RootPaneContainer)w).getRootPane() : null; + if (transparent) { + if (rp != null) { + rp.putClientProperty(TRANSPARENT_OLD_BG, w.getBackground()); + } + w.setBackground(new Color(0,0,0,0)); + } + else { + if (rp != null) { + Color bg = (Color)rp.getClientProperty(TRANSPARENT_OLD_BG); + // If the old bg is a + // apple.laf.CColorPaintUIResource, the window's + // transparent state will not change + if (bg != null) { + bg = new Color(bg.getRed(), bg.getGreen(), bg.getBlue(), bg.getAlpha()); + } + w.setBackground(bg); + rp.putClientProperty(TRANSPARENT_OLD_BG, null); + } + else { + w.setBackground(null); + } + } + fixWindowDragging(w, context); + } + } + private static class X11WindowUtils extends NativeWindowUtils { + private static Pixmap createBitmap(final Display dpy, + X11.Window win, + Raster raster) { + final X11 x11 = X11.INSTANCE; + Rectangle bounds = raster.getBounds(); + int width = bounds.x + bounds.width; + int height = bounds.y + bounds.height; + final Pixmap pm = x11.XCreatePixmap(dpy, win, width, height, 1); + final GC gc = x11.XCreateGC(dpy, pm, new NativeLong(0), null); + if (gc == null) { + return null; + } + x11.XSetForeground(dpy, gc, new NativeLong(0)); + x11.XFillRectangle(dpy, pm, gc, 0, 0, width, height); + final List rlist = new ArrayList(); + try { + RasterRangesUtils.outputOccupiedRanges(raster, new RasterRangesUtils.RangesOutput() { + @Override + public boolean outputRange(int x, int y, int w, int h) { + rlist.add(new Rectangle(x, y, w, h)); + return true; + } + }); + X11.XRectangle[] rects = (X11.XRectangle[]) + new X11.XRectangle().toArray(rlist.size()); + for (int i=0;i < rects.length;i++) { + Rectangle r = rlist.get(i); + rects[i].x = (short)r.x; + rects[i].y = (short)r.y; + rects[i].width = (short)r.width; + rects[i].height = (short)r.height; + // Optimization: write directly to native memory + Pointer p = rects[i].getPointer(); + p.setShort(0, (short)r.x); + p.setShort(2, (short)r.y); + p.setShort(4, (short)r.width); + p.setShort(6, (short)r.height); + rects[i].setAutoSynch(false); + // End optimization + } + final int UNMASKED = 1; + x11.XSetForeground(dpy, gc, new NativeLong(UNMASKED)); + x11.XFillRectangles(dpy, pm, gc, rects, rects.length); + } + finally { + x11.XFreeGC(dpy, gc); + } + return pm; + } + + private boolean didCheck; + private long[] alphaVisualIDs = {}; + + @Override + public boolean isWindowAlphaSupported() { + return getAlphaVisualIDs().length > 0; + } + + private static long getVisualID(GraphicsConfiguration config) { + // Use reflection to call + // X11GraphicsConfig.getVisual + try { + Object o = config.getClass() + .getMethod("getVisual", (Class[])null) + .invoke(config, (Object[])null); + return ((Number)o).longValue(); + } + catch (Exception e) { + // FIXME properly handle this error + e.printStackTrace(); + return -1; + } + } + + /** Return the default graphics configuration. */ + @Override + public GraphicsConfiguration getAlphaCompatibleGraphicsConfiguration() { + if (isWindowAlphaSupported()) { + GraphicsEnvironment env = + GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice[] devices = env.getScreenDevices(); + for (int i = 0; i < devices.length; i++) { + GraphicsConfiguration[] configs = + devices[i].getConfigurations(); + for (int j = 0; j < configs.length; j++) { + long visualID = getVisualID(configs[j]); + long[] ids = getAlphaVisualIDs(); + for (int k = 0; k < ids.length; k++) { + if (visualID == ids[k]) { + return configs[j]; + } + } + } + } + } + return super.getAlphaCompatibleGraphicsConfiguration(); + } + + /** + * Return the visual ID of the visual which supports an alpha + * channel. + */ + private synchronized long[] getAlphaVisualIDs() { + if (didCheck) { + return alphaVisualIDs; + } + didCheck = true; + X11 x11 = X11.INSTANCE; + Display dpy = x11.XOpenDisplay(null); + if (dpy == null) + return alphaVisualIDs; + XVisualInfo info = null; + try { + int screen = x11.XDefaultScreen(dpy); + XVisualInfo template = new XVisualInfo(); + template.screen = screen; + template.depth = 32; + template.c_class = X11.TrueColor; + NativeLong mask = new NativeLong(X11.VisualScreenMask + | X11.VisualDepthMask + | X11.VisualClassMask); + IntByReference pcount = new IntByReference(); + info = x11.XGetVisualInfo(dpy, mask, template, pcount); + if (info != null) { + List list = new ArrayList(); + XVisualInfo[] infos = + (XVisualInfo[])info.toArray(pcount.getValue()); + for (int i = 0; i < infos.length; i++) { + XRenderPictFormat format = + X11.Xrender.INSTANCE.XRenderFindVisualFormat(dpy, + infos[i].visual); + if (format.type == X11.Xrender.PictTypeDirect + && format.direct.alphaMask != 0) { + list.add(infos[i].visualid); + } + } + alphaVisualIDs = new long[list.size()]; + for (int i=0;i < alphaVisualIDs.length;i++) { + alphaVisualIDs[i] = ((Number)list.get(i)).longValue(); + } + return alphaVisualIDs; + } + } + finally { + if (info != null) { + x11.XFree(info.getPointer()); + } + x11.XCloseDisplay(dpy); + } + return alphaVisualIDs; + } + + private static X11.Window getContentWindow(Window w, X11.Display dpy, + X11.Window win, Point offset) { + if ((w instanceof Frame && !((Frame)w).isUndecorated()) + || (w instanceof Dialog && !((Dialog)w).isUndecorated())) { + X11 x11 = X11.INSTANCE; + X11.WindowByReference rootp = new X11.WindowByReference(); + X11.WindowByReference parentp = new X11.WindowByReference(); + PointerByReference childrenp = new PointerByReference(); + IntByReference countp = new IntByReference(); + x11.XQueryTree(dpy, win, rootp, parentp, childrenp, countp); + Pointer p = childrenp.getValue(); + int[] ids = p.getIntArray(0, countp.getValue()); + for (int id : ids) { + // TODO: more verification of correct window? + X11.Window child = new X11.Window(id); + X11.XWindowAttributes xwa = new X11.XWindowAttributes(); + x11.XGetWindowAttributes(dpy, child, xwa); + offset.x = -xwa.x; + offset.y = -xwa.y; + win = child; + break; + } + if (p != null) { + x11.XFree(p); + } + } + return win; + } + + private static X11.Window getDrawable(Component w) { + int id = (int)Native.getComponentID(w); + if (id == X11.None) + return null; + return new X11.Window(id); + } + + private static final long OPAQUE = 0xFFFFFFFFL; + private static final String OPACITY = "_NET_WM_WINDOW_OPACITY"; + + @Override + public void setWindowAlpha(final Window w, final float alpha) { + if (!isWindowAlphaSupported()) { + throw new UnsupportedOperationException("This X11 display does not provide a 32-bit visual"); + } + Runnable action = new Runnable() { + @Override + public void run() { + X11 x11 = X11.INSTANCE; + Display dpy = x11.XOpenDisplay(null); + if (dpy == null) + return; + try { + X11.Window win = getDrawable(w); + if (alpha == 1f) { + x11.XDeleteProperty(dpy, win, + x11.XInternAtom(dpy, OPACITY, + false)); + } + else { + int opacity = (int)((long)(alpha * OPAQUE) & 0xFFFFFFFF); + IntByReference patom = new IntByReference(opacity); + x11.XChangeProperty(dpy, win, + x11.XInternAtom(dpy, OPACITY, + false), + X11.XA_CARDINAL, 32, + X11.PropModeReplace, + patom.getPointer(), 1); + } + } + finally { + x11.XCloseDisplay(dpy); + } + } + }; + whenDisplayable(w, action); + } + + private class X11TransparentContentPane extends TransparentContentPane { + private static final long serialVersionUID = 1L; + + public X11TransparentContentPane(Container oldContent) { + super(oldContent); + } + + private Memory buffer; + private int[] pixels; + private final int[] pixel = new int[4]; + // Painting directly to the original Graphics + // fails to properly composite unless the destination + // is pure black. Too bad. + @Override + protected void paintDirect(BufferedImage buf, Rectangle bounds) { + Window window = SwingUtilities.getWindowAncestor(this); + X11 x11 = X11.INSTANCE; + X11.Display dpy = x11.XOpenDisplay(null); + X11.Window win = getDrawable(window); + Point offset = new Point(); + win = getContentWindow(window, dpy, win, offset); + X11.GC gc = x11.XCreateGC(dpy, win, new NativeLong(0), null); + + Raster raster = buf.getData(); + int w = bounds.width; + int h = bounds.height; + if (buffer == null || buffer.size() != w*h*4) { + buffer = new Memory(w*h*4); + pixels = new int[w*h]; + } + for (int y=0;y + * NOTE: Windows requires that sun.java2d.noddraw=true + * in order for alpha to work.

+ * NOTE: On OSX, the property + * apple.awt.draggableWindowBackground must be set to its + * final value before the heavyweight peer for the Window is + * created. Once {@link Component#addNotify} has been called on the + * component, causing creation of the heavyweight peer, changing this + * property has no effect. + * @see Apple Technote 2007 + */ + public static void setWindowAlpha(Window w, float alpha) { + getInstance().setWindowAlpha(w, Math.max(0f, Math.min(alpha, 1f))); + } + + /** + * Set the window to be transparent. Only explicitly painted pixels + * will be non-transparent. All pixels will be composited with + * whatever is under the window using their alpha values. + * + * On OSX, the property apple.awt.draggableWindowBackground + * must be set to its final value before the heavyweight peer for + * the Window is created. Once {@link Component#addNotify} has been + * called on the component, causing creation of the heavyweight peer, + * changing this property has no effect. + * @see Apple Technote 2007 + */ + public static void setWindowTransparent(Window w, boolean transparent) { + getInstance().setWindowTransparent(w, transparent); + } + + /** + * Obtains the set icon for the window associated with the specified + * window handle. + * + * @param hwnd + * The concerning window handle. + * @return Either the window's icon or {@code null} if an error + * occurred. + */ + public static BufferedImage getWindowIcon(final HWND hwnd) { + return getInstance().getWindowIcon(hwnd); + } + + /** + * Detects the size of an icon. + * + * @param hIcon + * The icon handle type. + * @return Either the requested icon's dimension or an {@link Dimension} + * instance of {@code (0, 0)}. + */ + public static Dimension getIconSize(final HICON hIcon) { + return getInstance().getIconSize(hIcon); + } + + /** + * Requests a list of all currently available Desktop windows. + * + * @param onlyVisibleWindows + * Specifies whether only currently visible windows will be + * considered ({@code true}). That are windows which are not + * minimized. The {@code WS_VISIBLE} flag will be checked (see: + * User32.IsWindowVisible(HWND)). + * + * @return A list with all windows and some detailed information. + */ + public static List getAllWindows( + final boolean onlyVisibleWindows) { + return getInstance().getAllWindows(onlyVisibleWindows); + } + + /** + * Tries to obtain the Window's title which belongs to the specified window + * handle. + * + * @param hwnd + * The concerning window handle. + * @return Either the title or an empty string of no title was found or an + * error occurred. + */ + public static String getWindowTitle(final HWND hwnd) { + return getInstance().getWindowTitle(hwnd); + } + + /** + * Detects the full file path of the process associated with the specified + * window handle. + * + * @param hwnd + * The concerning window handle for which the PE file path is + * required. + * @return The full file path of the PE file that is associated with the + * specified window handle. + */ + public static String getProcessFilePath(final HWND hwnd) { + return getInstance().getProcessFilePath(hwnd); + } + + /** + * Requests the location and size of the window associated with the + * specified window handle. + * + * @param hwnd + * The concerning window handle. + * @return The location and size of the window. + */ + public static Rectangle getWindowLocationAndSize(final HWND hwnd) { + return getInstance().getWindowLocationAndSize(hwnd); + } +} diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/LibUtil.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/LibUtil.java new file mode 100644 index 000000000000..cad07c366236 --- /dev/null +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/LibUtil.java @@ -0,0 +1,96 @@ +package gg.xp.xivsupport.gui.overlay; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.ProviderNotFoundException; +import java.nio.file.StandardCopyOption; + +// from https://www.adamh.cz/blog/2012/12/how-to-load-native-jni-library-from-jar/ +public class LibUtil { + private static final int MIN_PREFIX_LENGTH = 3; + private static final String NATIVE_FOLDER_PATH_PREFIX = "nativeutils"; + private static File temporaryDir; + + public static void loadLibraryFromJar(String path) { + + try { + if (null == path || !path.startsWith("/")) { + throw new IllegalArgumentException("The path has to be absolute (start with '/')."); + } + + // Obtain filename from path + String[] parts = path.split("/"); + String filename = (parts.length > 1) ? parts[parts.length - 1] : null; + + // Check if the filename is okay + if (filename == null || filename.length() < MIN_PREFIX_LENGTH) { + throw new IllegalArgumentException("The filename has to be at least 3 characters long."); + } + + // Prepare temporary file + if (temporaryDir == null) { + temporaryDir = createTempDirectory(NATIVE_FOLDER_PATH_PREFIX); + temporaryDir.deleteOnExit(); + } + + File temp = new File(temporaryDir, filename); + + try (InputStream is = ScalableJFrameLinuxRealImpl.class.getResourceAsStream(path)) { + Files.copy(is, temp.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + catch (IOException e) { + temp.delete(); + throw e; + } + catch (NullPointerException e) { + temp.delete(); + throw new FileNotFoundException("File " + path + " was not found inside JAR."); + } + + try { + System.load(temp.getAbsolutePath()); + } + finally { + if (isPosixCompliant()) { + // Assume POSIX compliant file system, can be deleted after loading + temp.delete(); + } + else { + // Assume non-POSIX, and don't delete until last file descriptor closed + temp.deleteOnExit(); + } + } + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static boolean isPosixCompliant() { + try { + return FileSystems.getDefault() + .supportedFileAttributeViews() + .contains("posix"); + } + catch (FileSystemNotFoundException + | ProviderNotFoundException + | SecurityException e) { + return false; + } + } + + private static File createTempDirectory(String prefix) throws IOException { + String tempDir = System.getProperty("java.io.tmpdir"); + File generatedDir = new File(tempDir, prefix + System.nanoTime()); + + if (!generatedDir.mkdir()) + throw new IOException("Failed to create temp directory " + generatedDir.getName()); + + return generatedDir; + } +} diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/ScalableJFrame.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/ScalableJFrame.java index d721225c881e..9b834dd909ec 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/ScalableJFrame.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/ScalableJFrame.java @@ -12,4 +12,6 @@ public ScalableJFrame(String title) throws HeadlessException { @Override public abstract double getScaleFactor(); + + public abstract void setClickThrough(boolean clickThrough); } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/ScalableJFrameLinuxNoopImpl.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/ScalableJFrameLinuxNoopImpl.java index 6f040956311c..e174ebc6de18 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/ScalableJFrameLinuxNoopImpl.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/ScalableJFrameLinuxNoopImpl.java @@ -44,4 +44,9 @@ public void setScaleFactor(double scaleFactor) { public double getScaleFactor() { return 1.0; } + + @Override + public void setClickThrough(boolean clickThrough) { + + } } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/ScalableJFrameLinuxRealImpl.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/ScalableJFrameLinuxRealImpl.java index 9709b3d37eae..f4883482d628 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/ScalableJFrameLinuxRealImpl.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/ScalableJFrameLinuxRealImpl.java @@ -1,28 +1,44 @@ package gg.xp.xivsupport.gui.overlay; +import com.sun.jna.platform.unix.X11; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import javax.swing.*; import java.awt.*; import java.awt.geom.AffineTransform; import java.awt.image.BufferStrategy; -import static java.awt.RenderingHints.KEY_ANTIALIASING; import static java.awt.RenderingHints.KEY_RENDERING; -import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON; import static java.awt.RenderingHints.VALUE_RENDER_QUALITY; public final class ScalableJFrameLinuxRealImpl extends ScalableJFrame { - private final int numBuffers; + private static final Logger log = LoggerFactory.getLogger(ScalableJFrameLinuxRealImpl.class); + private static final boolean enableClickThrough; + + static { + boolean enable; + try { + LibUtil.loadLibraryFromJar("/libjawt.so"); + enable = true; + } + catch (Throwable t) { + log.error("Could not load libjawt.so - overlay click-through will not work."); + enable = false; + } + enableClickThrough = enable; + } + private double scaleFactor; - private ScalableJFrameLinuxRealImpl(String title, double scaleFactor, int numBuffers) throws HeadlessException { + private ScalableJFrameLinuxRealImpl(String title, double scaleFactor) throws HeadlessException { super(title); this.scaleFactor = scaleFactor; - this.numBuffers = numBuffers; } - public static ScalableJFrame construct(String title, double defaultScaleFactor, int numBuffers) { - return new ScalableJFrameLinuxRealImpl(title, defaultScaleFactor, numBuffers); + public static ScalableJFrame construct(String title, double defaultScaleFactor) { + return new ScalableJFrameLinuxRealImpl(title, defaultScaleFactor); } @Override @@ -40,9 +56,7 @@ public void paint(Graphics g) { Graphics2D g2d = ((Graphics2D) drawGraphics); AffineTransform t = g2d.getTransform(); t.scale(scaleFactor, scaleFactor); - g2d.transform(t); - g2d.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON); -// g2d.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BILINEAR); + g2d.setTransform(t); g2d.setRenderingHint(KEY_RENDERING, VALUE_RENDER_QUALITY); getContentPane().paint(drawGraphics); @@ -78,4 +92,36 @@ public void setScaleFactor(double scaleFactor) { public double getScaleFactor() { return scaleFactor; } + + /* + Investigation: Normal "Shape" API isn't the right thing since we still want the window to occupy + the entire area. + + What we specifically need seems to be XFixesSetWindowShapeRegion(), with a ShapeInput. + C++ example: https://gist.github.com/ericek111/774a1661be69387de846f5f5a5977a46#file-xoverlay-cpp-L64 + void allow_input_passthrough (Window w) { + XserverRegion region = XFixesCreateRegion (g_display, NULL, 0); + + //XFixesSetWindowShapeRegion (g_display, w, ShapeBounding, 0, 0, 0); + XFixesSetWindowShapeRegion (g_display, w, ShapeInput, 0, 0, region); + + XFixesDestroyRegion (g_display, region); +} + */ + + @Override + public void setClickThrough(boolean clickThrough) { + if (!enableClickThrough) { + return; + } + if (clickThrough) { + Shape shape = new Rectangle(1, 1); + EnhancedWindowUtils.setWindowMask(this, shape, X11.Xext.ShapeInput); + } + else { + EnhancedWindowUtils.setWindowMask(this, null, X11.Xext.ShapeInput); + } + } + + } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/ScalableJFrameWindowsImpl.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/ScalableJFrameWindowsImpl.java index 103f85f8ad58..5ce7d140071c 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/ScalableJFrameWindowsImpl.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/ScalableJFrameWindowsImpl.java @@ -1,10 +1,21 @@ package gg.xp.xivsupport.gui.overlay; +import com.sun.jna.Native; +import com.sun.jna.platform.win32.User32; +import com.sun.jna.platform.win32.WinDef; +import com.sun.jna.platform.win32.WinUser; +import gg.xp.xivsupport.persistence.Platform; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.*; import java.awt.*; import java.awt.geom.AffineTransform; public final class ScalableJFrameWindowsImpl extends ScalableJFrame { + private static final Logger log = LoggerFactory.getLogger(ScalableJFrameWindowsImpl.class); + private final int numBuffers; private double scaleFactor; @@ -73,4 +84,42 @@ public void setScaleFactor(double scaleFactor) { public double getScaleFactor() { return scaleFactor; } + + @Override + public void setClickThrough(boolean clickThrough) { + setClickThrough(this, clickThrough); + } + + // https://docs.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles + private static final long WS_NOACTIVATE = 0x08000000L; + + private static void setClickThrough(JFrame w, boolean clickThrough) { + log.trace("Click-through: {}", clickThrough); + w.setFocusableWindowState(!clickThrough); + if (!Platform.isWindows()) { + log.warn("Setting click-through is not supported on non-Windows platforms at this time."); + return; + } + WinDef.HWND hwnd = getHWnd(w); + int wl = User32.INSTANCE.GetWindowLong(hwnd, WinUser.GWL_EXSTYLE); + if (clickThrough) { + wl |= WinUser.WS_EX_TRANSPARENT; + wl |= WS_NOACTIVATE; + } + else { + wl &= ~WinUser.WS_EX_TRANSPARENT; + wl &= ~WS_NOACTIVATE; + } + w.setBackground(new Color(0, 0, 0, 0)); + User32.INSTANCE.SetWindowLong(hwnd, WinUser.GWL_EXSTYLE, wl); + } + + /** + * Get the window handle from the OS + */ + private static WinDef.HWND getHWnd(Component w) { + WinDef.HWND hwnd = new WinDef.HWND(); + hwnd.setPointer(Native.getComponentPointer(w)); + return hwnd; + } } diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/XivOverlay.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/XivOverlay.java index 6fb1d7f19ab7..68063476a266 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/XivOverlay.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/overlay/XivOverlay.java @@ -1,9 +1,5 @@ package gg.xp.xivsupport.gui.overlay; -import com.sun.jna.Native; -import com.sun.jna.platform.win32.User32; -import com.sun.jna.platform.win32.WinDef; -import com.sun.jna.platform.win32.WinUser; import gg.xp.xivsupport.persistence.PersistenceProvider; import gg.xp.xivsupport.persistence.Platform; import gg.xp.xivsupport.persistence.settings.BooleanSetting; @@ -78,7 +74,7 @@ public XivOverlay(String title, String settingKeyBase, OverlayConfig oc, Persist else { opacity = new DoubleSetting(persistence, String.format("xiv-overlay.window-pos.%s.opacity", settingKeyBase), 1.0d, 1.0, 1.0); opacity.reset(); - frame = ScalableJFrameLinuxRealImpl.construct(title, scaleFactor.get(), numBuffers); + frame = ScalableJFrameLinuxRealImpl.construct(title, scaleFactor.get()); } enabled = new BooleanSetting(persistence, String.format("xiv-overlay.enable.%s.enabled", settingKeyBase), false); enabled.addListener(this::recalc); @@ -257,7 +253,8 @@ private void recalc() { frame.setVisible(true); } panel.setVisible(visible); - setClickThrough(frame, !editMode); + frame.setClickThrough(!editMode); +// setClickThrough(frame, !editMode); frame.setFocusable(editMode); if (editMode) { panel.setBorder(editBorder); @@ -288,38 +285,7 @@ public DoubleSetting getScaleSetting() { return scaleFactor; } - // https://docs.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles - private static final long WS_NOACTIVATE = 0x08000000L; - private static void setClickThrough(JFrame w, boolean clickThrough) { - log.trace("Click-through: {}", clickThrough); - w.setFocusableWindowState(!clickThrough); - if (!Platform.isWindows()) { - log.warn("Setting click-through is not supported on non-Windows platforms at this time."); - return; - } - WinDef.HWND hwnd = getHWnd(w); - int wl = User32.INSTANCE.GetWindowLong(hwnd, WinUser.GWL_EXSTYLE); - if (clickThrough) { - wl |= WinUser.WS_EX_TRANSPARENT; - wl |= WS_NOACTIVATE; - } - else { - wl &= ~WinUser.WS_EX_TRANSPARENT; - wl &= ~WS_NOACTIVATE; - } - w.setBackground(new Color(0, 0, 0, 0)); - User32.INSTANCE.SetWindowLong(hwnd, WinUser.GWL_EXSTYLE, wl); - } - - /** - * Get the window handle from the OS - */ - private static WinDef.HWND getHWnd(Component w) { - WinDef.HWND hwnd = new WinDef.HWND(); - hwnd.setPointer(Native.getComponentPointer(w)); - return hwnd; - } private void calcFrameTimes() { minFrameTime = 1000 / oc.getMaxFps().get(); diff --git a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tabs/UpdatesPanel.java b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tabs/UpdatesPanel.java index 92ecec91c76d..467cd314e87f 100644 --- a/xivsupport/src/main/java/gg/xp/xivsupport/gui/tabs/UpdatesPanel.java +++ b/xivsupport/src/main/java/gg/xp/xivsupport/gui/tabs/UpdatesPanel.java @@ -126,7 +126,12 @@ private void updateNow() { try { // Desktop.open seems to open it in such a way that when we exit, we release the mutex, so the updater // can relaunch the application correctly. - Desktop.getDesktop().open(Paths.get(installDir.toString(), "triggevent-upd.exe").toFile()); + if (Platform.isWindows()) { + Desktop.getDesktop().open(Paths.get(installDir.toString(), "triggevent-upd.exe").toFile()); + } + else { + Runtime.getRuntime().exec(new String[]{"sh", "triggevent-upd.sh"}); + } } catch (Throwable e) { log.error("Error launching updater", e); diff --git a/xivsupport/src/main/resources/libjawt.so b/xivsupport/src/main/resources/libjawt.so new file mode 100644 index 000000000000..80f0b74b01e0 Binary files /dev/null and b/xivsupport/src/main/resources/libjawt.so differ diff --git a/xivsupport/src/test/java/gg/xp/xivsupport/callouts/CalloutExpiryTest.java b/xivsupport/src/test/java/gg/xp/xivsupport/callouts/CalloutExpiryTest.java index 31a404706fe4..f3ae44dc5c0d 100644 --- a/xivsupport/src/test/java/gg/xp/xivsupport/callouts/CalloutExpiryTest.java +++ b/xivsupport/src/test/java/gg/xp/xivsupport/callouts/CalloutExpiryTest.java @@ -63,7 +63,7 @@ void testCalloutExpiryFixedNoEvent() throws Throwable { Assert.assertFalse(call.isExpired()); MatcherAssert.assertThat(data.getCurrentVisualCalls(), Matchers.hasSize(1)); - Thread.sleep(1000); + Thread.sleep(800); Assert.assertFalse(call.isExpired()); MatcherAssert.assertThat(data.getCurrentVisualCalls(), Matchers.hasSize(1)); @@ -83,7 +83,7 @@ void testCalloutExpiryFixedNoEvent() throws Throwable { Assert.assertFalse(call.isExpired()); MatcherAssert.assertThat(data.getCurrentVisualCalls(), Matchers.hasSize(1)); - Thread.sleep(1000); + Thread.sleep(800); Assert.assertFalse(call.isExpired()); MatcherAssert.assertThat(data.getCurrentVisualCalls(), Matchers.hasSize(1)); @@ -107,7 +107,7 @@ void testCalloutExpiryFixedWithEvent() throws Throwable { Assert.assertFalse(call.isExpired()); MatcherAssert.assertThat(data.getCurrentVisualCalls(), Matchers.hasSize(1)); - Thread.sleep(1000); + Thread.sleep(800); Assert.assertFalse(call.isExpired()); MatcherAssert.assertThat(data.getCurrentVisualCalls(), Matchers.hasSize(1)); @@ -128,7 +128,7 @@ void testCalloutExpiryFixedWithEvent() throws Throwable { Assert.assertFalse(call.isExpired()); MatcherAssert.assertThat(data.getCurrentVisualCalls(), Matchers.hasSize(1)); - Thread.sleep(1000); + Thread.sleep(800); Assert.assertFalse(call.isExpired()); MatcherAssert.assertThat(data.getCurrentVisualCalls(), Matchers.hasSize(1));