javamacosjavafx

JavaFX MacOS - window not movable with transparent titlebar


I managed to manipulate a JavaFX window using Java FFM and MacOS native NSWindow object. However, when setting the titlebarAppearsTransparent to true and adding the NSWindowStyleMaskFullSizeContentView the window is only movable when you manage to drag it at the very upper border. Even though the title bar at this point is not visible, I'd like to be able to drag the window using an "imaginary" toolbar...

So how do I get the window to recognize the title bar as a movable rectangle even though it is transparent?

So far this is what I've done to reach this. Code is very redundant, but currently I'm just fooling around trying to proofing a concept...

  1. Get the NSWindow handle
    private long getNSWindowHandle() throws Throwable {

        // Get class NSApplication
        try (Arena arena = Arena.ofConfined()) {

            long nsApplication = (long) objc_getClass.invoke(arena.allocateFrom("NSApplication"));
            System.out.println("nsApplication: " + Long.toHexString(nsApplication));


            // Get selector for sharedApplication
            long sharedAppSelector = (long) sel_registerName.invoke(arena.allocateFrom("sharedApplication"));
            System.out.println("sharedAppSelector: " + Long.toHexString(sharedAppSelector));

            // Get NSApplication shared instance
            long sharedApplication = (long) LINKER.downcallHandle(
                            symbolLookup.find("objc_msgSend").orElseThrow(),
                            FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG))
                                                    .invoke(nsApplication, sharedAppSelector);
            System.out.println("sharedApplication: " + Long.toHexString(sharedApplication));

            // Get selector for windows array
            long windowsSelector = (long) sel_registerName.invoke(arena.allocateFrom("windows"));
            System.out.println("windowsSelector: " + Long.toHexString(windowsSelector));

            // Get array of windows (NSArray of NSWindow objects)
            long windowsArray = (long) LINKER.downcallHandle(
                            symbolLookup.find("objc_msgSend").orElseThrow(),
                            FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG))
                                               .invoke(sharedApplication, windowsSelector);
            System.out.println("windowsArray: " + Long.toHexString(windowsArray));

            // Get selector for objectAtIndex:
            long objectAtIndexSelector = (long) sel_registerName.invoke(arena.allocateFrom("objectAtIndex:"));
            System.out.println("objectAtIndexSelector: " + Long.toHexString(objectAtIndexSelector));

            // Get first window (NSWindow) in the array (index 0)
            long nsWindow = (long) LINKER.downcallHandle(
                            symbolLookup.find("objc_msgSend").orElseThrow(),
                            FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG))
                                           .invoke(windowsArray, objectAtIndexSelector, (long) 0);
            System.out.println("nsWindow: " + Long.toHexString(nsWindow));
            
            return nsWindow;
        }
    }

  1. Modify the NSWindow for desired style
    private void setCustomWindow(long nsWindow) throws Throwable {

        try (Arena arena = Arena.ofConfined()) {

            // TRANSPARENT TITLE BAR
            var titleBarTransparentSelector = (long) sel_registerName.invoke(arena.allocateFrom("setTitlebarAppearsTransparent:"));
            LINKER.downcallHandle(
                            symbolLookup.find("objc_msgSend").orElseThrow(),
                            FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_BOOLEAN))
                    .invoke(nsWindow, titleBarTransparentSelector, true);


            // UPDATE STYLE MASK
            long styleMaskSelector = (long) sel_registerName.invoke(arena.allocateFrom("styleMask"));
            long setStyleMaskSelector = (long) sel_registerName.invoke(arena.allocateFrom("setStyleMask:"));
            long styleMask = (long) LINKER.downcallHandle(
                            symbolLookup.find("objc_msgSend").orElseThrow(),
                            FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG))
                                            .invoke(nsWindow, styleMaskSelector);

            styleMask &= ~NSWindowStyleMaskUnifiedTitleAndToolbar;
            styleMask |= NSWindowStyleMaskFullSizeContentView;
            styleMask |= NSWindowStyleMaskTitled;
            LINKER.downcallHandle(symbolLookup.find("objc_msgSend")
                                          .orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG))
                    .invoke(nsWindow, setStyleMaskSelector, styleMask);

            // TITLE SEPARATOR
            int titleSeparator = 1;
            long setTitlebarSeparator = (long) sel_registerName.invoke(arena.allocateFrom("setTitlebarSeparatorStyle:"));
            LINKER.downcallHandle(symbolLookup.find("objc_msgSend")
                                          .orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT))
                    .invoke(nsWindow, setTitlebarSeparator, titleSeparator);

            // TITLE VISIBILITY (0 = visible, 1 = hidden)
            int titleVisible = 0;
            long setTitleVisibility = (long) sel_registerName.invoke(arena.allocateFrom("setTitleVisibility:"));
            LINKER.downcallHandle(symbolLookup.find("objc_msgSend")
                                          .orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT))
                    .invoke(nsWindow, setTitleVisibility, titleVisible);


            // MOVABLE
            var setMovableSelector = (long) sel_registerName.invoke(arena.allocateFrom("setMovable:"));
            LINKER.downcallHandle(symbolLookup.find("objc_msgSend")
                                          .orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_BOOLEAN))
                    .invoke(nsWindow, setMovableSelector, true);

            // MOVABLE BY BACKGROUND
            var isMovableByBackgroundSelector = (long) sel_registerName.invoke(arena.allocateFrom("setMovableByWindowBackground:"));
            LINKER.downcallHandle(symbolLookup.find("objc_msgSend")
                                          .orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG, ValueLayout.JAVA_BOOLEAN))
                    .invoke(nsWindow, isMovableByBackgroundSelector, true);

        }
    }

Solution

  • I did it!

    After digging around in memory dumps and everything I was confident, that the problem does not lie within my configuration nor within Apples Objective-C libraries (duh), I decided to take a look at the JFX library to see how they initially create a window. After fuzzing around with a lot of stuff I discovered the is section in GlassView3D.m:

    - (BOOL)mouseDownCanMoveWindow
    {
        return NO;
    }
    

    Changing that to a YES did the trick. Window is now draggable at the "imaginary" titlebar 🥳

    As of yet I need to figure out the exact way this GlassView3D is implemented but I am happy for now that I got it to work.. Maybe this helps someone...

    Goes without saying, that you should do this at your own risk in a production environment.