diff --git a/.gitignore b/.gitignore index 48b701855..04312448a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ ComponentsGenerated.wxs !dist/javafx/**/lib !dist/javafx/**/bin dev.properties +xcuserdata/ +*.dylib +project.xcworkspace diff --git a/app/src/main/java/io/xpipe/app/core/AppTheme.java b/app/src/main/java/io/xpipe/app/core/AppTheme.java index 017c4cf75..52f3c54bb 100644 --- a/app/src/main/java/io/xpipe/app/core/AppTheme.java +++ b/app/src/main/java/io/xpipe/app/core/AppTheme.java @@ -43,6 +43,7 @@ public class AppTheme { public static void initThemeHandlers(Stage stage) { Runnable r = () -> { + stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass(OsType.getLocal().getId()), true); if (AppPrefs.get() == null) { var def = Theme.getDefaultLightTheme(); stage.getScene().getRoot().getStyleClass().add(def.getCssId()); diff --git a/app/src/main/java/io/xpipe/app/core/window/ModifiedStage.java b/app/src/main/java/io/xpipe/app/core/window/ModifiedStage.java index 63527e72b..ef0f69fa1 100644 --- a/app/src/main/java/io/xpipe/app/core/window/ModifiedStage.java +++ b/app/src/main/java/io/xpipe/app/core/window/ModifiedStage.java @@ -17,7 +17,7 @@ import org.apache.commons.lang3.SystemUtils; public class ModifiedStage extends Stage { public static boolean mergeFrame() { - return SystemUtils.IS_OS_WINDOWS_11; + return SystemUtils.IS_OS_WINDOWS_11 || SystemUtils.IS_OS_MAC; } public static void init() { @@ -55,24 +55,37 @@ public class ModifiedStage extends Stage { return; } - if (OsType.getLocal() != OsType.WINDOWS || AppPrefs.get() == null || AppPrefs.get().theme.getValue() == null) { + if (OsType.getLocal() == OsType.LINUX || AppPrefs.get() == null || AppPrefs.get().theme.getValue() == null) { stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), false); stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), true); return; } - var ctrl = new NativeWinWindowControl(stage); - ctrl.setWindowAttribute( - NativeWinWindowControl.DmwaWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE.get(), - AppPrefs.get().theme.getValue().isDark()); - boolean seamlessFrame; - if (AppPrefs.get().performanceMode().get() || !mergeFrame()) { - seamlessFrame = false; - } else { - seamlessFrame = ctrl.setWindowBackdrop(NativeWinWindowControl.DwmSystemBackDropType.MICA_ALT); + switch (OsType.getLocal()) { + case OsType.Linux linux -> { + } + case OsType.MacOs macOs -> { + var ctrl = new NativeMacOsWindowControl(stage); + var seamlessFrame = !AppPrefs.get().performanceMode().get() && mergeFrame(); + var seamlessFrameApplied = seamlessFrame && ctrl.setAppearance(seamlessFrame, AppPrefs.get().theme.getValue().isDark()); + stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrameApplied); + stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrameApplied); + } + case OsType.Windows windows -> { + var ctrl = new NativeWinWindowControl(stage); + ctrl.setWindowAttribute( + NativeWinWindowControl.DmwaWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE.get(), + AppPrefs.get().theme.getValue().isDark()); + boolean seamlessFrame; + if (AppPrefs.get().performanceMode().get() || !mergeFrame()) { + seamlessFrame = false; + } else { + seamlessFrame = ctrl.setWindowBackdrop(NativeWinWindowControl.DwmSystemBackDropType.MICA_ALT); + } + stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrame); + stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrame); + } } - stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrame); - stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrame); } private static void updateStage(Stage stage) { diff --git a/app/src/main/java/io/xpipe/app/core/window/NativeMacOsWindowControl.java b/app/src/main/java/io/xpipe/app/core/window/NativeMacOsWindowControl.java new file mode 100644 index 000000000..817d8c57a --- /dev/null +++ b/app/src/main/java/io/xpipe/app/core/window/NativeMacOsWindowControl.java @@ -0,0 +1,44 @@ +package io.xpipe.app.core.window; + +import com.sun.jna.NativeLong; +import io.xpipe.app.util.NativeBridge; +import io.xpipe.core.util.ModuleHelper; +import javafx.stage.Window; +import lombok.Getter; +import lombok.SneakyThrows; + +import java.lang.reflect.Method; + +@Getter +public class NativeMacOsWindowControl { + + private final long nsWindow; + + @SneakyThrows + public NativeMacOsWindowControl(Window stage) { + Method tkStageGetter = Window.class.getDeclaredMethod("getPeer"); + tkStageGetter.setAccessible(true); + Object tkStage = tkStageGetter.invoke(stage); + Method getPlatformWindow = tkStage.getClass().getDeclaredMethod("getPlatformWindow"); + getPlatformWindow.setAccessible(true); + Object platformWindow = getPlatformWindow.invoke(tkStage); + Method getNativeHandle = platformWindow.getClass().getMethod("getNativeHandle"); + getNativeHandle.setAccessible(true); + Object nativeHandle = getNativeHandle.invoke(platformWindow); + this.nsWindow = (long) nativeHandle; + } + + public boolean setAppearance(boolean seamlessFrame, boolean darkMode) { + if (!ModuleHelper.isImage()) { + return false; + } + + var lib = NativeBridge.getMacOsLibrary(); + if (lib.isEmpty()) { + return false; + } + + lib.get().setAppearance(new NativeLong(nsWindow), seamlessFrame, darkMode); + return true; + } +} diff --git a/app/src/main/java/io/xpipe/app/util/NativeBridge.java b/app/src/main/java/io/xpipe/app/util/NativeBridge.java new file mode 100644 index 000000000..87bf14b73 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/NativeBridge.java @@ -0,0 +1,36 @@ +package io.xpipe.app.util; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.NativeLong; +import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.core.util.XPipeInstallation; + +import java.util.Map; +import java.util.Optional; + +public class NativeBridge { + + private static MacOsLibrary macOsLibrary; + private static boolean loadingFailed; + + public static Optional getMacOsLibrary() { + if (macOsLibrary == null && !loadingFailed) { + try { + System.setProperty("jna.library.path", XPipeInstallation.getCurrentInstallationBasePath() + .resolve("Contents").resolve("runtime").resolve("Contents").resolve("Home").resolve("lib").toString()); + var l = Native.load("xpipe_bridge", MacOsLibrary.class, Map.of()); + macOsLibrary = l; + } catch (Throwable t) { + ErrorEvent.fromThrowable(t).handle(); + loadingFailed = true; + } + } + return Optional.ofNullable(macOsLibrary); + } + + public static interface MacOsLibrary extends Library { + + public abstract void setAppearance(NativeLong window, boolean seamlessFrame, boolean dark); + } +} diff --git a/app/src/main/resources/io/xpipe/app/resources/style/style.css b/app/src/main/resources/io/xpipe/app/resources/style/style.css index a964ddde4..7c9572193 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/style.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/style.css @@ -9,6 +9,10 @@ -fx-background-color: transparent; } +.root:macos:seamless-frame { + -fx-padding: 0 0 25 0; +} + .root:dark:separate-frame .background { -fx-background-color: derive(-color-bg-default, 1%); } @@ -53,6 +57,11 @@ -fx-padding: 0 0 0 0; } +.root:macos:seamless-frame.layout > .background { + -fx-background-insets: 0; + -fx-border-insets: 0; +} + .root:seamless-frame.layout > .background > * { -fx-background-radius: 0 10 0 0; -fx-border-radius: 0 10 0 0; diff --git a/core/src/main/java/io/xpipe/core/process/OsType.java b/core/src/main/java/io/xpipe/core/process/OsType.java index 9882e409e..f977700b7 100644 --- a/core/src/main/java/io/xpipe/core/process/OsType.java +++ b/core/src/main/java/io/xpipe/core/process/OsType.java @@ -46,6 +46,8 @@ public interface OsType { sealed interface Local extends OsType permits OsType.Windows, OsType.Linux, OsType.MacOs { + String getId(); + default Any toAny() { return (Any) this; } @@ -131,6 +133,11 @@ public interface OsType { return "Windows"; } } + + @Override + public String getId() { + return "windows"; + } } class Unix implements OsType { @@ -197,6 +204,11 @@ public interface OsType { final class Linux extends Unix implements OsType, Local, Any { + @Override + public String getId() { + return "linux"; + } + @Override public String determineOperatingSystemName(ShellControl pc) throws Exception { try (CommandControl c = pc.command("lsb_release -a").start()) { @@ -223,6 +235,11 @@ public interface OsType { final class MacOs implements OsType, Local, Any { + @Override + public String getId() { + return "macos"; + } + @Override public String makeFileSystemCompatible(String name) { // Technically the backslash is supported, but it causes all kinds of troubles, so we also exclude it diff --git a/dist/base.gradle b/dist/base.gradle index 1c0469b6f..068f4972c 100644 --- a/dist/base.gradle +++ b/dist/base.gradle @@ -229,6 +229,19 @@ if (org.gradle.internal.os.OperatingSystem.current().isWindows()) { commandLine "$projectDir/misc/mac/sign_and_notarize.sh", "$projectDir", rootProject.arch.toString(), rootProject.productName } } + + if (fullVersion) { + def nativeLib = "$projectDir/native_lib/macos" + def proj = "$nativeLib/xpipe_bridge.xcodeproj" + exec { + environment 'CONFIGURATION_BUILD_DIR', project.getLayout().getBuildDirectory().dir("native_lib") + commandLine 'xcodebuild', '-configuration', 'Release', '-project', proj, '-scheme', 'xpipe_bridge', '-derivedDataPath', project.getLayout().getBuildDirectory().dir("native_lib").get(), 'build' + } + copy { + from project.getLayout().getBuildDirectory().dir("native_lib/Build/Products/Release").get().file('libxpipe_bridge.dylib') + into "$distDir/$app/Contents/runtime/Contents/Home/lib/" + } + } } } }