Skip to content

Commit 5341ae9

Browse files
authored
Works on Windows (#19)
Co-authored-by: U-DESKTOP-QVUEOL6\tanin <@tanin>
1 parent b016ebc commit 5341ae9

16 files changed

Lines changed: 239 additions & 52 deletions

build.gradle.kts

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
22
import java.nio.file.Files
33
import java.nio.file.Paths
4+
import java.util.Locale
45
import kotlin.io.path.absolutePathString
56
import kotlin.io.path.createTempDirectory
67
import kotlin.io.path.isExecutable
@@ -11,11 +12,23 @@ plugins {
1112
jacoco
1213
}
1314

15+
enum class OS {
16+
MAC, WINDOWS, LINUX
17+
}
18+
19+
val currentOS = when {
20+
System.getProperty("os.name").lowercase().contains("mac") -> OS.MAC
21+
System.getProperty("os.name").lowercase().contains("windows") -> OS.WINDOWS
22+
else -> OS.LINUX
23+
}
24+
1425

1526
val isNotarizing = System.getenv("NOTARIZE") != null || gradle.startParameter.taskNames.find { s ->
1627
s.lowercase().contains("notarize") || s.lowercase().contains("jpackage") || s.lowercase().contains("staple")
1728
} != null
1829

30+
31+
1932
val provisionprofileDir = layout.projectDirectory
2033
.dir("mac-resources")
2134
.dir("provisionprofile")
@@ -75,7 +88,28 @@ java {
7588
}
7689
}
7790

