Skip to content

Commit

Permalink
Handroll gtk bindings, hopefully fixes open/save file dialogs on Linux
Browse files Browse the repository at this point in the history
also remove the OS conditional compilation removals, it makes deving these more annoying and they aren't really needed as trimmer should take them out anyways (since we avoid referencing them using compile time constants)
  • Loading branch information
CasualPokePlayer committed Feb 18, 2024
1 parent d6b58f6 commit 891fa8e
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 62 deletions.
2 changes: 1 addition & 1 deletion GSR.Emu/GSR.Emu.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../GSRCommon.props" />
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.49-beta" PrivateAssets="all" Condition="'$(GSR_WINDOWS)' == 'true'" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.49-beta" PrivateAssets="all" />
<ProjectReference Include="$(ProjectDir)../GSR.Audio/GSR.Audio.csproj" />
<ProjectReference Include="$(ProjectDir)../GSR.Input/GSR.Input.csproj" />
</ItemGroup>
Expand Down
18 changes: 1 addition & 17 deletions GSR.Input/GSR.Input.csproj
Original file line number Diff line number Diff line change
@@ -1,23 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../GSRCommon.props" />
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.49-beta" PrivateAssets="all" Condition="'$(GSR_WINDOWS)' == 'true'" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.49-beta" PrivateAssets="all" />
<ProjectReference Include="$(ProjectDir)../externals/SDL2-CS/SDL2-CS.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(GSR_WINDOWS)' != 'true'">
<Compile Remove="Keyboards/RawKeyInput.cs" />
<Content Include="Keyboards/RawKeyInput.cs" />
</ItemGroup>
<ItemGroup Condition="'$(GSR_OSX)' != 'true'">
<Compile Remove="Keyboards/QuartzImports.cs" />
<Compile Remove="Keyboards/QuartzKeyInput.cs" />
<Content Include="Keyboards/QuartzImports.cs" />
<Content Include="Keyboards/QuartzKeyInput.cs" />
</ItemGroup>
<ItemGroup Condition="'$(GSR_LINUX)' != 'true'">
<Compile Remove="Keyboards/X11Imports.cs" />
<Compile Remove="Keyboards/X11KeyInput.cs" />
<Content Include="Keyboards/X11Imports.cs" />
<Content Include="Keyboards/X11KeyInput.cs" />
</ItemGroup>
</Project>
3 changes: 1 addition & 2 deletions GSR/GSR.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@
<ItemGroup>
<PackageReference Include="SharpCompress" Version="0.36.0" />
<PackageReference Include="System.IO.Hashing" Version="8.0.0" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.49-beta" PrivateAssets="all" Condition="'$(GSR_WINDOWS)' == 'true'" />
<PackageReference Include="GtkSharp" Version="3.24.24.95" Condition="'$(GSR_LINUX)' == 'true'" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.49-beta" PrivateAssets="all" />
<ProjectReference Include="$(ProjectDir)../externals/ImGui.NET/ImGui.NET.csproj" />
<ProjectReference Include="$(ProjectDir)../externals/SDL2-CS/SDL2-CS.csproj" />
<ProjectReference Include="$(ProjectDir)../GSR.Audio/GSR.Audio.csproj" />
Expand Down
223 changes: 223 additions & 0 deletions GSR/Gui/GtkFileChooser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Reflection;
using System.Runtime.InteropServices;

namespace GSR.Gui;

