changeset 2355:94cf5cc89227

Add an option to use the system file picker on Linux and Windows
author Michael Pavone <pavone@retrodev.com>
date Sat, 21 Oct 2023 19:22:01 -0700
parents a773b8f09292
children 12d594e69e04
files Makefile default.cfg nuklear_ui/blastem_nuklear.c nuklear_ui/filechooser.h nuklear_ui/filechooser_gtk.c nuklear_ui/filechooser_null.c nuklear_ui/filechooser_win.c
diffstat 7 files changed, 318 insertions(+), 30 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Thu Oct 19 23:27:57 2023 -0700
+++ b/Makefile	Sat Oct 21 19:22:01 2023 -0700
@@ -15,6 +15,7 @@
 MEM:=mem_win.o
 TERMINAL:=terminal_win.o
 FONT:=nuklear_ui/font_win.o
+CHOOSER:=nuklear_ui/filechooser_win.o
 NET:=net_win.o
 EXE:=.exe
 SO:=dll
@@ -32,7 +33,7 @@
 endif
 GLEW32S_LIB:=$(GLEW_PREFIX)/lib/Release/$(GLUDIR)/glew32s.lib
 CFLAGS:=-std=gnu99 -Wreturn-type -Werror=return-type -Werror=implicit-function-declaration -Wpointer-arith -Werror=pointer-arith
-LDFLAGS:=-lm -lmingw32 -lws2_32 -mwindows
+LDFLAGS:=-lm -lmingw32 -lws2_32 -lcomdlg32 -mwindows
 ifneq ($(MAKECMDGOALS),libblastem.dll)
 CFLAGS+= -I"$(SDL2_PREFIX)/include/SDL2" -I"$(GLEW_PREFIX)/include" -DGLEW_STATIC
 LDFLAGS+= $(GLEW32S_LIB) -L"$(SDL2_PREFIX)/lib" -lSDL2main -lSDL2 -lopengl32 -lglu32
@@ -52,6 +53,7 @@
 ifeq ($(OS),Darwin)
 LIBS=sdl2 glew
 FONT:=nuklear_ui/font_mac.o
+CHOOSER:=nuklear_ui/filechooser_null.o
 SO:=dylib
 else
 SO:=so
@@ -71,6 +73,19 @@
 endif #USE_GLES
 endif #USE_FBDEV
 FONT:=nuklear_ui/font.o
+CHOOSER:=nuklear_ui/filechooser_gtk.o
+GTKFLAGS:=$(shell pkg-config --cflags gtk+-3.0 2>/dev/null)
+ifeq ($(GTKFLAGS),)
+GTKFLAGS:=$(shell pkg-config --cflags gtk+-2.0 2>/dev/null)
+ifeq ($(GTKFLAGS),)
+CHOOSER:=nuklear_ui/filechooser_null.o
+endif
+endif
+ifeq ($(GTKFLAGS),)
+else
+EXTRA_NUKLEAR_LDFLAGS:=-ldl
+endif
+CFLAGS+= $(GTKFLAGS)
 endif #Darwin
 
 ifdef HOST_ZLIB
@@ -84,23 +99,24 @@
 #This should really be based on whether or not the C compiler is clang rather than based on the OS
 CFLAGS+= -Wno-logical-op-parentheses
 endif
+
 ifdef PORTABLE
 ifdef USE_GLES
 ifndef GLES_LIB
 GLES_LIB:=$(shell pkg-config --libs glesv2)
 endif
 LDFLAGS:=-lm $(GLES_LIB)
-else
+else #USE_GLES
 CFLAGS+= -DGLEW_STATIC -Iglew/include
 LDFLAGS:=-lm glew/lib/libGLEW.a -lEGL
-endif
+endif #USE_GLES
 
 ifeq ($(OS),Darwin)
 SDL_INCLUDE_PATH:=Frameworks/SDL2.framework/Headers
 CFLAGS+=  -mmacosx-version-min=10.10
 LDFLAGS+= -FFrameworks -framework SDL2 -framework OpenGL -framework AppKit -mmacosx-version-min=10.10
 FIXUP:=install_name_tool -change @rpath/SDL2.framework/Versions/A/SDL2 @executable_path/Frameworks/SDL2.framework/Versions/A/SDL2
-else
+else #Darwin
 SDL_INCLUDE_PATH:=sdl/include
 LDFLAGS+= -Wl,-rpath='$$ORIGIN/lib' -Llib -lSDL2
 ifndef USE_GLES
@@ -109,7 +125,7 @@
 endif #Darwin
 CFLAGS+= -I$(SDL_INCLUDE_PATH)
 
-else
+else #PORTABLE
 ifeq ($(MAKECMDGOALS),libblastem.$(SO))
 LDFLAGS:=-lm
 else
@@ -180,6 +196,10 @@
 TRANSOBJS=gen.o backend.o $(MEM) arena.o tern.o
 M68KOBJS=68kinst.o disasm.o
 
+ifdef NO_FILE_CHOOSER
+CHOOSER:=nuklear_ui/filechooser_nulll.o
+endif
+
 ifdef NEW_CORE
 Z80OBJS=z80.o z80inst.o
 M68KOBJS+= m68k.o
@@ -198,7 +218,7 @@
 endif
 AUDIOOBJS=ym2612.o psg.o wave.o flac.o vgm.o event_log.o render_audio.o rf5c164.o
 CONFIGOBJS=config.o tern.o util.o paths.o
-NUKLEAROBJS=$(FONT) nuklear_ui/blastem_nuklear.o nuklear_ui/sfnt.o
+NUKLEAROBJS=$(FONT) $(CHOOSER) nuklear_ui/blastem_nuklear.o nuklear_ui/sfnt.o
 RENDEROBJS=ppm.o controller_info.o
 ifdef USE_FBDEV
 RENDEROBJS+= render_fbdev.o
@@ -226,6 +246,7 @@
 CFLAGS+= -DDISABLE_NUKLEAR
 else
 MAINOBJS+= $(NUKLEAROBJS)
+LDFLAGS+=$(EXTRA_NUKLEAR_LDFLAGS)
 endif
 
 ifeq ($(CPU),x86_64)
--- a/default.cfg	Thu Oct 19 23:27:57 2023 -0700
+++ b/default.cfg	Sat Oct 21 19:22:01 2023 -0700
@@ -404,6 +404,8 @@
 	extensions bin gen md smd sms gg zip gz cue iso
 	#specifies the preferred save-state format, set to gst for Genecyst compatible states
 	state_format native
+	#set to on to use the native file picker on your OS instead of the builtin one
+	use_native_filechooser off
 }
 
 system {
@@ -435,4 +437,4 @@
 }
 
 #Don't manually edit `version`, it's used for automatic config migration
-version 3
+version 7
--- a/nuklear_ui/blastem_nuklear.c	Thu Oct 19 23:27:57 2023 -0700
+++ b/nuklear_ui/blastem_nuklear.c	Sat Oct 21 19:22:01 2023 -0700
@@ -9,6 +9,7 @@
 #include "blastem_nuklear.h"
 #include "nuklear_rawfb.h"
 #include "font.h"
+#include "filechooser.h"
 #include "../render.h"
 #include "../render_sdl.h"
 #include "../util.h"
@@ -77,6 +78,35 @@
 static const char *browser_setting_path;
 static const char **browser_ext_list;
 static uint32_t browser_num_exts;
+static uint8_t use_native_filechooser;
+
+static void handle_chooser_result(uint8_t normal_open, char *full_path)
+{
+	if(normal_open) {
+		if (current_system) {
+			current_system->next_rom = full_path;
+			current_system->request_exit(current_system);
+		} else {
+			init_system_with_media(full_path, SYSTEM_UNKNOWN);
+			free(full_path);
+		}
+
+		clear_view_stack();
+		show_play_view();
+	} else if (browser_setting_path) {
+		config = tern_insert_path(config, browser_setting_path, (tern_val){.ptrval = full_path}, TVAL_PTR);
+		config_dirty = 1;
+		browser_ext_list = NULL;
+		pop_view();
+	} else {
+		lockon_media(full_path);
+		free(full_path);
+
+		clear_view_stack();
+		show_play_view();
+	}
+}
+
 void view_file_browser(struct nk_context *context, uint8_t normal_open)
 {
 	static dir_entry *entries;
@@ -88,6 +118,18 @@
 	if (!browser_cur_path) {
 		get_initial_browse_path(&browser_cur_path);
 	}
+	if (use_native_filechooser && native_filechooser_available()) {
+		char *path = native_filechooser_pick(browser_label, browser_cur_path);
+		if (path) {
+			free(browser_cur_path);
+			browser_cur_path = path_dirname(path);
+			handle_chooser_result(normal_open, path);
+		} else {
+			browser_ext_list = NULL;
+			pop_view();
+		}
+		return;
+	}
 	if (!entries) {
 		entries = get_dir_list(browser_cur_path, &num_entries);
 		if (entries) {
@@ -155,29 +197,7 @@
 				free_dir_list(entries, num_entries);
 				entries = NULL;
 			} else {
-				if(normal_open) {
-					if (current_system) {
-						current_system->next_rom = full_path;
-						current_system->request_exit(current_system);
-					} else {
-						init_system_with_media(full_path, SYSTEM_UNKNOWN);
-						free(full_path);
-					}
-
-					clear_view_stack();
-					show_play_view();
-				} else if (browser_setting_path) {
-					config = tern_insert_path(config, browser_setting_path, (tern_val){.ptrval = full_path}, TVAL_PTR);
-					config_dirty = 1;
-					browser_ext_list = NULL;
-					pop_view();
-				} else {
-					lockon_media(full_path);
-					free(full_path);
-
-					clear_view_stack();
-					show_play_view();
-				}
+				handle_chooser_result(normal_open, full_path);
 			}
 			selected_entry = -1;
 		}
@@ -2277,10 +2297,15 @@
 		}
 		selected_init = settings_dropdown(context, "Initial RAM Value", ram_inits, num_inits, selected_init, "system\0ram_init\0");
 		settings_toggle(context, "Remember ROM Path", "ui\0remember_path\0", 1);
+		settings_toggle(context, "Use Native File Picker", "ui\0use_native_filechooser\0", 0);
 		settings_toggle(context, "Save config with EXE", "ui\0config_in_exe_dir\0", 0);
 		settings_string(context, "Game Save Path", "ui\0save_path\0", "$USERDATA/blastem/$ROMNAME");
 
 		if (nk_button_label(context, "Back")) {
+			if (config_dirty) {
+				char *unf = tern_find_path(config, "ui\0use_native_filechooser\0", TVAL_PTR).ptrval;
+				use_native_filechooser = unf && !strcmp(unf, "on");
+			}
 			pop_view();
 		}
 		nk_end(context);
@@ -2689,6 +2714,8 @@
 	render_set_ui_render_fun(blastem_nuklear_render);
 	render_set_event_handler(handle_event);
 	render_set_gl_context_handlers(context_destroyed, context_created);
+	char *unf = tern_find_path(config, "ui\0use_native_filechooser\0", TVAL_PTR).ptrval;
+	use_native_filechooser = unf && !strcmp(unf, "on");
 
 	atexit(persist_config_exit);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nuklear_ui/filechooser.h	Sat Oct 21 19:22:01 2023 -0700
@@ -0,0 +1,7 @@
+#ifndef FILECHOOSER_H_
+#define FILECHOOSER_H_
+
+uint8_t native_filechooser_available(void);
+char* native_filechooser_pick(const char *title, const char *start_directory);
+
+#endif //FILECHOOSER_H_
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nuklear_ui/filechooser_gtk.c	Sat Oct 21 19:22:01 2023 -0700
@@ -0,0 +1,185 @@
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <gtk/gtk.h>
+#include <dlfcn.h>
+
+typedef GtkWidget* (*gtk_file_chooser_dialog_new_t)(const gchar *title, GtkWindow *parent, GtkFileChooserAction action, const gchar *first_button_text, ...);
+typedef gint (*gtk_dialog_run_t)(GtkDialog *dialog);
+typedef void (*gtk_widget_destroy_t)(GtkWidget *widget);
+typedef gchar* (*gtk_file_chooser_get_filename_t)(GtkFileChooser *chooser);
+typedef gboolean (*gtk_init_check_t)(int *argc, char **argv);
+typedef gboolean (*gtk_file_chooser_set_current_folder_t)(GtkFileChooser *chooser, const gchar *filename);
+typedef void (*gtk_file_chooser_setadd_filter_t)(GtkFileChooser *chooser, GtkFileFilter *filter);
+typedef gboolean (*gtk_events_pending_t)(void);
+typedef gboolean (*gtk_main_iteration_t)(void);
+typedef GtkFileFilter* (*gtk_file_filter_new_t)(void);
+typedef void (*gtk_file_filter_set_name_t)(GtkFileFilter *filter, const gchar *name);
+typedef void (*gtk_file_filter_add_pattern_t)(GtkFileFilter *filter, const gchar *pattern);
+
+typedef struct {
+	gtk_file_chooser_dialog_new_t gtk_file_chooser_dialog_new;
+	gtk_dialog_run_t gtk_dialog_run;
+	gtk_widget_destroy_t gtk_widget_destroy;
+	gtk_file_chooser_get_filename_t gtk_file_chooser_get_filename;
+	gtk_file_chooser_set_current_folder_t gtk_file_chooser_set_current_folder;
+	gtk_file_chooser_setadd_filter_t gtk_file_chooser_add_filter;
+	gtk_file_chooser_setadd_filter_t gtk_file_chooser_set_filter;
+	gtk_file_filter_new_t gtk_file_filter_new;
+	gtk_file_filter_set_name_t gtk_file_filter_set_name;
+	gtk_file_filter_add_pattern_t gtk_file_filter_add_pattern;
+	gtk_init_check_t gtk_init_check;
+	gtk_events_pending_t gtk_events_pending;
+	gtk_main_iteration_t gtk_main_iteration;
+} gtk;
+
+#define LOAD_SYM(s, t, name) t->name = dlsym(s, #name); if (!t->name) { fputs("filechooser_gtk: Failed to load " #name "\n", stderr); goto error_cleanup; }
+
+static gtk* check_init_gtk(void)
+{
+	static const char *so_paths[] = {
+#ifdef X86_64
+		"/usr/lib/x86_64-linux-gnu/libgtk-3.so.0",
+		"/usr/lib/x86_64-linux-gnu/libgtk-x11-2.0.so.0",
+#elif X86_32
+		"/usr/lib/i386-linux-gnu/libgtk-3.so.0",
+		"/usr/lib/i386-linux-gnu/libgtk-x11-2.0.so.0",
+#else
+	//TODO: what are these paths on ARM?
+#endif
+	};
+	static gtk *funcs;
+	static uint8_t already_init;
+	if (!already_init) {
+		void *so = NULL;
+		for (int i = 0; !so && i < sizeof(so_paths)/sizeof(*so_paths); i++)
+		{
+			so = dlopen(so_paths[i], RTLD_NOW | RTLD_LOCAL);
+		}
+		if (so) {
+			funcs = calloc(1, sizeof(gtk));
+
+			LOAD_SYM(so, funcs, gtk_file_chooser_dialog_new)
+			LOAD_SYM(so, funcs, gtk_dialog_run)
+			LOAD_SYM(so, funcs, gtk_widget_destroy)
+			LOAD_SYM(so, funcs, gtk_file_chooser_get_filename)
+			LOAD_SYM(so, funcs, gtk_file_chooser_set_current_folder)
+			LOAD_SYM(so, funcs, gtk_file_chooser_add_filter)
+			LOAD_SYM(so, funcs, gtk_file_chooser_set_filter)
+			LOAD_SYM(so, funcs, gtk_file_filter_new)
+			LOAD_SYM(so, funcs, gtk_file_filter_set_name)
+			LOAD_SYM(so, funcs, gtk_file_filter_add_pattern)
+			LOAD_SYM(so, funcs, gtk_init_check)
+			LOAD_SYM(so, funcs, gtk_events_pending)
+			LOAD_SYM(so, funcs, gtk_main_iteration)
+
+			if (funcs->gtk_init_check(NULL, NULL)) {
+				return funcs;
+			}
+
+error_cleanup:
+			free(funcs);
+			dlclose(so);
+		}
+	}
+	return funcs;
+}
+
+uint8_t native_filechooser_available(void)
+{
+	return !!check_init_gtk();
+}
+
+char* native_filechooser_pick(const char *title, const char *start_directory)
+{
+	gtk *g = check_init_gtk();
+	if (!g) {
+		return NULL;
+	}
+	GtkFileChooser *chooser = (GtkFileChooser *)g->gtk_file_chooser_dialog_new(
+		title, NULL, GTK_FILE_CHOOSER_ACTION_OPEN,
+		"Cancel", GTK_RESPONSE_CANCEL,
+		"Open", GTK_RESPONSE_ACCEPT,
+		NULL
+	);
+	if (!chooser) {
+		return NULL;
+	}
+	if (start_directory) {
+		g->gtk_file_chooser_set_current_folder(chooser, start_directory);
+	}
+	GtkFileFilter *filter = g->gtk_file_filter_new();
+	g->gtk_file_filter_set_name(filter, "All Files");
+	g->gtk_file_filter_add_pattern(filter, "*");
+	g->gtk_file_chooser_add_filter(chooser, filter);
+
+	filter = g->gtk_file_filter_new();
+	g->gtk_file_filter_set_name(filter, "All Supported Types");
+	g->gtk_file_filter_add_pattern(filter, "*.zip");
+	g->gtk_file_filter_add_pattern(filter, "*.bin");
+	g->gtk_file_filter_add_pattern(filter, "*.bin.gz");
+	g->gtk_file_filter_add_pattern(filter, "*.gen");
+	g->gtk_file_filter_add_pattern(filter, "*.gen.gz");
+	g->gtk_file_filter_add_pattern(filter, "*.md");
+	g->gtk_file_filter_add_pattern(filter, "*.md.gz");
+	g->gtk_file_filter_add_pattern(filter, "*.sms");
+	g->gtk_file_filter_add_pattern(filter, "*.sms.gz");
+	g->gtk_file_filter_add_pattern(filter, "*.gg");
+	g->gtk_file_filter_add_pattern(filter, "*.gg.gz");
+	g->gtk_file_filter_add_pattern(filter, "*.sg");
+	g->gtk_file_filter_add_pattern(filter, "*.sg.gz");
+	g->gtk_file_filter_add_pattern(filter, "*.cue");
+	g->gtk_file_filter_add_pattern(filter, "*.toc");
+	g->gtk_file_filter_add_pattern(filter, "*.flac");
+	g->gtk_file_filter_add_pattern(filter, "*.vgm");
+	g->gtk_file_filter_add_pattern(filter, "*.vgz");
+	g->gtk_file_filter_add_pattern(filter, "*.vgm.gz");
+	g->gtk_file_chooser_add_filter(chooser, filter);
+	g->gtk_file_chooser_set_filter(chooser, filter);
+
+	filter = g->gtk_file_filter_new();
+	g->gtk_file_filter_set_name(filter, "Genesis/MD");
+	g->gtk_file_filter_add_pattern(filter, "*.zip");
+	g->gtk_file_filter_add_pattern(filter, "*.bin");
+	g->gtk_file_filter_add_pattern(filter, "*.bin.gz");
+	g->gtk_file_filter_add_pattern(filter, "*.gen");
+	g->gtk_file_filter_add_pattern(filter, "*.gen.gz");
+	g->gtk_file_filter_add_pattern(filter, "*.md");
+	g->gtk_file_filter_add_pattern(filter, "*.md.gz");
+	g->gtk_file_chooser_add_filter(chooser, filter);
+
+	filter = g->gtk_file_filter_new();
+	g->gtk_file_filter_set_name(filter, "Sega/Mega CD");
+	g->gtk_file_filter_add_pattern(filter, "*.cue");
+	g->gtk_file_filter_add_pattern(filter, "*.toc");
+	g->gtk_file_chooser_add_filter(chooser, filter);
+
+	filter = g->gtk_file_filter_new();
+	g->gtk_file_filter_set_name(filter, "Sega 8-bit");
+	g->gtk_file_filter_add_pattern(filter, "*.sms");
+	g->gtk_file_filter_add_pattern(filter, "*.sms.gz");
+	g->gtk_file_filter_add_pattern(filter, "*.gg");
+	g->gtk_file_filter_add_pattern(filter, "*.gg.gz");
+	g->gtk_file_filter_add_pattern(filter, "*.sg");
+	g->gtk_file_filter_add_pattern(filter, "*.sg.gz");
+	g->gtk_file_chooser_add_filter(chooser, filter);
+
+	filter = g->gtk_file_filter_new();
+	g->gtk_file_filter_set_name(filter, "Audio/VGM");
+	g->gtk_file_filter_add_pattern(filter, "*.flac");
+	g->gtk_file_filter_add_pattern(filter, "*.vgm");
+	g->gtk_file_filter_add_pattern(filter, "*.vgz");
+	g->gtk_file_filter_add_pattern(filter, "*.vgm.gz");
+	g->gtk_file_chooser_add_filter(chooser, filter);
+
+	char *ret = NULL;
+	if (GTK_RESPONSE_ACCEPT == g->gtk_dialog_run((GtkDialog*)chooser)) {
+		ret = g->gtk_file_chooser_get_filename(chooser);
+	}
+	g->gtk_widget_destroy((GtkWidget *)chooser);
+	while (g->gtk_events_pending())
+	{
+		g->gtk_main_iteration();
+	}
+	return ret;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nuklear_ui/filechooser_null.c	Sat Oct 21 19:22:01 2023 -0700
@@ -0,0 +1,12 @@
+#include <stddef.h>
+#include <stdint.h>
+
+uint8_t native_filechooser_available(void)
+{
+	return 0;
+}
+
+char* native_filechooser_pick(const char *title, const char *start_directory)
+{
+	return NULL;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/nuklear_ui/filechooser_win.c	Sat Oct 21 19:22:01 2023 -0700
@@ -0,0 +1,34 @@
+#include <windows.h>
+#include <stdint.h>
+
+uint8_t native_filechooser_available(void)
+{
+	return 1;
+}
+
+char* native_filechooser_pick(const char *title, const char *start_directory)
+{
+	char file_name[MAX_PATH] = "";
+	OPENFILENAMEA ofn = {
+		.lStructSize = sizeof(ofn),
+		.hwndOwner = NULL, //TODO: should probably get the HWND of the main window
+		.lpstrFilter =
+			"All Files\0*.*\0"
+			"All Supported Types\0*.zip;*.bin;*.bin.gz;*.gen;*.gen.gz;*.md;*.md.gz;*.sms;*.sms.gz;*.gg;*.gg.gz;*.sg;*.sg.gz;*.cue;*.toc;*.flac;*.vgm;*.vgz;*.vgm.gz\0"
+			"Genesis/MD\0.zip;*.bin;*.bin.gz;*.gen;*.gen*.gz;*.md;*.md.gz\0"
+			"Sega/Mega CD\0*.cue;*.toc\0"
+			"Sega 8-bit\0*.sms;*.sms.gz;*.gg;*.gg.gz;*.sg;*.sg.gz\0"
+			"Audio/VGM\0*.flac;*.vgm;*.vgz;*.vgm.gz\0",
+		.nFilterIndex = 2,
+		.lpstrFile = file_name,
+		.nMaxFile = sizeof(file_name),
+		.lpstrInitialDir = start_directory,
+		.lpstrTitle = title,
+		.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_LONGNAMES,
+	};
+	char *ret = NULL;
+	if (GetOpenFileNameA(&ofn)) {
+		ret = strdup(file_name);
+	}
+	return ret;
+}