91+
tasks.register<Exec>("compileWindowsApi") {
92+
onlyIf {
93+
currentOS == OS.WINDOWS
94+
}
95+
group = "build"
96+
description = "Compile C code and output the dll to the resource directory."
97+
98+
commandLine(
99+
"gcc",
100+
"-shared",
101+
"-o",
102+
"./src/main/resources/native/WindowsApi.dll",
103+
"./src/main/c/WindowsApi.c",
104+
"-lcomdlg32",
105+
"-lgdi32"
106+
)
107+
}
108+
78109
tasks.register<Exec>("compileSwift") {
110+
onlyIf {
111+
System.getProperty("os.name").lowercase().contains("mac")
112+
}
79113
group = "build"
80114
description = "Compile Swift code and output the dylib to the resource directory."
81115

@@ -96,7 +130,11 @@ tasks.register<Exec>("compileSwift") {
96130
}
97131

98132
tasks.named<JavaCompile>("compileJava") {
99-
dependsOn("compileSwift")
133+
if (currentOS == OS.MAC) {
134+
dependsOn("compileSwift")
135+
} else if (currentOS == OS.WINDOWS) {
136+
dependsOn("compileWindowsApi")
137+
}
100138
options.compilerArgs.addAll(listOf(
101139
"--add-exports",
102140
"java.base/sun.security.x509=ALL-UNNAMED",
@@ -148,24 +186,28 @@ tasks.named<Test>("test") {
148186
var mainClassName = "tanin.javaelectron.Main"
149187
application {
150188
mainClass.set(mainClassName)
151-
applicationDefaultJvmArgs = listOf(
152-
"-XstartOnFirstThread",
153-
"--add-exports",
154-
"java.base/sun.security.x509=ALL-UNNAMED",
155-
"--add-exports",
156-
"java.base/sun.security.tools.keytool=ALL-UNNAMED",
157-
)
189+
applicationDefaultJvmArgs = buildList {
190+
if (currentOS == OS.MAC) {
191+
add("-XstartOnFirstThread")
192+
}
193+
add("--add-exports")
194+
add("java.base/sun.security.x509=ALL-UNNAMED")
195+
add("--add-exports")
196+
add("java.base/sun.security.tools.keytool=ALL-UNNAMED")
197+
}
158198
}
159199

160200
tasks.jar {
161201
manifest.attributes["Main-Class"] = mainClassName
162202
}
163203

204+
val executableExt = if (currentOS == OS.WINDOWS) ".cmd" else ""
205+
164206
tasks.register<Exec>("compileTailwind") {
165207
environment("NODE_ENV", "production")
208+
executable = "./node_modules/.bin/postcss${executableExt}"
166209

167-
commandLine(
168-
"./node_modules/.bin/postcss",
210+
args = listOf(
169211
"./frontend/stylesheets/tailwindbase.css",
170212
"--config",
171213
".",
@@ -177,9 +219,9 @@ tasks.register<Exec>("compileTailwind") {
177219
tasks.register<Exec>("compileSvelte") {
178220
environment("NODE_ENV", "production")
179221
environment("ENABLE_SVELTE_CHECK", "true")
222+
executable = "./node_modules/.bin/webpack${executableExt}"
180223

181-
commandLine(
182-
"./node_modules/webpack/bin/webpack.js",
224+
args = listOf(
183225
"--config",
184226
"./webpack.config.js",
185227
"--output-path",
@@ -398,6 +440,13 @@ tasks.register("bareJpackage") {
398440
outputs.file(outputFile)
399441
outputDir.get().asFile.deleteRecursively()
400442

443+
// -XstartOnFirstThread is required for MacOS
444+
val maybeStartOnFirstThread = if (currentOS == OS.MAC) {
445+
"-XstartOnFirstThread"
446+
} else {
447+
""
448+
}
449+
401450
doLast {
402451
runCmd(
403452
jpackageBin.absolutePathString(),
@@ -418,9 +467,8 @@ tasks.register("bareJpackage") {
418467
"--app-content", provisionprofileDir.file("embedded.provisionprofile").asFile.absolutePath,
419468
"--app-content", layout.buildDirectory.file("resources-native").get().asFile.resolve("app").absolutePath,
420469
"--java-options",
421-
// -XstartOnFirstThread is required for MacOs
422470
// -Djava.library.path=$APPDIR/resources is needed because we put all dylibs there.
423-
"-XstartOnFirstThread -Djava.library.path=\$APPDIR/resources --add-exports java.base/sun.security.x509=ALL-UNNAMED --add-exports java.base/sun.security.tools.keytool=ALL-UNNAMED"
471+
"$maybeStartOnFirstThread -Djava.library.path=\$APPDIR/resources --add-exports java.base/sun.security.x509=ALL-UNNAMED --add-exports java.base/sun.security.tools.keytool=ALL-UNNAMED"
424472
)
425473
}
426474
}

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main/c/WindowsApi.c

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#include <windows.h>
2+
#include <stdio.h>
3+
#include <string.h>
4+
5+
char* getStringFromC(char *input) {
6+
char* str = strdup(input);
7+
return str;
8+
}
9+
10+
__declspec(dllexport) void freeString(char* str) {
11+
free(str); // Free the memory allocated in C
12+
}
13+
14+
__declspec(dllexport) char* openFileDialog(long hwnd, bool isSaved) {
15+
OPENFILENAME ofn;
16+
CHAR szFile[MAX_PATH];
17+
18+
ZeroMemory(&ofn, sizeof(ofn));
19+
ofn.lStructSize = sizeof(ofn);
20+
ofn.hwndOwner = (HWND) (uintptr_t) hwnd;
21+
ofn.lpstrFile = szFile;
22+
ofn.lpstrFile[0] = '\0';
23+
ofn.nMaxFile = sizeof(szFile);
24+
ofn.lpstrFilter = "All Files (*.*)\0";
25+
ofn.lpstrTitle = isSaved ? "Select a file to save" : "Select a file to open";
26+
ofn.Flags = isSaved ? OFN_OVERWRITEPROMPT : (OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST);
27+
28+
bool result = isSaved ? GetSaveFileName(&ofn) : GetOpenFileName(&ofn);
29+
30+
// Display the Open dialog box
31+
if (result == TRUE) {
32+
printf("Selected file: %s\n", ofn.lpstrFile);
33+
fflush(stdout);
34+
return getStringFromC(ofn.lpstrFile);
35+
} else {
36+
printf("No file selected or an error occurred.\n");
37+
fflush(stdout);
38+
return NULL;
39+
}
40+
}
Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,69 @@
11
package tanin.javaelectron;
22

3+
import com.eclipsesource.json.Json;
34
import sun.misc.Signal;
45
import tanin.ejwf.SelfSignedCertificate;
6+
import tanin.javaelectron.nativeinterface.Base;
57
import tanin.javaelectron.nativeinterface.MacOsApi;
8+
import tanin.javaelectron.nativeinterface.WebviewNative;
9+
import tanin.javaelectron.nativeinterface.WindowsApi;
610

11+
import java.io.IOException;
12+
import java.nio.file.Files;
13+
import java.nio.file.Path;
714
import java.util.logging.Level;
815
import java.util.logging.Logger;
916

1017
import static tanin.javaelectron.nativeinterface.WebviewNative.N;
1118

1219
public class Browser {
13-
public static interface JsInvoker {
14-
void invoke(String js);
20+
public static interface OnFileSelected {
21+
void invoke(String path);
1522
}
16-
23+
1724
private static final Logger logger = Logger.getLogger(Browser.class.getName());
1825

1926
String url;
2027
boolean isDebug;
2128
private long pointer;
2229
SelfSignedCertificate cert;
2330

31+
private static final String OS_NAME = System.getProperty("os.name").toLowerCase();
32+
33+
private MacOsApi.OnFileSelected onFileSelected = null;
34+
2435
public Browser(String url, boolean isDebug) {
2536
this.url = url;
2637
this.isDebug = isDebug;
2738
}
2839

2940
public void run() throws InterruptedException {
30-
MacOsApi.N.setupMenu();
41+
if (Base.CURRENT_OS == Base.OperatingSystem.MAC) {
42+
MacOsApi.N.setupMenu();
43+
}
3144

3245
pointer = N.webview_create(isDebug, null);
3346
N.webview_navigate(pointer, url);
3447

3548
Signal.handle(new Signal("INT"), sig -> terminate());
3649
Runtime.getRuntime().addShutdownHook(new Thread(this::terminate));
3750

38-
MacOsApi.N.nsWindowMakeKeyAndOrderFront();
51+
if (Base.CURRENT_OS == Base.OperatingSystem.MAC) {
52+
MacOsApi.N.nsWindowMakeKeyAndOrderFront();
53+
}
3954
N.webview_run(pointer);
4055
if (this.pointer != 0) {
4156
N.webview_destroy(this.pointer);
4257
this.pointer = 0;
4358
}
4459
}
4560

61+
public long getWindowPointer() {
62+
return N.webview_get_window(pointer);
63+
}
64+
4665
public void eval(String js) {
47-
N.webview_eval(pointer, js);
66+
N.webview_dispatch(pointer, ($pointer, arg) -> N.webview_eval(pointer, js), 0);
4867
}
4968

5069
private void terminate() {
@@ -58,4 +77,44 @@ private void terminate() {
5877
logger.log(Level.WARNING, "Error while terminating webview", e);
5978
}
6079
}
80+
81+
void openFileDialog(boolean isSaved, OnFileSelected fileSelected) {
82+
if (Base.CURRENT_OS == Base.OperatingSystem.WINDOWS) {
83+
var thread = new Thread(() -> {
84+
var pointer = WindowsApi.N.openFileDialog(getWindowPointer(), isSaved);
85+
86+
if (pointer == null) {
87+
logger.info("No file has been selected");
88+
} else {
89+
try {
90+
String filePath = pointer.getString(0);
91+
fileSelected.invoke(filePath);
92+
} finally {
93+
WindowsApi.N.freeString(pointer);
94+
}
95+
}
96+
});
97+
thread.start();
98+
} else if (Base.CURRENT_OS == Base.OperatingSystem.MAC) {
99+
onFileSelected = filePath -> {
100+
System.out.println("Opening file: " + filePath);
101+
102+
MacOsApi.N.startAccessingSecurityScopedResource(filePath);
103+
try {
104+
fileSelected.invoke(filePath);
105+
} finally {
106+
MacOsApi.N.stopAccessingSecurityScopedResource(filePath);
107+
}
108+
onFileSelected = null;
109+
};
110+
111+
if (isSaved) {
112+
MacOsApi.N.saveFile(onFileSelected);
113+
} else {
114+
MacOsApi.N.openFile(onFileSelected);
115+
}
116+
} else {
117+
throw new RuntimeException("Unsupported OS: " + OS_NAME);
118+
}
119+
}
61120
}

src/main/java/tanin/javaelectron/Main.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import tanin.ejwf.MinumBuilder;
44
import tanin.ejwf.SelfSignedCertificate;
55
import tanin.javaelectron.nativeinterface.Base;
6+
import tanin.javaelectron.nativeinterface.WindowsApi;
67

78
import java.io.IOException;
89
import java.util.logging.LogManager;
@@ -11,8 +12,6 @@
1112
public class Main {
1213
private static final Logger logger = Logger.getLogger(Main.class.getName());
1314

14-
private static Browser browser;
15-
1615
static {
1716
try (var configFile = Main.class.getResourceAsStream("/logging.properties")) {
1817
LogManager.getLogManager().readConfiguration(configFile);
@@ -31,16 +30,18 @@ public static void main(String[] args) throws Exception {
3130
logger.info(" Certificate SHA-256 Fingerprint: " + SelfSignedCertificate.getSHA256Fingerprint(cert.cert().getEncoded()));
3231

3332
var authKey = SelfSignedCertificate.generateRandomString(32);
34-
var main = new Server(cert, authKey, js -> browser.eval(js));
33+
var main = new Server(cert, authKey);
3534
logger.info("Starting...");
3635
main.start();
3736

38-
var sslPort = main.minum.getSslServer().getPort();
37+
var port = main.minum.getSslServer().getPort();
38+
// var port = main.minum.getServer().getPort();
3939

40-
browser = new Browser(
41-
"https://localhost:" + sslPort + "?authKey=" + authKey,
40+
var browser = new Browser(
41+
"https://localhost:" + port + "?authKey=" + authKey,
4242
MinumBuilder.IS_LOCAL_DEV
4343
);
44+
main.browser = browser;
4445
browser.run();
4546

4647
logger.info("Exiting");

0 commit comments

Comments
 (0)