diff --git a/BeefLibs/corlib/src/IO/FileDialog.bf b/BeefLibs/corlib/src/IO/FileDialog.bf index 1f81f2bd..6b4893c8 100644 --- a/BeefLibs/corlib/src/IO/FileDialog.bf +++ b/BeefLibs/corlib/src/IO/FileDialog.bf @@ -577,4 +577,306 @@ namespace System.IO } } } +#elif BF_PLATFORM_LINUX +namespace System.IO; + +enum DialogResult +{ + None = 2, + OK = 0, + Cancel = 1 +} + +abstract class CommonDialog +{ + protected Linux.DBus* mBus ~ Linux.SdBusUnref(_); + protected Linux.DBusMsg* mRequest ~ Linux.SdBusMessageUnref(_); + protected Linux.DBusErr mError ~ Linux.SdBusErrorFree(&_); + protected String mTitle ~ delete _; + protected String mInitialDir ~ delete _; + protected String[] mFileNames ~ DeleteContainerAndItems!(_); + protected bool mDone; + + private uint32 mResult; + + + public virtual void Reset() + { + DeleteAndNullify!(mTitle); + DeleteAndNullify!(mInitialDir); + DeleteContainerAndItems!(mFileNames); + mFileNames = null; + } + + public StringView Title + { + set + { + String.NewOrSet!(mTitle, value); + } + + get + { + return mTitle; + } + } + + public Result ShowDialog(INativeWindow owner = null) + { + TryC!(Linux.SdBusOpenUser(&mBus)); // Maybe keep the bus open while the program is running ? + + Linux.DBusMsg* call = ?; + TryC!(Linux.SdBusNewMethodCall( + mBus, + &call, + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.FileChooser", + Method)); + + Linux.SdBusMessageAppend(call, "ss", "", Title.ToScopeCStr!()); //TODO : set parent_window to X11/Wayland handle + Linux.SdBusMessageOpenContainer(call, .Array, "{sv}"); + if(mInitialDir != null) + { + Linux.SdBusMessageOpenContainer(call, .DictEntry, "sv"); + Linux.SdBusMessageAppendBasic(call, .String, "current_folder"); + Linux.SdBusMessageOpenContainer(call, .Variant, "ay"); + Linux.SdBusMessageAppendArray(call, .Byte, mInitialDir.CStr(), (.)mInitialDir.Length); + Linux.SdBusMessageCloseContainer(call); + Linux.SdBusMessageCloseContainer(call); + } + AddOptions(call); + Linux.SdBusMessageCloseContainer(call); + TryC!(Linux.SdBusCall(mBus, call, uint32.MaxValue, &mError, &mRequest)); // TODO : change timeout + + Linux.SdBusMessageUnref(call); + + char8* path = ?; + TryC!(Linux.SdBusMessageRead(mRequest, "o", &path)); + TryC!(Linux.SdBusMatchSignal(mBus, + null, + null, + path, + "org.freedesktop.portal.Request", + "Response", + => ParseResponse, + Internal.UnsafeCastToPtr(this) + )); + + while(!mDone) + { + Linux.DBusMsg* m = ?; + TryC!(Linux.SdBusWait(mBus, uint64.MaxValue)); + TryC!(Linux.SdBusProcess(mBus, &m)); + Linux.SdBusMessageUnref(m); + } + + return (DialogResult)mResult; + } + + private static int32 ParseResponse(Linux.DBusMsg* response, void* ptr, Linux.DBusErr* error) + { + Self dia = (.)Internal.UnsafeCastToObject(ptr); + + char8* key = ?; + + Linux.SdBusMessageReadBasic(response, .UInt32, &dia.mResult); + Linux.SdBusMessageEnterContainer(response, .Array, "{sv}"); + while(Linux.SdBusMessagePeekType(response, null, null) != 0) + { + Linux.SdBusMessageEnterContainer(response, .DictEntry, "sv"); + Linux.SdBusMessageReadBasic(response, .String, &key); + switch(StringView(key)) + { + case "uris": + List uris = scope .(); + + Linux.SdBusMessageEnterContainer(response, .Variant, "as"); + Linux.SdBusMessageEnterContainer(response, .Array, "s"); + while(Linux.SdBusMessagePeekType(response, null, null) != 0) + { + char8* uri = ?; + Linux.SdBusMessageReadBasic(response, .String, &uri); + uris.Add(new .(StringView(uri+7))); // Removing the "file://" prefix + } + Linux.SdBusMessageExitContainer(response); + Linux.SdBusMessageExitContainer(response); + + dia.mFileNames = new .[uris.Count]; + uris.CopyTo(dia.mFileNames); + default: + Linux.SdBusMessageSkip(response, "v"); + } + Linux.SdBusMessageExitContainer(response); + } + Linux.SdBusMessageExitContainer(response); + + dia.mDone = true; + return 0; + } + + protected abstract char8* Method { get; } + protected abstract void AddOptions(Linux.DBusMsg* m); +} + +public abstract class FileDialog : CommonDialog +{ + protected int32 mOptions; + private String mFilter ~ delete _; + + public this() + { + Reset(); + } + + public override void Reset() + { + base.Reset(); + DeleteAndNullify!(mFilter); + } + + public StringView InitialDirectory + { + set + { + String.NewOrSet!(mInitialDir, value); + } + + get + { + return mInitialDir; + } + } + + public String[] FileNames + { + get + { + return mFileNames; + } + } + + public StringView FileName + { + set + { + if (mFileNames == null) + { + mFileNames = new String[](new String(value)); + } + } + } + + public bool Multiselect + { + get + { + return GetOption(512); + } + + set + { + SetOption(512, value); + } + } + + public bool ValidateNames // Unused kept for compatibility + { + get + { + return !GetOption(256); + } + + set + { + SetOption(256, !value); + } + } + + public StringView DefaultExt { get; set; } // Unused kept for compatibility + + public void GetFilter(String outFilter) + { + if (mFilter != null) + outFilter.Append(mFilter); + } + + public Result SetFilter(StringView value) + { + String useValue = scope String(value); + if (useValue != null && useValue.Length > 0) + { + var formats = String.StackSplit!(useValue, '|'); + if (formats == null || formats.Count % 2 != 0) + { + return .Err; + } + /// + /*String[] formats = value.Split('|'); + if (formats == null || formats.Length % 2 != 0) + { + throw new ArgumentException(SR.GetString(SR.FileDialogInvalidFilter)); + }*/ + String.NewOrSet!(mFilter, useValue); + } + else + { + useValue = null; + DeleteAndNullify!(mFilter); + } + + return .Ok; + } + + protected bool GetOption(int32 option) + { + return (mOptions & option) != 0; + } + + protected void SetOption(int32 option, bool value) + { + if (value) + { + mOptions |= option; + } + else + { + mOptions &= ~option; + } + } + + protected override void AddOptions(Linux.DBusMsg* m) + { + if(Multiselect) + { + Linux.SdBusMessageOpenContainer(m, .DictEntry, "sv"); + Linux.SdBusMessageAppend(m, "sv", "multiple", "b", 1); + Linux.SdBusMessageCloseContainer(m); + } + + if(mFilter != null) + { + Linux.SdBusMessageOpenContainer(m, .DictEntry, "sv"); + Linux.SdBusMessageAppendBasic(m, .String, "filters"); + Linux.SdBusMessageOpenContainer(m, .Variant, "a(sa(us))"); + Linux.SdBusMessageOpenContainer(m, .Array, "(sa(us))"); + for(let filter in mFilter.Split('|')) + { + Linux.SdBusMessageOpenContainer(m, .Struct, "sa(us)"); + Linux.SdBusMessageAppendBasic(m, .String, filter.ToScopeCStr!()); + Linux.SdBusMessageOpenContainer(m, .Array, "(us)"); + @filter.MoveNext(); + for(let ext in @filter.Current.Split(';')) + { + Linux.SdBusMessageAppend(m, "(us)", 0, ext.ToScopeCStr!()); + } + Linux.SdBusMessageCloseContainer(m); + Linux.SdBusMessageCloseContainer(m); + } + Linux.SdBusMessageCloseContainer(m); + Linux.SdBusMessageCloseContainer(m); + Linux.SdBusMessageCloseContainer(m); + } + } +} #endif \ No newline at end of file diff --git a/BeefLibs/corlib/src/IO/FolderBrowserDialog.bf b/BeefLibs/corlib/src/IO/FolderBrowserDialog.bf index 1a73b017..7c8e6d04 100644 --- a/BeefLibs/corlib/src/IO/FolderBrowserDialog.bf +++ b/BeefLibs/corlib/src/IO/FolderBrowserDialog.bf @@ -221,4 +221,41 @@ namespace System.IO } } } +#elif BF_PLATFORM_LINUX +namespace System.IO; + +public class FolderBrowserDialog : CommonDialog +{ + public enum FolderKind + { + Open, + Save + } + + public this(FolderKind kind = .Open) + { + // Only OpenFile allows directory selection so FolderKind isn't stored + Reset(); + } + + public StringView SelectedPath + { + get + { + return mDone ? (mFileNames == null) ? "" : mFileNames[0] : mInitialDir; //Response gets stored in mFileNames + } + set + { + mInitialDir.Set(value); + } + } + + protected override char8* Method => "OpenFile"; + protected override void AddOptions(Linux.DBusMsg* m) + { + Linux.SdBusMessageOpenContainer(m, .DictEntry, "sv"); + Linux.SdBusMessageAppend(m, "sv", "directory", "b", 1); + Linux.SdBusMessageCloseContainer(m); + } +} #endif \ No newline at end of file diff --git a/BeefLibs/corlib/src/IO/OpenFileDialog.bf b/BeefLibs/corlib/src/IO/OpenFileDialog.bf index c7c79d11..3dd67676 100644 --- a/BeefLibs/corlib/src/IO/OpenFileDialog.bf +++ b/BeefLibs/corlib/src/IO/OpenFileDialog.bf @@ -173,4 +173,23 @@ namespace System.IO } } +#elif BF_PLATFORM_LINUX +namespace System.IO; + +public class OpenFileDialog : FileDialog +{ + public bool ShowReadOnly // Unused kept for compatibility + { + get + { + return !GetOption(4); + } + set + { + SetOption(4, !value); + } + } + + protected override char8* Method => "OpenFile"; +} #endif \ No newline at end of file diff --git a/BeefLibs/corlib/src/IO/SaveFileDialog.bf b/BeefLibs/corlib/src/IO/SaveFileDialog.bf index 58e1bca9..6e79461a 100644 --- a/BeefLibs/corlib/src/IO/SaveFileDialog.bf +++ b/BeefLibs/corlib/src/IO/SaveFileDialog.bf @@ -139,8 +139,36 @@ namespace System.IO } } -#else +#elif BF_PLATFORM_LINUX +namespace System.IO; +public class SaveFileDialog : FileDialog +{ + public virtual bool OverwritePrompt // Unused kept for compatibility + { + get + { + return GetOption(2); + } + + set + { + SetOption(2, value); + } + } + + protected override char8* Method => "SaveFile"; + protected override void AddOptions(Linux.DBusMsg* m) + { + if(mFileNames != null) + { + Linux.SdBusMessageOpenContainer(m, .DictEntry, "sv"); + Linux.SdBusMessageAppend(m, "sv", "current_name", "s", mFileNames[0].CStr()); + Linux.SdBusMessageCloseContainer(m); + } + } +} +#else namespace System.IO { [Error("This class is only available on Windows")] @@ -149,5 +177,4 @@ namespace System.IO } } - #endif \ No newline at end of file diff --git a/BeefLibs/corlib/src/Linux.bf b/BeefLibs/corlib/src/Linux.bf new file mode 100644 index 00000000..fe07c556 --- /dev/null +++ b/BeefLibs/corlib/src/Linux.bf @@ -0,0 +1,103 @@ +#if BF_PLATFORM_LINUX +using System.Interop; +namespace System; + +class Linux +{ + public struct DBus; + public struct DBusMsg; + public struct DBusSlot; + + public enum DBusType : char8 + { + Invalid = 0, + Byte = 'y', + Bool = 'b', + Int16 = 'n', + UInt16 = 'q', + Int32 = 'i', + UInt32 = 'u', + Int64 = 'x', + UInt64 = 't', + Double = 'd', + String = 's', + ObjectPath = 'o', + Signature = 'g', + UnixFD = 'h', + Array = 'a', + Variant = 'v', + Struct = 'r', /* not actually used in signatures */ + StructBegin = '(', + StructEnd = ')', + DictEntry = 'e', /* not actually used in signatures */ + DictEntryBegin = '{', + DictEntryEnd = '}' + } + + [CRepr] + public struct DBusErr + { + public char8* name; + public char8* message; + public c_int _need_free; + } + + public typealias DBusMsgHandler = function int32(DBusMsg *m, void *userdata, DBusErr *ret_error); + + [Import("libsystemd.so"), LinkName("sd_bus_open_user")] + public static extern c_int SdBusOpenUser(DBus **ret); + [Import("libsystemd.so"), LinkName("sd_bus_open_system")] + public static extern c_int SdBusOpenSystem(DBus **ret); + [Import("libsystemd.so"), LinkName("sd_bus_unref")] + public static extern DBus* SdBusUnref(DBus *bus); + [Import("libsystemd.so"), LinkName("sd_bus_call")] + public static extern c_int SdBusCall(DBus *bus, DBusMsg *m, uint64 usec, DBusErr *ret_error, DBusMsg **reply); + [Import("libsystemd.so"), LinkName("sd_bus_process")] + public static extern c_int SdBusProcess(DBus *bus, DBusMsg **r); + [Import("libsystemd.so"), LinkName("sd_bus_wait")] + public static extern c_int SdBusWait(DBus *bus, uint64 timeout_usec); + + [Import("libsystemd.so"), LinkName("sd_bus_message_new_method_call")] + public static extern c_int SdBusNewMethodCall(DBus *bus, DBusMsg **m, char8 *destination, char8 *path, char8 *iface, char8 *member); + [Import("libsystemd.so"), LinkName("sd_bus_message_unref")] + public static extern DBusMsg* SdBusMessageUnref(DBusMsg *m); + [Import("libsystemd.so"), LinkName("sd_bus_message_append")] + public static extern c_int SdBusMessageAppend(DBusMsg *m, char8 *types, ...); + [Import("libsystemd.so"), LinkName("sd_bus_message_append_basic")] + public static extern c_int SdBusMessageAppendBasic(DBusMsg *m, DBusType type, void *p); + [Import("libsystemd.so"), LinkName("sd_bus_message_append_array")] + public static extern c_int SdBusMessageAppendArray(DBusMsg *m, DBusType type, void *ptr, c_size size); + [Import("libsystemd.so"), LinkName("sd_bus_message_open_container")] + public static extern c_int SdBusMessageOpenContainer(DBusMsg *m, DBusType type, char8 *contents); + [Import("libsystemd.so"), LinkName("sd_bus_message_close_container")] + public static extern c_int SdBusMessageCloseContainer(DBusMsg *m); + + [Import("libsystemd.so"), LinkName("sd_bus_message_read")] + public static extern c_int SdBusMessageRead(DBusMsg *m, char8 *types, ...); + [Import("libsystemd.so"), LinkName("sd_bus_message_read_basic")] + public static extern c_int SdBusMessageReadBasic(DBusMsg *m, DBusType type, void *p); + [Import("libsystemd.so"), LinkName("sd_bus_message_read_array")] + public static extern c_int SdBusMessageReadArray(DBusMsg *m, DBusType type, void **ptr, c_size *size); + [Import("libsystemd.so"), LinkName("sd_bus_message_skip")] + public static extern c_int SdBusMessageSkip(DBusMsg *m, char8 *types); + [Import("libsystemd.so"), LinkName("sd_bus_message_enter_container")] + public static extern c_int SdBusMessageEnterContainer(DBusMsg *m, DBusType type, char8 *contents); + [Import("libsystemd.so"), LinkName("sd_bus_message_exit_container")] + public static extern c_int SdBusMessageExitContainer(DBusMsg *m); + [Import("libsystemd.so"), LinkName("sd_bus_message_peek_type")] + public static extern c_int SdBusMessagePeekType(DBusMsg *m, char8 *type, char8 **contents); + + [Import("libsystemd.so"), LinkName("sd_bus_call_method")] + public static extern c_int SdBusCallMethod(DBus *bus, char8 *destination, char8 *path, char8 *iface, char8 *member, DBusErr *ret_error, DBusMsg **reply, char8 *types, ...); + [Import("libsystemd.so"), LinkName("sd_bus_match_signal")] + public static extern c_int SdBusMatchSignal(DBus *bus, DBusSlot **ret, char8 *sender, char8 *path, char8 *iface, char8 *member, DBusMsgHandler callback, void *userdata); + + [Import("libsystemd.so"), LinkName("sd_bus_error_free")] + public static extern void SdBusErrorFree(DBusErr *e); +} + +static +{ + public static mixin TryC(int result) { if(result < 0) return .Err; } +} +#endif \ No newline at end of file