mirror of
https://github.com/AssetRipper/AssetRipper.NativeDialogs.git
synced 2026-11-13 18:06:41 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e7a80aa80 | ||
|
|
c4569ae1b3 | ||
|
|
0431d0b536 | ||
|
|
801b203fca | ||
|
|
5d207fbdf7 | ||
|
|
7f3ac48095 | ||
|
|
e4515bb59a |
No files matched your search
@@ -8,7 +8,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
publish_example:
|
||||
runs-on: ${{ matrix.config.os }}
|
||||
|
||||
strategy:
|
||||
@@ -61,3 +61,57 @@ jobs:
|
||||
./AssetRipper.NativeDialogs.Example/bin/Release/net9.0/${{ matrix.config.runtime }}/publish/*.so
|
||||
./AssetRipper.NativeDialogs.Example/bin/Release/net9.0/${{ matrix.config.runtime }}/publish/*.dylib
|
||||
if-no-files-found: error
|
||||
|
||||
publish_web_api:
|
||||
runs-on: ${{ matrix.config.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
config:
|
||||
- { name: win_x64, os: windows-latest, runtime: win-x64, executable: AssetRipper.NativeDialogs.WebApi.exe }
|
||||
- { name: win_arm64, os: windows-latest, runtime: win-arm64, executable: AssetRipper.NativeDialogs.WebApi.exe }
|
||||
- { name: linux_x64, os: ubuntu-22.04, runtime: linux-x64, executable: AssetRipper.NativeDialogs.WebApi }
|
||||
- { name: linux_arm64, os: ubuntu-22.04, runtime: linux-arm64, executable: AssetRipper.NativeDialogs.WebApi }
|
||||
- { name: mac_x64, os: macos-latest, runtime: osx-x64, executable: AssetRipper.NativeDialogs.WebApi }
|
||||
- { name: mac_arm64, os: macos-latest, runtime: osx-arm64, executable: AssetRipper.NativeDialogs.WebApi }
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
# https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/cross-compile#linux
|
||||
- name: Install Arm64 Dependencies
|
||||
if: matrix.config.runtime == 'linux-arm64'
|
||||
run: |
|
||||
sudo dpkg --add-architecture arm64
|
||||
sudo bash -c 'cat > /etc/apt/sources.list.d/arm64.list <<EOF
|
||||
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy main restricted
|
||||
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy-updates main restricted
|
||||
deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ jammy-backports main restricted universe multiverse
|
||||
EOF'
|
||||
sudo sed -i -e 's/deb http/deb [arch=amd64] http/g' /etc/apt/sources.list
|
||||
sudo sed -i -e 's/deb mirror/deb [arch=amd64] mirror/g' /etc/apt/sources.list
|
||||
sudo apt update
|
||||
sudo apt install -y clang llvm binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu zlib1g-dev:arm64
|
||||
|
||||
- name: Publish
|
||||
run: dotnet publish -c Release -r ${{ matrix.config.runtime }}
|
||||
working-directory: ./AssetRipper.NativeDialogs.WebApi/
|
||||
|
||||
- name: List Files
|
||||
shell: bash
|
||||
run: ls -R ./AssetRipper.NativeDialogs.WebApi/bin/Release/
|
||||
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: WebApi_${{ matrix.config.name }}
|
||||
path: |
|
||||
./AssetRipper.NativeDialogs.WebApi/bin/Release/net9.0/${{ matrix.config.runtime }}/publish/${{ matrix.config.executable }}
|
||||
./AssetRipper.NativeDialogs.WebApi/bin/Release/net9.0/${{ matrix.config.runtime }}/publish/*.dll
|
||||
./AssetRipper.NativeDialogs.WebApi/bin/Release/net9.0/${{ matrix.config.runtime }}/publish/*.so
|
||||
./AssetRipper.NativeDialogs.WebApi/bin/Release/net9.0/${{ matrix.config.runtime }}/publish/*.dylib
|
||||
if-no-files-found: error
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace AssetRipper.NativeDialogs.WebApi;
|
||||
|
||||
[JsonSerializable(typeof(string[]))]
|
||||
internal partial class AppJsonSerializerContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<PublishAot>true</PublishAot>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AssetRipper.NativeDialogs\AssetRipper.NativeDialogs.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,77 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace AssetRipper.NativeDialogs.WebApi;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
WebApplicationBuilder builder = WebApplication.CreateSlimBuilder(args);
|
||||
|
||||
builder.Services.ConfigureHttpJsonOptions(options =>
|
||||
{
|
||||
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
|
||||
});
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
app.MapGetFunction("/", () => Task.FromResult<string?>("Welcome to the AssetRipper Native Dialogs Web API!"));
|
||||
app.MapGetFunction("/open-file", OpenFileDialog.OpenFile);
|
||||
app.MapGetFunction("/open-files", OpenFileDialog.OpenFiles);
|
||||
app.MapGetFunction("/open-folder", OpenFolderDialog.OpenFolder);
|
||||
app.MapGetFunction("/open-folders", OpenFolderDialog.OpenFolders);
|
||||
app.MapGetFunction("/save-file", SaveFileDialog.SaveFile);
|
||||
app.MapGetFunction("/confirm", static async Task<string?> () =>
|
||||
{
|
||||
bool? result = await ConfirmationDialog.Confirm(new() { Message = "Do you acknowledge?", Type = ConfirmationDialog.Type.YesNo });
|
||||
return result switch
|
||||
{
|
||||
true => "User acknowledged.",
|
||||
false => "User did not acknowledge.",
|
||||
null => "User canceled the dialog."
|
||||
};
|
||||
});
|
||||
app.MapGetFunction("/message", static async Task<string?> () =>
|
||||
{
|
||||
await MessageDialog.Message("Hello, World!");
|
||||
return "Message dialog shown.";
|
||||
});
|
||||
|
||||
app.Run();
|
||||
}
|
||||
|
||||
private static void DisableCaching(this HttpResponse response)
|
||||
{
|
||||
response.Headers.CacheControl = "no-store, max-age=0";
|
||||
}
|
||||
|
||||
private static void MapGetFunction(this WebApplication app, [StringSyntax("Route")] string path, Func<Task<string?>> func)
|
||||
{
|
||||
app.MapGet(path, async (context) =>
|
||||
{
|
||||
context.Response.DisableCaching();
|
||||
await JsonResult(await func.Invoke()).ExecuteAsync(context);
|
||||
});
|
||||
}
|
||||
|
||||
private static void MapGetFunction(this WebApplication app, [StringSyntax("Route")] string path, Func<Task<string[]?>> func)
|
||||
{
|
||||
app.MapGet(path, async (context) =>
|
||||
{
|
||||
context.Response.DisableCaching();
|
||||
await JsonResult(await func.Invoke()).ExecuteAsync(context);
|
||||
});
|
||||
}
|
||||
|
||||
private static IResult JsonResult<T>(T? value) where T : class
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return Results.Text("null", "text/json");
|
||||
}
|
||||
else
|
||||
{
|
||||
return Results.Json(value, AppJsonSerializerContext.Default);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -18,6 +18,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
README.md = README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AssetRipper.NativeDialogs.WebApi", "AssetRipper.NativeDialogs.WebApi\AssetRipper.NativeDialogs.WebApi.csproj", "{18204EF5-F8B0-4314-BF6C-3FBB2AD6FB12}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -32,6 +34,10 @@ Global
|
||||
{B666C286-5649-456B-A356-FB91DC0DE0C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B666C286-5649-456B-A356-FB91DC0DE0C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B666C286-5649-456B-A356-FB91DC0DE0C3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{18204EF5-F8B0-4314-BF6C-3FBB2AD6FB12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{18204EF5-F8B0-4314-BF6C-3FBB2AD6FB12}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{18204EF5-F8B0-4314-BF6C-3FBB2AD6FB12}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{18204EF5-F8B0-4314-BF6C-3FBB2AD6FB12}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GtkSharp" Version="3.24.24.117-develop" />
|
||||
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.26100.2" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.Versioning;
|
||||
using TerraFX.Interop.Windows;
|
||||
@@ -111,35 +112,88 @@ public static class ConfirmationDialog
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
private static Task<bool?> ConfirmLinux(Options options)
|
||||
private static async Task<bool?> ConfirmLinux(Options options)
|
||||
{
|
||||
if (Gtk.Global.IsSupported)
|
||||
string escapedMessage = ProcessExecutor.EscapeString(options.Message);
|
||||
if (await LinuxHelper.HasZenity())
|
||||
{
|
||||
bool? result;
|
||||
GtkHelper.EnsureInitialized();
|
||||
|
||||
using Gtk.MessageDialog md = new(
|
||||
null,
|
||||
Gtk.DialogFlags.Modal,
|
||||
Gtk.MessageType.Info,
|
||||
options.Type == Type.OkCancel ? Gtk.ButtonsType.OkCancel : Gtk.ButtonsType.YesNo,
|
||||
options.Message
|
||||
);
|
||||
|
||||
int response = md.Run();
|
||||
result = response switch
|
||||
Process process = new()
|
||||
{
|
||||
(int)Gtk.ResponseType.Ok or (int)Gtk.ResponseType.Yes => true,
|
||||
(int)Gtk.ResponseType.Cancel or (int)Gtk.ResponseType.No => false,
|
||||
_ => throw new($"Unexpected response type: {response}"),
|
||||
StartInfo = new()
|
||||
{
|
||||
FileName = "zenity",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
},
|
||||
};
|
||||
|
||||
return Task.FromResult(result);
|
||||
process.StartInfo.ArgumentList.Add("--question");
|
||||
process.StartInfo.ArgumentList.Add("--text");
|
||||
process.StartInfo.ArgumentList.Add(escapedMessage);
|
||||
if (options.Type != Type.YesNo)
|
||||
{
|
||||
string escapedTrueLabel = ProcessExecutor.EscapeString(options.TrueLabel);
|
||||
string escapedFalseLabel = ProcessExecutor.EscapeString(options.FalseLabel);
|
||||
|
||||
process.StartInfo.ArgumentList.Add("--ok-label");
|
||||
process.StartInfo.ArgumentList.Add(escapedTrueLabel);
|
||||
process.StartInfo.ArgumentList.Add("--cancel-label");
|
||||
process.StartInfo.ArgumentList.Add(escapedFalseLabel);
|
||||
}
|
||||
|
||||
if (!process.Start())
|
||||
{
|
||||
return null; // Failed to start the process
|
||||
}
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
return process.ExitCode switch
|
||||
{
|
||||
0 => true, // User clicked OK or Yes
|
||||
1 => false, // User clicked Cancel or No
|
||||
_ => null, // An error occurred
|
||||
};
|
||||
}
|
||||
else if (await LinuxHelper.HasKDialog())
|
||||
{
|
||||
Process process = new()
|
||||
{
|
||||
StartInfo = new()
|
||||
{
|
||||
FileName = "kdialog",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
},
|
||||
};
|
||||
|
||||
process.StartInfo.ArgumentList.Add(options.Type is Type.YesNo ? "--yesno" : "--okcancel");
|
||||
process.StartInfo.ArgumentList.Add(escapedMessage);
|
||||
|
||||
if (!process.Start())
|
||||
{
|
||||
return null; // Failed to start the process
|
||||
}
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
return process.ExitCode switch
|
||||
{
|
||||
0 => true, // User clicked OK or Yes
|
||||
1 => false, // User clicked Cancel or No
|
||||
_ => null, // An error occurred
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback
|
||||
return Task.FromResult<bool?>(null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace AssetRipper.NativeDialogs;
|
||||
|
||||
internal static class GtkHelper
|
||||
{
|
||||
private static bool isInitialized;
|
||||
|
||||
public static void EnsureInitialized()
|
||||
{
|
||||
if (!isInitialized)
|
||||
{
|
||||
Gtk.Application.Init();
|
||||
isInitialized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace AssetRipper.NativeDialogs;
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
internal static class LinuxHelper
|
||||
{
|
||||
private static bool? hasZenity;
|
||||
private static bool? hasKDialog;
|
||||
|
||||
public static async Task<bool> HasSupportedBackend()
|
||||
{
|
||||
if (hasZenity is null)
|
||||
{
|
||||
await HasZenity();
|
||||
}
|
||||
if (hasKDialog is null)
|
||||
{
|
||||
await HasKDialog();
|
||||
}
|
||||
return hasZenity == true || hasKDialog == true;
|
||||
}
|
||||
|
||||
public static async Task<bool> HasZenity()
|
||||
{
|
||||
return hasZenity ??= await ProcessExecutor.HasCommand("zenity");
|
||||
}
|
||||
|
||||
public static async Task<bool> HasKDialog()
|
||||
{
|
||||
return hasKDialog ??= await ProcessExecutor.HasCommand("kdialog");
|
||||
}
|
||||
}
|
||||
@@ -79,27 +79,20 @@ public static class MessageDialog
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
private static Task MessageLinux(Options options)
|
||||
private static async Task MessageLinux(Options options)
|
||||
{
|
||||
if (Gtk.Global.IsSupported)
|
||||
string escapedMessage = ProcessExecutor.EscapeString(options.Message);
|
||||
if (await LinuxHelper.HasZenity())
|
||||
{
|
||||
GtkHelper.EnsureInitialized();
|
||||
|
||||
using Gtk.MessageDialog md = new(
|
||||
null,
|
||||
Gtk.DialogFlags.Modal,
|
||||
Gtk.MessageType.Info,
|
||||
Gtk.ButtonsType.Ok,
|
||||
options.Message
|
||||
);
|
||||
md.Run();
|
||||
|
||||
return Task.CompletedTask;
|
||||
await ProcessExecutor.TryRun("zenity", "--info", "--text", escapedMessage);
|
||||
}
|
||||
else if (await LinuxHelper.HasKDialog())
|
||||
{
|
||||
await ProcessExecutor.TryRun("kdialog", "--msgbox", escapedMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback
|
||||
return Task.CompletedTask;
|
||||
// Fallback: do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,12 @@
|
||||
|
||||
public static class NativeDialog
|
||||
{
|
||||
public static bool Supported =>
|
||||
OperatingSystem.IsWindows() ||
|
||||
OperatingSystem.IsMacOS() ||
|
||||
(OperatingSystem.IsLinux() && Gtk.Global.IsSupported);
|
||||
private static bool SupportedLinux { get; } = OperatingSystem.IsLinux() && LinuxHelper.HasSupportedBackend().WaitForResult();
|
||||
public static bool Supported => OperatingSystem.IsWindows() || OperatingSystem.IsMacOS() || (OperatingSystem.IsLinux() && SupportedLinux);
|
||||
|
||||
private static T WaitForResult<T>(this Task<T> task)
|
||||
{
|
||||
task.Wait();
|
||||
return task.Result;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Versioning;
|
||||
using TerraFX.Interop.Windows;
|
||||
@@ -69,32 +70,20 @@ public static class OpenFileDialog
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
private static Task<string?> OpenFileLinux()
|
||||
private static async Task<string?> OpenFileLinux()
|
||||
{
|
||||
if (Gtk.Global.IsSupported)
|
||||
if (await LinuxHelper.HasZenity())
|
||||
{
|
||||
string? result;
|
||||
GtkHelper.EnsureInitialized();
|
||||
|
||||
using Gtk.FileChooserNative dlg = new(
|
||||
"Open a file", null,
|
||||
Gtk.FileChooserAction.Open, "Open", "Cancel");
|
||||
|
||||
if (dlg.Run() == (int)Gtk.ResponseType.Accept)
|
||||
{
|
||||
result = dlg.Filename;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = null; // User canceled the dialog
|
||||
}
|
||||
|
||||
return Task.FromResult(result);
|
||||
return await ProcessExecutor.TryRun("zenity", "--file-selection");
|
||||
}
|
||||
else if (await LinuxHelper.HasKDialog())
|
||||
{
|
||||
return await ProcessExecutor.TryRun("kdialog", "--getopenfilename");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback
|
||||
return Task.FromResult<string?>(null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,34 +181,30 @@ public static class OpenFileDialog
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
private static Task<string[]?> OpenFilesLinux()
|
||||
private static async Task<string[]?> OpenFilesLinux()
|
||||
{
|
||||
if (Gtk.Global.IsSupported)
|
||||
if (await LinuxHelper.HasZenity())
|
||||
{
|
||||
string[]? result;
|
||||
GtkHelper.EnsureInitialized();
|
||||
|
||||
using Gtk.FileChooserNative dlg = new(
|
||||
"Open files", null,
|
||||
Gtk.FileChooserAction.Open, "Open", "Cancel");
|
||||
|
||||
dlg.SelectMultiple = true; // Allow multiple folder selection
|
||||
|
||||
if (dlg.Run() == (int)Gtk.ResponseType.Accept)
|
||||
string? output = await ProcessExecutor.TryRun("zenity", "--file-selection", "--multiple");
|
||||
if (string.IsNullOrEmpty(output))
|
||||
{
|
||||
result = dlg.Filenames;
|
||||
return null; // User canceled the dialog
|
||||
}
|
||||
else
|
||||
return output.Split('|');
|
||||
}
|
||||
else if (await LinuxHelper.HasKDialog())
|
||||
{
|
||||
string? output = await ProcessExecutor.TryRun("kdialog", "--getopenfilenames");
|
||||
if (string.IsNullOrEmpty(output))
|
||||
{
|
||||
result = null; // User canceled the dialog
|
||||
return null; // User canceled the dialog
|
||||
}
|
||||
|
||||
return Task.FromResult(result);
|
||||
return output.Split('\n');
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback
|
||||
return Task.FromResult<string[]?>(null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,33 +80,35 @@ public static class OpenFolderDialog
|
||||
|
||||
// Create the FileOpenDialog object.
|
||||
hr = Windows.CoCreateInstance(&CLSID_FileOpenDialog, null, (uint)CLSCTX.CLSCTX_INPROC_SERVER, &IID_IFileOpenDialog, (void**)&pFileDialog);
|
||||
if (!Windows.SUCCEEDED(hr))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set the options on the dialog.
|
||||
uint dwOptions;
|
||||
pFileDialog->GetOptions(&dwOptions);
|
||||
pFileDialog->SetOptions(dwOptions | (uint)FILEOPENDIALOGOPTIONS.FOS_PICKFOLDERS | (uint)FILEOPENDIALOGOPTIONS.FOS_FORCEFILESYSTEM);
|
||||
|
||||
// Show the dialog
|
||||
hr = pFileDialog->Show(default);
|
||||
if (Windows.SUCCEEDED(hr))
|
||||
{
|
||||
// Set the options on the dialog.
|
||||
uint dwOptions;
|
||||
pFileDialog->GetOptions(&dwOptions);
|
||||
pFileDialog->SetOptions(dwOptions | (uint)FILEOPENDIALOGOPTIONS.FOS_PICKFOLDERS | (uint)FILEOPENDIALOGOPTIONS.FOS_FORCEFILESYSTEM);
|
||||
|
||||
// Show the dialog
|
||||
hr = pFileDialog->Show(default);
|
||||
IShellItem* pItem;
|
||||
hr = pFileDialog->GetResult(&pItem);
|
||||
if (Windows.SUCCEEDED(hr))
|
||||
{
|
||||
IShellItem* pItem;
|
||||
hr = pFileDialog->GetResult(&pItem);
|
||||
char* pszFilePath = null;
|
||||
hr = pItem->GetDisplayName(SIGDN.SIGDN_FILESYSPATH, &pszFilePath);
|
||||
if (Windows.SUCCEEDED(hr))
|
||||
{
|
||||
char* pszFilePath = null;
|
||||
hr = pItem->GetDisplayName(SIGDN.SIGDN_FILESYSPATH, &pszFilePath);
|
||||
if (Windows.SUCCEEDED(hr))
|
||||
{
|
||||
result = new string(pszFilePath);
|
||||
Windows.CoTaskMemFree(pszFilePath);
|
||||
}
|
||||
pItem->Release();
|
||||
result = new string(pszFilePath);
|
||||
Windows.CoTaskMemFree(pszFilePath);
|
||||
}
|
||||
pItem->Release();
|
||||
}
|
||||
pFileDialog->Release();
|
||||
}
|
||||
pFileDialog->Release();
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -118,32 +120,20 @@ public static class OpenFolderDialog
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
private static Task<string?> OpenFolderLinux()
|
||||
private static async Task<string?> OpenFolderLinux()
|
||||
{
|
||||
if (Gtk.Global.IsSupported)
|
||||
if (await LinuxHelper.HasZenity())
|
||||
{
|
||||
string? result;
|
||||
GtkHelper.EnsureInitialized();
|
||||
|
||||
using Gtk.FileChooserNative dlg = new(
|
||||
"Open a folder", null,
|
||||
Gtk.FileChooserAction.SelectFolder, "Open", "Cancel");
|
||||
|
||||
if (dlg.Run() == (int)Gtk.ResponseType.Accept)
|
||||
{
|
||||
result = dlg.Filename;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = null; // User canceled the dialog
|
||||
}
|
||||
|
||||
return Task.FromResult(result);
|
||||
return await ProcessExecutor.TryRun("zenity", "--file-selection", "--directory");
|
||||
}
|
||||
else if (await LinuxHelper.HasKDialog())
|
||||
{
|
||||
return await ProcessExecutor.TryRun("kdialog", "--getexistingdirectory");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback
|
||||
return Task.FromResult<string?>(null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,15 +158,105 @@ public static class OpenFolderDialog
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static async Task<string[]?> OpenFoldersWindows()
|
||||
private static Task<string[]?> OpenFoldersWindows()
|
||||
{
|
||||
// Todo: proper Windows implementation
|
||||
string? path = await OpenFolder();
|
||||
if (string.IsNullOrEmpty(path))
|
||||
TaskCompletionSource<string[]?> tcs = new();
|
||||
|
||||
Thread thread = new(() =>
|
||||
{
|
||||
return null; // User canceled the dialog
|
||||
try
|
||||
{
|
||||
// Run the STA work
|
||||
Debug.Assert(OperatingSystem.IsWindows());
|
||||
string[]? result = OpenFoldersWindowsInternal();
|
||||
|
||||
// Mark task complete
|
||||
tcs.SetResult(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.SetException(ex);
|
||||
}
|
||||
});
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private unsafe static string[]? OpenFoldersWindowsInternal()
|
||||
{
|
||||
HRESULT hr = Windows.CoInitializeEx(null, (uint)COINIT.COINIT_APARTMENTTHREADED);
|
||||
switch (hr.Value)
|
||||
{
|
||||
case S.S_OK:
|
||||
Windows.CoUninitialize();
|
||||
throw new InvalidOperationException("CoInitializeEx failed with S_OK, which should never happen because .NET is supposed to initialize the thread.");
|
||||
case S.S_FALSE:
|
||||
// The thread is already initialized, which is expected.
|
||||
break;
|
||||
case RPC.RPC_E_CHANGED_MODE:
|
||||
// The thread is already initialized with a different mode, which is unexpected.
|
||||
throw new InvalidOperationException("CoInitializeEx failed with RPC_E_CHANGED_MODE, which should never happen because we only call this method in STA threads.");
|
||||
}
|
||||
return [path];
|
||||
|
||||
IFileOpenDialog* pFileDialog = null;
|
||||
|
||||
// Assign the CLSID and IID for the FileOpenDialog.
|
||||
Guid CLSID_FileOpenDialog = new("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7");
|
||||
Guid IID_IFileOpenDialog = new("D57C7288-D4AD-4768-BE02-9D969532D960");
|
||||
|
||||
// Create the FileOpenDialog object.
|
||||
hr = Windows.CoCreateInstance(&CLSID_FileOpenDialog, null, (uint)CLSCTX.CLSCTX_INPROC_SERVER, &IID_IFileOpenDialog, (void**)&pFileDialog);
|
||||
if (!Windows.SUCCEEDED(hr))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set the options on the dialog.
|
||||
uint dwOptions = default;
|
||||
pFileDialog->GetOptions(&dwOptions);
|
||||
pFileDialog->SetOptions(dwOptions | (uint)FILEOPENDIALOGOPTIONS.FOS_PICKFOLDERS | (uint)FILEOPENDIALOGOPTIONS.FOS_ALLOWMULTISELECT | (uint)FILEOPENDIALOGOPTIONS.FOS_FORCEFILESYSTEM);
|
||||
|
||||
string[]? result = null;
|
||||
|
||||
// Show the dialog
|
||||
hr = pFileDialog->Show(default);
|
||||
if (Windows.SUCCEEDED(hr))
|
||||
{
|
||||
IShellItemArray* pItemArray;
|
||||
hr = pFileDialog->GetResults(&pItemArray);
|
||||
if (Windows.SUCCEEDED(hr))
|
||||
{
|
||||
uint itemCount = default;
|
||||
pItemArray->GetCount(&itemCount);
|
||||
if (itemCount > 0)
|
||||
{
|
||||
result = new string[itemCount];
|
||||
for (uint i = 0; i < itemCount; i++)
|
||||
{
|
||||
IShellItem* pItem;
|
||||
hr = pItemArray->GetItemAt(i, &pItem);
|
||||
if (Windows.SUCCEEDED(hr))
|
||||
{
|
||||
char* pszFilePath = null;
|
||||
hr = pItem->GetDisplayName(SIGDN.SIGDN_FILESYSPATH, &pszFilePath);
|
||||
if (Windows.SUCCEEDED(hr))
|
||||
{
|
||||
result[i] = new string(pszFilePath);
|
||||
Windows.CoTaskMemFree(pszFilePath);
|
||||
}
|
||||
pItem->Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
pItemArray->Release();
|
||||
}
|
||||
}
|
||||
pFileDialog->Release();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("macos")]
|
||||
@@ -201,34 +281,27 @@ public static class OpenFolderDialog
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
private static Task<string[]?> OpenFoldersLinux()
|
||||
private static async Task<string[]?> OpenFoldersLinux()
|
||||
{
|
||||
if (Gtk.Global.IsSupported)
|
||||
if (await LinuxHelper.HasZenity())
|
||||
{
|
||||
string[]? result;
|
||||
GtkHelper.EnsureInitialized();
|
||||
|
||||
using Gtk.FileChooserNative dlg = new(
|
||||
"Open folders", null,
|
||||
Gtk.FileChooserAction.SelectFolder, "Open", "Cancel");
|
||||
|
||||
dlg.SelectMultiple = true; // Allow multiple folder selection
|
||||
|
||||
if (dlg.Run() == (int)Gtk.ResponseType.Accept)
|
||||
string? output = await ProcessExecutor.TryRun("zenity", "--file-selection", "--directory", "--multiple");
|
||||
if (string.IsNullOrEmpty(output))
|
||||
{
|
||||
result = dlg.Filenames;
|
||||
return null; // User canceled the dialog
|
||||
}
|
||||
else
|
||||
{
|
||||
result = null; // User canceled the dialog
|
||||
}
|
||||
|
||||
return Task.FromResult(result);
|
||||
return output.Split('|');
|
||||
}
|
||||
else if (await LinuxHelper.HasKDialog())
|
||||
{
|
||||
// KDialog does not support selecting multiple directories, so we can only select one.
|
||||
string? path = await OpenFolderLinux();
|
||||
return string.IsNullOrEmpty(path) ? null : [path];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback
|
||||
return Task.FromResult<string[]?>(null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
|
||||
namespace AssetRipper.NativeDialogs;
|
||||
|
||||
@@ -31,16 +33,35 @@ internal static class ProcessExecutor
|
||||
|
||||
static async Task<string?> TryRunProcess(Process process)
|
||||
{
|
||||
StringBuilder output = new();
|
||||
process.OutputDataReceived += (sender, e) =>
|
||||
{
|
||||
if (e.Data != null)
|
||||
{
|
||||
output.AppendLine(e.Data);
|
||||
}
|
||||
};
|
||||
|
||||
if (!process.Start())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string? path = process.StandardOutput.ReadLine();
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
return process.ExitCode == 0 ? path : null;
|
||||
string result = output.ToString().Trim();
|
||||
|
||||
return process.ExitCode == 0 && result.Length > 0 ? result : null;
|
||||
}
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
public static async Task<bool> HasCommand(string command)
|
||||
{
|
||||
string? result = await TryRun("/bin/bash", "-c", $"command -v {EscapeString(command)}");
|
||||
return !string.IsNullOrEmpty(result);
|
||||
}
|
||||
}
|
||||
@@ -71,32 +71,20 @@ public static class SaveFileDialog
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("linux")]
|
||||
private static Task<string?> SaveFileLinux()
|
||||
private static async Task<string?> SaveFileLinux()
|
||||
{
|
||||
if (Gtk.Global.IsSupported)
|
||||
if (await LinuxHelper.HasZenity())
|
||||
{
|
||||
string? result;
|
||||
GtkHelper.EnsureInitialized();
|
||||
|
||||
using Gtk.FileChooserNative dlg = new(
|
||||
"Save a file", null,
|
||||
Gtk.FileChooserAction.Save, "Save", "Cancel");
|
||||
|
||||
if (dlg.Run() == (int)Gtk.ResponseType.Accept)
|
||||
{
|
||||
result = dlg.Filename;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = null; // User canceled the dialog
|
||||
}
|
||||
|
||||
return Task.FromResult(result);
|
||||
return await ProcessExecutor.TryRun("zenity", "--file-selection", "--save");
|
||||
}
|
||||
else if (await LinuxHelper.HasKDialog())
|
||||
{
|
||||
return await ProcessExecutor.TryRun("kdialog", "--getsavefilename");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback
|
||||
return Task.FromResult<string?>(null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
|
||||
|
||||
<VersionPrefix>1.0.0</VersionPrefix>
|
||||
<VersionPrefix>1.0.1</VersionPrefix>
|
||||
<Copyright>Copyright (c) 2025 ds5678</Copyright>
|
||||
<Authors>ds5678</Authors>
|
||||
<Company>AssetRipper</Company>
|
||||
|
||||
Reference in new issue
Block a user