internal sealed partial class GtkFileChooser : IDisposable
{
private const string LIBGTK = "libgtk";

// most of the file chooser apis use only need at least gtk 2.4
private static readonly ImmutableArray<string> _gtkLibraryNames =
[
"libgtk-3.so",
"libgtk-3.so.0",
"libgtk-x11-2.0.so",
"libgtk-x11-2.0.so.0",
];

private static IntPtr GtkImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
{
if (libraryName != LIBGTK)
{
return NativeLibrary.TryLoad(libraryName, assembly, searchPath, out var handle) ? handle : IntPtr.Zero;
}

foreach (var gtkLibraryName in _gtkLibraryNames)
{
if (NativeLibrary.TryLoad(gtkLibraryName, assembly, searchPath, out var gtkHandle))
{
return gtkHandle;
}
}

return IntPtr.Zero;
}

[UnmanagedCallersOnly]
private static void StubGtkLogger(IntPtr log_domain, int log_level, IntPtr message, IntPtr user_data)
{
}

static GtkFileChooser()
{
// currently this assembly (GSR) only needs this special logic for Gtk
// only 1 import resolver may be assigned per assembly
// so this might need to be moved to another class if another library needs special logic
NativeLibrary.SetDllImportResolver(typeof(GtkFileChooser).Assembly, GtkImportResolver);

try
{
IsAvailable = gtk_init_check(IntPtr.Zero, IntPtr.Zero);
// prevent gtk log spam
unsafe
{
_ = g_log_set_default_handler(&StubGtkLogger, IntPtr.Zero);
}
}
catch
{
// g_log_set_default_handler might not be available (requires at least 2.6)
// but most file chooser apis might still be available
}
}

public static readonly bool IsAvailable;

public enum FileChooserAction
{
Open = 0,
Save = 1,
}

public enum Response
{
Accept = -3,
Cancel = -6,
}

private readonly IntPtr _chooser;

public GtkFileChooser(string title, FileChooserAction action)
{
_chooser = gtk_file_chooser_dialog_new(title, IntPtr.Zero, action, IntPtr.Zero);
if (_chooser == IntPtr.Zero)
{
throw new("Failed to create Gtk file chooser!");
}
}

public void AddButton(string buttonText, Response responseId)
{
_ = gtk_dialog_add_button(_chooser, buttonText, responseId);
}

public void AddFilter(string name, IEnumerable<string> patterns)
{
var filter = gtk_file_filter_new();
if (filter == IntPtr.Zero)
{
throw new("Failed to create file filter");
}

gtk_file_filter_set_name(filter, name);
foreach (var pattern in patterns)
{
gtk_file_filter_add_pattern(filter, pattern);
}

gtk_file_chooser_add_filter(_chooser, filter);
}

public void SetCurrentFolder(string folder)
{
_ = gtk_file_chooser_set_current_folder(_chooser, folder);
}

public void SetCurrentName(string name)
{
gtk_file_chooser_set_current_name(_chooser, name);
}

public void SetOverwriteConfirmation(bool doOverwriteConfirmation)
{
try
{
gtk_file_chooser_set_do_overwrite_confirmation(_chooser, doOverwriteConfirmation);
}
catch
{
// might not be available (requires at least 2.8), not critical for usage however
}
}

public Response RunDialog()
{
return gtk_dialog_run(_chooser);
}

public string GetFilename()
{
var filename = gtk_file_chooser_get_filename(_chooser);
try
{
return Marshal.PtrToStringUTF8(filename);
}
finally
{
g_free(filename);
}
}

public void Dispose()
{
while (gtk_events_pending())
{
gtk_main_iteration();
}

gtk_widget_destroy(_chooser);

while (gtk_events_pending())
{
gtk_main_iteration();
}
}

[LibraryImport(LIBGTK)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool gtk_init_check(IntPtr argc, IntPtr argv);

[LibraryImport(LIBGTK)]
private static unsafe partial IntPtr g_log_set_default_handler(delegate* unmanaged<IntPtr, int, IntPtr, IntPtr, void> log_func, IntPtr user_data);

[LibraryImport(LIBGTK, StringMarshalling = StringMarshalling.Utf8)]
private static partial IntPtr gtk_file_chooser_dialog_new(string title, IntPtr parent, FileChooserAction action, IntPtr first_button_text);

[LibraryImport(LIBGTK, StringMarshalling = StringMarshalling.Utf8)]
private static partial IntPtr gtk_dialog_add_button(IntPtr dialog, string button_text, Response response_id);

[LibraryImport(LIBGTK)]
private static partial IntPtr gtk_file_filter_new();

[LibraryImport(LIBGTK, StringMarshalling = StringMarshalling.Utf8)]
private static partial void gtk_file_filter_set_name(IntPtr filter, string name);

[LibraryImport(LIBGTK, StringMarshalling = StringMarshalling.Utf8)]
private static partial void gtk_file_filter_add_pattern(IntPtr filter, string pattern);

[LibraryImport(LIBGTK, StringMarshalling = StringMarshalling.Utf8)]
private static partial void gtk_file_chooser_add_filter(IntPtr chooser, IntPtr filter);

[LibraryImport(LIBGTK, StringMarshalling = StringMarshalling.Utf8)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool gtk_file_chooser_set_current_folder(IntPtr chooser, string filename);

[LibraryImport(LIBGTK, StringMarshalling = StringMarshalling.Utf8)]
private static partial void gtk_file_chooser_set_current_name(IntPtr chooser, string name);

[LibraryImport(LIBGTK)]
private static partial void gtk_file_chooser_set_do_overwrite_confirmation(IntPtr chooser, [MarshalAs(UnmanagedType.Bool)] bool do_overwrite_confirmation);

[LibraryImport(LIBGTK)]
private static partial Response gtk_dialog_run(IntPtr dialog);

[LibraryImport(LIBGTK)]
private static partial IntPtr gtk_file_chooser_get_filename(IntPtr chooser);

[LibraryImport(LIBGTK)]
private static partial void g_free(IntPtr p);

[LibraryImport(LIBGTK)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool gtk_events_pending();

[LibraryImport(LIBGTK)]
private static partial void gtk_main_iteration();

[LibraryImport(LIBGTK)]
private static partial void gtk_widget_destroy(IntPtr widget);
}
36 changes: 14 additions & 22 deletions GSR/Gui/OpenFileDialog.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
#if GSR_OSX || GSR_LINUX
using System.Linq;
#endif

#if GSR_WINDOWS
using Windows.Win32;
Expand All @@ -9,11 +12,6 @@
#if GSR_OSX
using AppKit;
using Foundation;
using System.Linq;
#endif

#if GSR_LINUX
using Gtk;
#endif

namespace GSR.Gui;
Expand Down Expand Up @@ -95,25 +93,19 @@ public static string ShowDialog(string description, string baseDir, IEnumerable<
#if GSR_LINUX
public static string ShowDialog(string description, string baseDir, IEnumerable<string> fileTypes)
{
if (!GtkFileChooser.IsAvailable)
{
return null;
}

try
{
using var dialog = new FileChooserDialog($"Open {description}", null, FileChooserAction.Open, ResponseType.Cancel, "_Cancel", ResponseType.Accept, "_Open");
try
{
using var fileFilter = new FileFilter();
fileFilter.Name = description;
foreach (var fileType in fileTypes)
{
fileFilter.AddPattern($"*{fileType}");
}
dialog.AddFilter(fileFilter);
dialog.SetCurrentFolder(baseDir ?? AppContext.BaseDirectory);
return (ResponseType)dialog.Run() == ResponseType.Accept ? dialog.Filename : null;
}
finally
{
dialog.Destroy();
}
using var dialog = new GtkFileChooser($"Open {description}", GtkFileChooser.FileChooserAction.Open);
dialog.AddButton("_Cancel", GtkFileChooser.Response.Cancel);
dialog.AddButton("_Open", GtkFileChooser.Response.Accept);
dialog.AddFilter(description, fileTypes.Select(ft => $"*{ft}"));
dialog.SetCurrentFolder(baseDir ?? AppContext.BaseDirectory);
return dialog.RunDialog() == GtkFileChooser.Response.Accept ? dialog.GetFilename() : null;
}
catch
{
Expand Down
33 changes: 13 additions & 20 deletions GSR/Gui/SaveFileDialog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@
using Foundation;
#endif

#if GSR_LINUX
using Gtk;
#endif

namespace GSR.Gui;

/// <summary>
Expand Down Expand Up @@ -101,24 +97,21 @@ public static string ShowDialog(string description, string baseDir, string filen
#if GSR_LINUX
public static string ShowDialog(string description, string baseDir, string filename, string ext)
{
if (!GtkFileChooser.IsAvailable)
{
return null;
}

try
{
using var dialog = new FileChooserDialog($"Save {description}", null, FileChooserAction.Save, ResponseType.Cancel, "_Cancel", ResponseType.Accept, "_Save");
try
{
dialog.DoOverwriteConfirmation = true;
using var fileFilter = new FileFilter();
fileFilter.Name = description;
fileFilter.AddPattern($"*{ext}");
dialog.AddFilter(fileFilter);
dialog.SetCurrentFolder(baseDir);
dialog.SetFilename(filename);
return (ResponseType)dialog.Run() == ResponseType.Accept ? dialog.Filename : null;
}
finally
{
dialog.Destroy();
}
using var dialog = new GtkFileChooser($"Save {description}", GtkFileChooser.FileChooserAction.Save);
dialog.AddButton("_Cancel", GtkFileChooser.Response.Cancel);
dialog.AddButton("_Save", GtkFileChooser.Response.Accept);
dialog.AddFilter(description, [ $"*{ext}" ]);
dialog.SetCurrentFolder(baseDir ?? AppContext.BaseDirectory);
dialog.SetCurrentName($"{filename}{ext}");
dialog.SetOverwriteConfirmation(true);
return dialog.RunDialog() == GtkFileChooser.Response.Accept ? dialog.GetFilename() : null;
}
catch
{
Expand Down

0 comments on commit 891fa8e

Please sign in to comment.