WIP: Add support for open and save file dialogs
parent
69169f7ca2
commit
18d2c2f0a9
@ -0,0 +1,55 @@
|
||||
package file_dialog
|
||||
|
||||
// Save file dialog function
|
||||
show_save_file_dialog :: proc(
|
||||
title: string = "Save File",
|
||||
filters: []File_Filter = {},
|
||||
default_filename: string = "",
|
||||
default_directory: string = ""
|
||||
) -> Result {
|
||||
when ODIN_OS == .Windows {
|
||||
return show_save_file_dialog_windows(title, filters, default_filename, default_directory)
|
||||
} else when ODIN_OS == .Linux {
|
||||
return show_save_file_dialog_linux(title, filters, default_filename, default_directory)
|
||||
} else {
|
||||
return {
|
||||
success = false,
|
||||
error_message = "Unsupported platform",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main cross-platform function
|
||||
show_open_file_dialog :: proc(
|
||||
title: string = "Open File",
|
||||
filters: []File_Filter = {},
|
||||
allow_multiple: bool = false
|
||||
) -> Result {
|
||||
when ODIN_OS == .Windows {
|
||||
return show_open_file_dialog_windows(title, filters, allow_multiple)
|
||||
} else when ODIN_OS == .Linux {
|
||||
return show_open_file_dialog_linux(title, filters, allow_multiple)
|
||||
} else {
|
||||
return {
|
||||
success = false,
|
||||
error_message = "Unsupported platform",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience function to clean up results
|
||||
destroy_result :: proc(result: ^Result) {
|
||||
if result.file_path != "" {
|
||||
delete(result.file_path)
|
||||
}
|
||||
if len(result.file_paths) > 0 {
|
||||
for path in result.file_paths {
|
||||
delete(path)
|
||||
}
|
||||
delete(result.file_paths)
|
||||
}
|
||||
if result.error_message != "" {
|
||||
delete(result.error_message)
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,77 @@
|
||||
#+build linux
|
||||
#+feature dynamic-literals
|
||||
package file_dialog
|
||||
|
||||
import "core:log"
|
||||
import "core:strings"
|
||||
import os2 "core:os/os2"
|
||||
|
||||
SAVE_FILE_COMMANDS :: [][]string{
|
||||
{"yad", "--file", "--save"}
|
||||
}
|
||||
|
||||
OPEN_SINGLE_FILE_COMMANDS :: [][]string{
|
||||
{"yad", "--file"}
|
||||
}
|
||||
|
||||
show_save_file_dialog_linux :: proc(title: string, filters: []File_Filter, default_filename: string = "", default_directory: string = "") -> (result: Result) {
|
||||
|
||||
result.success = false
|
||||
process_desc: os2.Process_Desc
|
||||
for command in SAVE_FILE_COMMANDS {
|
||||
log.debug("attempting to use", command[0], "to get file path")
|
||||
process_desc.command = command
|
||||
|
||||
state, stdout_bytes, stderr_bytes, err := os2.process_exec(process_desc, context.allocator)
|
||||
if err == .Not_Exist {
|
||||
log.debug("couldn't find", command[0])
|
||||
continue
|
||||
} else if err != nil {
|
||||
log.error("error whilst getting filename: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
stdout := string(stdout_bytes)
|
||||
|
||||
if len(stdout) == 0 do result.file_path = strings.clone("")
|
||||
else if stdout[len(stdout)-1] == '\n' do result.file_path = strings.clone(stdout[:len(stdout)-1])
|
||||
else do result.file_path = strings.clone(string(stdout))
|
||||
|
||||
result.success = true
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
show_open_file_dialog_linux :: proc(
|
||||
title: string,
|
||||
filters: []File_Filter = {},
|
||||
allow_multiple: bool = false
|
||||
) -> (result: Result) {
|
||||
result.success = false
|
||||
process_desc: os2.Process_Desc
|
||||
for command in OPEN_SINGLE_FILE_COMMANDS {
|
||||
log.debug("attempting to use", command[0], "to get file path")
|
||||
process_desc.command = command
|
||||
|
||||
state, stdout_bytes, stderr_bytes, err := os2.process_exec(process_desc, context.allocator)
|
||||
if err == .Not_Exist {
|
||||
log.debug("couldn't find", command[0])
|
||||
continue
|
||||
} else if err != nil {
|
||||
log.error("error whilst getting filename: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
stdout := string(stdout_bytes)
|
||||
|
||||
if len(stdout) == 0 do result.file_path = strings.clone("")
|
||||
else if stdout[len(stdout)-1] == '\n' do result.file_path = strings.clone(stdout[:len(stdout)-1])
|
||||
else do result.file_path = strings.clone(string(stdout))
|
||||
|
||||
result.success = true
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
package file_dialog
|
||||
|
||||
// Cross-platform file dialog result
|
||||
Result :: struct {
|
||||
success: bool,
|
||||
file_path: string, // For single selection (backward compatibility)
|
||||
file_paths: []string, // For multiple selection
|
||||
error_message: string,
|
||||
}
|
||||
|
||||
// File filter for dialog
|
||||
File_Filter :: struct {
|
||||
name: string,
|
||||
pattern: string,
|
||||
}
|
||||
@ -0,0 +1,267 @@
|
||||
#+build windows
|
||||
package file_dialog
|
||||
|
||||
import "core:strings"
|
||||
import "core:c"
|
||||
import "core:fmt"
|
||||
|
||||
foreign import comdlg32 "system:comdlg32.lib"
|
||||
foreign import ole32 "system:ole32.lib"
|
||||
foreign import shell32 "system:shell32.lib"
|
||||
|
||||
OPENFILENAME :: struct {
|
||||
lStructSize: c.ulong,
|
||||
hwndOwner: rawptr,
|
||||
hInstance: rawptr,
|
||||
lpstrFilter: cstring,
|
||||
lpstrCustomFilter: cstring,
|
||||
nMaxCustFilter: c.ulong,
|
||||
nFilterIndex: c.ulong,
|
||||
lpstrFile: cstring,
|
||||
nMaxFile: c.ulong,
|
||||
lpstrFileTitle: cstring,
|
||||
nMaxFileTitle: c.ulong,
|
||||
lpstrInitialDir: cstring,
|
||||
lpstrTitle: cstring,
|
||||
Flags: c.ulong,
|
||||
nFileOffset: c.ushort,
|
||||
nFileExtension: c.ushort,
|
||||
lpstrDefExt: cstring,
|
||||
lCustData: c.ulong_ptr,
|
||||
lpfnHook: rawptr,
|
||||
lpTemplateName: cstring,
|
||||
pvReserved: rawptr,
|
||||
dwReserved: c.ulong,
|
||||
FlagsEx: c.ulong,
|
||||
}
|
||||
|
||||
OFN_PATHMUSTEXIST :: 0x00000800
|
||||
OFN_FILEMUSTEXIST :: 0x00001000
|
||||
OFN_HIDEREADONLY :: 0x00000004
|
||||
OFN_ALLOWMULTISELECT :: 0x00000200
|
||||
|
||||
@(default_calling_convention="stdcall")
|
||||
foreign comdlg32 {
|
||||
GetOpenFileNameA :: proc(lpofn: ^OPENFILENAME) -> c.int ---
|
||||
GetSaveFileNameA :: proc(lpofn: ^OPENFILENAME) -> c.int ---
|
||||
}
|
||||
|
||||
show_open_file_dialog_windows :: proc(title: string, filters: []File_Filter, allow_multiple: bool) -> File_Dialog_Result {
|
||||
MAX_PATH :: 260
|
||||
BUFFER_SIZE :: 32768 // Larger buffer for multiple files
|
||||
file_buffer: [BUFFER_SIZE]c.char
|
||||
|
||||
// Build filter string (format: "Description\0*.ext\0Description2\0*.ext2\0\0")
|
||||
filter_str := strings.builder_make()
|
||||
defer strings.builder_destroy(&filter_str)
|
||||
|
||||
if len(filters) > 0 {
|
||||
for filter in filters {
|
||||
strings.write_string(&filter_str, filter.name)
|
||||
strings.write_byte(&filter_str, 0)
|
||||
strings.write_string(&filter_str, filter.pattern)
|
||||
strings.write_byte(&filter_str, 0)
|
||||
}
|
||||
} else {
|
||||
strings.write_string(&filter_str, "All Files")
|
||||
strings.write_byte(&filter_str, 0)
|
||||
strings.write_string(&filter_str, "*.*")
|
||||
strings.write_byte(&filter_str, 0)
|
||||
}
|
||||
strings.write_byte(&filter_str, 0) // Double null terminator
|
||||
|
||||
filter_cstr := strings.clone_to_cstring(strings.to_string(filter_str))
|
||||
defer delete(filter_cstr)
|
||||
|
||||
title_cstr := strings.clone_to_cstring(title)
|
||||
defer delete(title_cstr)
|
||||
|
||||
flags := OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY
|
||||
if allow_multiple {
|
||||
flags |= OFN_ALLOWMULTISELECT
|
||||
}
|
||||
|
||||
ofn := OPENFILENAME{
|
||||
lStructSize = size_of(OPENFILENAME),
|
||||
lpstrFile = &file_buffer[0],
|
||||
nMaxFile = BUFFER_SIZE,
|
||||
lpstrFilter = filter_cstr,
|
||||
nFilterIndex = 1,
|
||||
lpstrTitle = title_cstr,
|
||||
Flags = flags,
|
||||
}
|
||||
|
||||
if GetOpenFileNameA(&ofn) != 0 {
|
||||
buffer_str := string(cstring(&file_buffer[0]))
|
||||
|
||||
if allow_multiple {
|
||||
// Parse multiple files format: "directory\0file1\0file2\0\0"
|
||||
parts := strings.split(buffer_str, "\x00")
|
||||
defer delete(parts)
|
||||
|
||||
if len(parts) > 1 && parts[1] != "" {
|
||||
// Multiple files selected
|
||||
directory := parts[0]
|
||||
file_paths := make([]string, len(parts) - 1)
|
||||
|
||||
for i := 1; i < len(parts); i+=1 {
|
||||
if parts[i] != "" {
|
||||
full_path := strings.concatenate({directory, "\\", parts[i]})
|
||||
file_paths[i-1] = full_path
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success = true,
|
||||
file_path = file_paths[0], // First file for compatibility
|
||||
file_paths = file_paths,
|
||||
}
|
||||
} else {
|
||||
// Single file selected (even with multiple selection enabled)
|
||||
return {
|
||||
success = true,
|
||||
file_path = strings.clone(buffer_str),
|
||||
file_paths = {strings.clone(buffer_str)},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Single file selection
|
||||
return {
|
||||
success = true,
|
||||
file_path = strings.clone(buffer_str),
|
||||
file_paths = {strings.clone(buffer_str)},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success = false,
|
||||
error_message = "User cancelled or dialog failed",
|
||||
}
|
||||
}
|
||||
|
||||
show_save_file_dialog_macos :: proc(title: string, filters: []File_Filter, default_filename: string = "", default_directory: string = "") -> File_Dialog_Result {
|
||||
panel := NSSavePanel_savePanel()
|
||||
|
||||
title_nsstr := NSStringFromCString(strings.clone_to_cstring(title))
|
||||
NSSavePanel_setTitle(panel, title_nsstr)
|
||||
|
||||
// Set default filename if provided
|
||||
if default_filename != "" {
|
||||
filename_nsstr := NSStringFromCString(strings.clone_to_cstring(default_filename))
|
||||
NSSavePanel_setNameFieldStringValue(panel, filename_nsstr)
|
||||
}
|
||||
|
||||
// Set default directory if provided
|
||||
if default_directory != "" {
|
||||
dir_nsstr := NSStringFromCString(strings.clone_to_cstring(default_directory))
|
||||
dir_url := NSURL_fileURLWithPath(dir_nsstr)
|
||||
if dir_url != nil {
|
||||
NSSavePanel_setDirectoryURL(panel, dir_url)
|
||||
}
|
||||
}
|
||||
|
||||
// Note: File filters in NSSavePanel require more complex setup
|
||||
// This is a simplified version - full implementation would use UTType
|
||||
|
||||
response := NSSavePanel_runModal(panel)
|
||||
|
||||
if response == NSModalResponseOK {
|
||||
url := NSSavePanel_URL(panel)
|
||||
if url != nil {
|
||||
path_nsstr := NSURL_path(url)
|
||||
path_cstr := NSString_UTF8String(path_nsstr)
|
||||
file_path := strings.clone(string(path_cstr))
|
||||
return {
|
||||
success = true,
|
||||
file_path = file_path,
|
||||
file_paths = {file_path},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success = false,
|
||||
error_message = "User cancelled or dialog failed",
|
||||
}
|
||||
}
|
||||
|
||||
show_save_file_dialog_windows :: proc(
|
||||
title: string,
|
||||
filters: []File_Filter,
|
||||
default_filename: string = "",
|
||||
default_directory: string = ""
|
||||
) -> File_Dialog_Result {
|
||||
MAX_PATH :: 260
|
||||
file_buffer: [MAX_PATH]c.char
|
||||
|
||||
// Set default filename if provided
|
||||
if default_filename != "" {
|
||||
filename_cstr := strings.clone_to_cstring(default_filename)
|
||||
defer delete(filename_cstr)
|
||||
|
||||
// Copy to buffer (ensure null termination)
|
||||
filename_len := min(len(default_filename), MAX_PATH - 1)
|
||||
for i in 0..<filename_len {
|
||||
file_buffer[i] = c.char(default_filename[i])
|
||||
}
|
||||
file_buffer[filename_len] = 0
|
||||
}
|
||||
|
||||
// Build filter string
|
||||
filter_str := strings.builder_make()
|
||||
defer strings.builder_destroy(&filter_str)
|
||||
|
||||
if len(filters) > 0 {
|
||||
for filter in filters {
|
||||
strings.write_string(&filter_str, filter.name)
|
||||
strings.write_byte(&filter_str, 0)
|
||||
strings.write_string(&filter_str, filter.pattern)
|
||||
strings.write_byte(&filter_str, 0)
|
||||
}
|
||||
} else {
|
||||
strings.write_string(&filter_str, "All Files")
|
||||
strings.write_byte(&filter_str, 0)
|
||||
strings.write_string(&filter_str, "*.*")
|
||||
strings.write_byte(&filter_str, 0)
|
||||
}
|
||||
strings.write_byte(&filter_str, 0) // Double null terminator
|
||||
|
||||
filter_cstr := strings.clone_to_cstring(strings.to_string(filter_str))
|
||||
defer delete(filter_cstr)
|
||||
|
||||
title_cstr := strings.clone_to_cstring(title)
|
||||
defer delete(title_cstr)
|
||||
|
||||
// Default directory (optional)
|
||||
dir_cstr: cstring = nil
|
||||
if default_directory != "" {
|
||||
dir_cstr = strings.clone_to_cstring(default_directory)
|
||||
defer delete(dir_cstr)
|
||||
}
|
||||
|
||||
ofn := OPENFILENAME{
|
||||
lStructSize = size_of(OPENFILENAME),
|
||||
lpstrFile = &file_buffer[0],
|
||||
nMaxFile = MAX_PATH,
|
||||
lpstrFilter = filter_cstr,
|
||||
nFilterIndex = 1,
|
||||
lpstrTitle = title_cstr,
|
||||
lpstrInitialDir = dir_cstr,
|
||||
Flags = OFN_PATHMUSTEXIST | OFN_HIDEREADONLY,
|
||||
}
|
||||
|
||||
if GetSaveFileNameA(&ofn) != 0 {
|
||||
return {
|
||||
success = true,
|
||||
file_path = strings.clone(string(cstring(&file_buffer[0]))),
|
||||
file_paths = {strings.clone(string(cstring(&file_buffer[0])))},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success = false,
|
||||
error_message = "User cancelled or dialog failed",
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue