Browse Source

Add love.window.showFileDialog(type, callback [, settings]).

Resolves #990.

The type parameter is an enum: "openfile", "openfolder", or "savefile".

The callback parameter takes 3 args:
  files (array of full platform-dependent paths to selected files, empty if the dialog is canceled by the user),
  filtername (nil if none is set, see below),
  errorstring (nil if no error).

The optional settings parameter of showFileDialog has the following fields:
{
  title = string (optional title),
  acceptlabel = string (optional custom name for the Accept button),
  cancellabel = string (optional custom name for the Cancel button),
  defaultname = string (optional default folder/file name for the file text box),
  filters = table (filter names as keys and filter pattern strings as values).
}
Sasha Szpakowski 7 months ago
parent
commit
ecd08a2b0f

+ 2 - 0
src/common/Reference.h

@@ -69,6 +69,8 @@ public:
 	 **/
 	 **/
 	void push(lua_State *L);
 	void push(lua_State *L);
 
 
+	lua_State *getPinnedL() const { return pinnedL; }
+
 private:
 private:
 
 
 	// A pinned coroutine (probably the main thread) belonging to the Lua state
 	// A pinned coroutine (probably the main thread) belonging to the Lua state

+ 27 - 21
src/modules/event/sdl/Event.cpp

@@ -37,6 +37,7 @@
 #include <cmath>
 #include <cmath>
 
 
 #include "joystick/sdl/Joystick.h"
 #include "joystick/sdl/Joystick.h"
+#include "window/sdl/Window.h"
 
 
 namespace love
 namespace love
 {
 {
@@ -47,25 +48,22 @@ namespace sdl
 
 
 // SDL reports mouse coordinates in the window coordinate system in OS X, but
 // SDL reports mouse coordinates in the window coordinate system in OS X, but
 // we want them in pixel coordinates (may be different with high-DPI enabled.)
 // we want them in pixel coordinates (may be different with high-DPI enabled.)
-static void windowToDPICoords(double *x, double *y)
+static void windowToDPICoords(love::window::Window *window, double *x, double *y)
 {
 {
-	auto window = Module::getInstance<window::Window>(Module::M_WINDOW);
 	if (window)
 	if (window)
 		window->windowToDPICoords(x, y);
 		window->windowToDPICoords(x, y);
 }
 }
 
 
-static void clampToWindow(double *x, double *y)
+static void clampToWindow(love::window::Window *window, double *x, double *y)
 {
 {
-	auto window = Module::getInstance<window::Window>(Module::M_WINDOW);
 	if (window)
 	if (window)
 		window->clampPositionInWindow(x, y);
 		window->clampPositionInWindow(x, y);
 }
 }
 
 
-static void normalizedToDPICoords(double *x, double *y)
+static void normalizedToDPICoords(love::window::Window *window, double *x, double *y)
 {
 {
 	double w = 1.0, h = 1.0;
 	double w = 1.0, h = 1.0;
 
 
-	auto window = Module::getInstance<window::Window>(Module::M_WINDOW);
 	if (window)
 	if (window)
 	{
 	{
 		w = window->getWidth();
 		w = window->getWidth();
@@ -192,6 +190,7 @@ Message *Event::convert(const SDL_Event &e)
 
 
 	love::filesystem::Filesystem *filesystem = nullptr;
 	love::filesystem::Filesystem *filesystem = nullptr;
 	love::sensor::Sensor *sensorInstance = nullptr;
 	love::sensor::Sensor *sensorInstance = nullptr;
+	love::window::Window *win = Module::getInstance<window::Window>(Module::M_WINDOW);
 
 
 	love::keyboard::Keyboard::Key key = love::keyboard::Keyboard::KEY_UNKNOWN;
 	love::keyboard::Keyboard::Key key = love::keyboard::Keyboard::KEY_UNKNOWN;
 	love::keyboard::Keyboard::Scancode scancode = love::keyboard::Keyboard::SCANCODE_UNKNOWN;
 	love::keyboard::Keyboard::Scancode scancode = love::keyboard::Keyboard::SCANCODE_UNKNOWN;
@@ -202,6 +201,15 @@ Message *Event::convert(const SDL_Event &e)
 	love::touch::sdl::Touch *touchmodule = nullptr;
 	love::touch::sdl::Touch *touchmodule = nullptr;
 	love::touch::Touch::TouchInfo touchinfo = {};
 	love::touch::Touch::TouchInfo touchinfo = {};
 
 
+	if (win)
+	{
+		// Dubious cast, but it's not like having an SDL event backend
+		// with a non-SDL window backend will be a thing.
+		auto sdlwin = dynamic_cast<love::window::sdl::Window *>(win);
+		if (sdlwin != nullptr)
+			sdlwin->handleSDLEvent(e);
+	}
+
 	switch (e.type)
 	switch (e.type)
 	{
 	{
 	case SDL_EVENT_KEY_DOWN:
 	case SDL_EVENT_KEY_DOWN:
@@ -262,9 +270,9 @@ Message *Event::convert(const SDL_Event &e)
 			// able to handle out-of-bounds coordinates. SDL has a hint to turn off
 			// able to handle out-of-bounds coordinates. SDL has a hint to turn off
 			// auto capture, but it doesn't report the mouse's position at the edge of
 			// auto capture, but it doesn't report the mouse's position at the edge of
 			// the window if the mouse moves fast enough when it's off.
 			// the window if the mouse moves fast enough when it's off.
-			clampToWindow(&x, &y);
-			windowToDPICoords(&x, &y);
-			windowToDPICoords(&xrel, &yrel);
+			clampToWindow(win, &x, &y);
+			windowToDPICoords(win, &x, &y);
+			windowToDPICoords(win, &xrel, &yrel);
 
 
 			vargs.emplace_back(x);
 			vargs.emplace_back(x);
 			vargs.emplace_back(y);
 			vargs.emplace_back(y);
@@ -292,8 +300,8 @@ Message *Event::convert(const SDL_Event &e)
 			double px = (double) e.button.x;
 			double px = (double) e.button.x;
 			double py = (double) e.button.y;
 			double py = (double) e.button.y;
 
 
-			clampToWindow(&px, &py);
-			windowToDPICoords(&px, &py);
+			clampToWindow(win, &px, &py);
+			windowToDPICoords(win, &px, &py);
 
 
 			vargs.emplace_back(px);
 			vargs.emplace_back(px);
 			vargs.emplace_back(py);
 			vargs.emplace_back(py);
@@ -329,8 +337,8 @@ Message *Event::convert(const SDL_Event &e)
 		// SDL's coords are normalized to [0, 1], but we want screen coords for direct touches.
 		// SDL's coords are normalized to [0, 1], but we want screen coords for direct touches.
 		if (touchinfo.deviceType == love::touch::Touch::DEVICE_TOUCHSCREEN)
 		if (touchinfo.deviceType == love::touch::Touch::DEVICE_TOUCHSCREEN)
 		{
 		{
-			normalizedToDPICoords(&touchinfo.x, &touchinfo.y);
-			normalizedToDPICoords(&touchinfo.dx, &touchinfo.dy);
+			normalizedToDPICoords(win, &touchinfo.x, &touchinfo.y);
+			normalizedToDPICoords(win, &touchinfo.dx, &touchinfo.dy);
 		}
 		}
 
 
 		// We need to update the love.touch.sdl internal state from here.
 		// We need to update the love.touch.sdl internal state from here.
@@ -387,7 +395,7 @@ Message *Event::convert(const SDL_Event &e)
 	case SDL_EVENT_WINDOW_RESTORED:
 	case SDL_EVENT_WINDOW_RESTORED:
 	case SDL_EVENT_WINDOW_EXPOSED:
 	case SDL_EVENT_WINDOW_EXPOSED:
 	case SDL_EVENT_WINDOW_OCCLUDED:
 	case SDL_EVENT_WINDOW_OCCLUDED:
-		msg = convertWindowEvent(e);
+		msg = convertWindowEvent(e, win);
 		break;
 		break;
 	case SDL_EVENT_DISPLAY_ORIENTATION:
 	case SDL_EVENT_DISPLAY_ORIENTATION:
 		{
 		{
@@ -440,7 +448,7 @@ Message *Event::convert(const SDL_Event &e)
 		{
 		{
 			double x = e.drop.x;
 			double x = e.drop.x;
 			double y = e.drop.y;
 			double y = e.drop.y;
-			windowToDPICoords(&x, &y);
+			windowToDPICoords(win, &x, &y);
 			vargs.emplace_back(x);
 			vargs.emplace_back(x);
 			vargs.emplace_back(y);
 			vargs.emplace_back(y);
 			msg = new Message("dropcompleted", vargs);
 			msg = new Message("dropcompleted", vargs);
@@ -450,7 +458,7 @@ Message *Event::convert(const SDL_Event &e)
 		{
 		{
 			double x = e.drop.x;
 			double x = e.drop.x;
 			double y = e.drop.y;
 			double y = e.drop.y;
-			windowToDPICoords(&x, &y);
+			windowToDPICoords(win, &x, &y);
 			vargs.emplace_back(x);
 			vargs.emplace_back(x);
 			vargs.emplace_back(y);
 			vargs.emplace_back(y);
 			msg = new Message("dropmoved", vargs);
 			msg = new Message("dropmoved", vargs);
@@ -466,7 +474,7 @@ Message *Event::convert(const SDL_Event &e)
 
 
 			double x = e.drop.x;
 			double x = e.drop.x;
 			double y = e.drop.y;
 			double y = e.drop.y;
-			windowToDPICoords(&x, &y);
+			windowToDPICoords(win, &x, &y);
 
 
 			if (filesystem->isRealDirectory(filepath))
 			if (filesystem->isRealDirectory(filepath))
 			{
 			{
@@ -681,14 +689,13 @@ Message *Event::convertJoystickEvent(const SDL_Event &e) const
 	return msg;
 	return msg;
 }
 }
 
 
-Message *Event::convertWindowEvent(const SDL_Event &e)
+Message *Event::convertWindowEvent(const SDL_Event &e, love::window::Window *win)
 {
 {
 	Message *msg = nullptr;
 	Message *msg = nullptr;
 
 
 	std::vector<Variant> vargs;
 	std::vector<Variant> vargs;
 	vargs.reserve(4);
 	vargs.reserve(4);
 
 
-	window::Window *win = nullptr;
 	graphics::Graphics *gfx = nullptr;
 	graphics::Graphics *gfx = nullptr;
 
 
 	auto event = e.type;
 	auto event = e.type;
@@ -735,7 +742,6 @@ Message *Event::convertWindowEvent(const SDL_Event &e)
 			double height = e.window.data2;
 			double height = e.window.data2;
 
 
 			gfx = Module::getInstance<graphics::Graphics>(Module::M_GRAPHICS);
 			gfx = Module::getInstance<graphics::Graphics>(Module::M_GRAPHICS);
-			win = Module::getInstance<window::Window>(Module::M_WINDOW);
 			if (win)
 			if (win)
 				win->onSizeChanged(e.window.data1, e.window.data2);
 				win->onSizeChanged(e.window.data1, e.window.data2);
 
 
@@ -750,7 +756,7 @@ Message *Event::convertWindowEvent(const SDL_Event &e)
 			{
 			{
 				width = win->getWidth();
 				width = win->getWidth();
 				height = win->getHeight();
 				height = win->getHeight();
-				windowToDPICoords(&width, &height);
+				windowToDPICoords(win, &width, &height);
 			}
 			}
 
 
 			vargs.emplace_back(width);
 			vargs.emplace_back(width);

+ 6 - 1
src/modules/event/sdl/Event.h

@@ -28,6 +28,11 @@
 // SDL
 // SDL
 #include <SDL3/SDL_events.h>
 #include <SDL3/SDL_events.h>
 
 
+namespace love::window
+{
+class Window;
+}
+
 namespace love
 namespace love
 {
 {
 namespace event
 namespace event
@@ -63,7 +68,7 @@ private:
 
 
 	Message *convert(const SDL_Event &e);
 	Message *convert(const SDL_Event &e);
 	Message *convertJoystickEvent(const SDL_Event &e) const;
 	Message *convertJoystickEvent(const SDL_Event &e) const;
-	Message *convertWindowEvent(const SDL_Event &e);
+	Message *convertWindowEvent(const SDL_Event &e, love::window::Window *win);
 
 
 }; // Event
 }; // Event
 
 

+ 41 - 92
src/modules/window/Window.cpp

@@ -56,112 +56,61 @@ void Window::swapBuffers()
 {
 {
 }
 }
 
 
-bool Window::getConstant(const char *in, FullscreenType &out)
+STRINGMAP_CLASS_BEGIN(Window, Window::Setting, Window::SETTING_MAX_ENUM, setting)
 {
 {
-	return fullscreenTypes.find(in, out);
+	{"fullscreen", Window::SETTING_FULLSCREEN},
+	{"fullscreentype", Window::SETTING_FULLSCREEN_TYPE},
+	{"vsync", Window::SETTING_VSYNC},
+	{"msaa", Window::SETTING_MSAA},
+	{"stencil", Window::SETTING_STENCIL},
+	{"depth", Window::SETTING_DEPTH},
+	{"resizable", Window::SETTING_RESIZABLE},
+	{"minwidth", Window::SETTING_MIN_WIDTH},
+	{"minheight", Window::SETTING_MIN_HEIGHT},
+	{"borderless", Window::SETTING_BORDERLESS},
+	{"centered", Window::SETTING_CENTERED},
+	{"displayindex", Window::SETTING_DISPLAYINDEX},
+	{"display", Window::SETTING_DISPLAY},
+	{"highdpi", Window::SETTING_HIGHDPI},
+	{"usedpiscale", Window::SETTING_USE_DPISCALE},
+	{"refreshrate", Window::SETTING_REFRESHRATE},
+	{"x", Window::SETTING_X},
+	{"y", Window::SETTING_Y},
 }
 }
+STRINGMAP_CLASS_END(Window, Window::Setting, Window::SETTING_MAX_ENUM, setting)
 
 
-bool Window::getConstant(FullscreenType in, const char *&out)
+STRINGMAP_CLASS_BEGIN(Window, Window::FullscreenType, Window::FULLSCREEN_MAX_ENUM, fullscreenType)
 {
 {
-	return fullscreenTypes.find(in, out);
+	{"exclusive", Window::FULLSCREEN_EXCLUSIVE},
+	{"desktop", Window::FULLSCREEN_DESKTOP},
 }
 }
+STRINGMAP_CLASS_END(Window, Window::FullscreenType, Window::FULLSCREEN_MAX_ENUM, fullscreenType)
 
 
-std::vector<std::string> Window::getConstants(FullscreenType)
+STRINGMAP_CLASS_BEGIN(Window, Window::MessageBoxType, Window::MESSAGEBOX_MAX_ENUM, messageBoxType)
 {
 {
-	return fullscreenTypes.getNames();
+	{"error", Window::MESSAGEBOX_ERROR},
+	{"warning", Window::MESSAGEBOX_WARNING},
+	{"info", Window::MESSAGEBOX_INFO},
 }
 }
+STRINGMAP_CLASS_END(Window, Window::MessageBoxType, Window::MESSAGEBOX_MAX_ENUM, messageBoxType)
 
 
-bool Window::getConstant(const char *in, Setting &out)
+STRINGMAP_CLASS_BEGIN(Window, Window::FileDialogType, Window::FILEDIALOG_MAX_ENUM, fileDialogType)
 {
 {
-	return settings.find(in, out);
+	{ "openfile", Window::FILEDIALOG_OPENFILE },
+	{ "openfolder", Window::FILEDIALOG_OPENFOLDER },
+	{ "savefile", Window::FILEDIALOG_SAVEFILE },
 }
 }
+STRINGMAP_CLASS_END(Window, Window::FileDialogType, Window::FILEDIALOG_MAX_ENUM, fileDialogType)
 
 
-bool Window::getConstant(Setting in, const char *&out)
+STRINGMAP_CLASS_BEGIN(Window, Window::DisplayOrientation, Window::ORIENTATION_MAX_ENUM, orientation)
 {
 {
-	return settings.find(in, out);
+	{"unknown", Window::ORIENTATION_UNKNOWN},
+	{"landscape", Window::ORIENTATION_LANDSCAPE},
+	{"landscapeflipped", Window::ORIENTATION_LANDSCAPE_FLIPPED},
+	{"portrait", Window::ORIENTATION_PORTRAIT},
+	{"portraitflipped", Window::ORIENTATION_PORTRAIT_FLIPPED},
 }
 }
-
-bool Window::getConstant(const char *in, MessageBoxType &out)
-{
-	return messageBoxTypes.find(in, out);
-}
-
-bool Window::getConstant(MessageBoxType in, const char *&out)
-{
-	return messageBoxTypes.find(in, out);
-}
-
-std::vector<std::string> Window::getConstants(MessageBoxType)
-{
-	return messageBoxTypes.getNames();
-}
-
-bool Window::getConstant(const char *in, DisplayOrientation &out)
-{
-	return orientations.find(in, out);
-}
-
-bool Window::getConstant(DisplayOrientation in, const char *&out)
-{
-	return orientations.find(in, out);
-}
-
-std::vector<std::string> Window::getConstants(DisplayOrientation)
-{
-	return orientations.getNames();
-}
-
-StringMap<Window::Setting, Window::SETTING_MAX_ENUM>::Entry Window::settingEntries[] =
-{
-	{"fullscreen", SETTING_FULLSCREEN},
-	{"fullscreentype", SETTING_FULLSCREEN_TYPE},
-	{"vsync", SETTING_VSYNC},
-	{"msaa", SETTING_MSAA},
-	{"stencil", SETTING_STENCIL},
-	{"depth", SETTING_DEPTH},
-	{"resizable", SETTING_RESIZABLE},
-	{"minwidth", SETTING_MIN_WIDTH},
-	{"minheight", SETTING_MIN_HEIGHT},
-	{"borderless", SETTING_BORDERLESS},
-	{"centered", SETTING_CENTERED},
-	{"displayindex", SETTING_DISPLAYINDEX},
-	{"display", SETTING_DISPLAY},
-	{"highdpi", SETTING_HIGHDPI},
-	{"usedpiscale", SETTING_USE_DPISCALE},
-	{"refreshrate", SETTING_REFRESHRATE},
-	{"x", SETTING_X},
-	{"y", SETTING_Y},
-};
-
-StringMap<Window::Setting, Window::SETTING_MAX_ENUM> Window::settings(Window::settingEntries, sizeof(Window::settingEntries));
-
-StringMap<Window::FullscreenType, Window::FULLSCREEN_MAX_ENUM>::Entry Window::fullscreenTypeEntries[] =
-{
-	{"exclusive", FULLSCREEN_EXCLUSIVE},
-	{"desktop", FULLSCREEN_DESKTOP},
-};
-
-StringMap<Window::FullscreenType, Window::FULLSCREEN_MAX_ENUM> Window::fullscreenTypes(Window::fullscreenTypeEntries, sizeof(Window::fullscreenTypeEntries));
-
-StringMap<Window::MessageBoxType, Window::MESSAGEBOX_MAX_ENUM>::Entry Window::messageBoxTypeEntries[] =
-{
-	{"error", MESSAGEBOX_ERROR},
-	{"warning", MESSAGEBOX_WARNING},
-	{"info", MESSAGEBOX_INFO},
-};
-
-StringMap<Window::MessageBoxType, Window::MESSAGEBOX_MAX_ENUM> Window::messageBoxTypes(Window::messageBoxTypeEntries, sizeof(Window::messageBoxTypeEntries));
-
-StringMap<Window::DisplayOrientation, Window::ORIENTATION_MAX_ENUM>::Entry Window::orientationEntries[] =
-{
-	{"unknown", ORIENTATION_UNKNOWN},
-	{"landscape", ORIENTATION_LANDSCAPE},
-	{"landscapeflipped", ORIENTATION_LANDSCAPE_FLIPPED},
-	{"portrait", ORIENTATION_PORTRAIT},
-	{"portraitflipped", ORIENTATION_PORTRAIT_FLIPPED},
-};
-
-StringMap<Window::DisplayOrientation, Window::ORIENTATION_MAX_ENUM> Window::orientations(Window::orientationEntries, sizeof(Window::orientationEntries));
+STRINGMAP_CLASS_END(Window, Window::DisplayOrientation, Window::ORIENTATION_MAX_ENUM, orientation)
 
 
 } // window
 } // window
 } // love
 } // love

+ 35 - 28
src/modules/window/Window.h

@@ -55,6 +55,8 @@ class Window : public Module
 {
 {
 public:
 public:
 
 
+	typedef void (*FileDialogCallback)(void *context, const std::vector<std::string> &files, const char *filtername, const char *err);
+
 	// Different window settings.
 	// Different window settings.
 	enum Setting
 	enum Setting
 	{
 	{
@@ -94,6 +96,14 @@ public:
 		MESSAGEBOX_MAX_ENUM
 		MESSAGEBOX_MAX_ENUM
 	};
 	};
 
 
+	enum FileDialogType
+	{
+		FILEDIALOG_OPENFILE,
+		FILEDIALOG_OPENFOLDER,
+		FILEDIALOG_SAVEFILE,
+		FILEDIALOG_MAX_ENUM
+	};
+
 	enum DisplayOrientation
 	enum DisplayOrientation
 	{
 	{
 		ORIENTATION_UNKNOWN,
 		ORIENTATION_UNKNOWN,
@@ -129,6 +139,24 @@ public:
 		bool attachToWindow;
 		bool attachToWindow;
 	};
 	};
 
 
+	struct FileDialogFilter
+	{
+		std::string name;
+		std::string pattern;
+	};
+
+	struct FileDialogData
+	{
+		FileDialogType type;
+		std::string title;
+		std::string acceptLabel;
+		std::string cancelLabel;
+		std::string defaultName;
+		std::vector<FileDialogFilter> filters;
+		bool multiSelect;
+		bool attachToWindow;
+	};
+
 	virtual ~Window();
 	virtual ~Window();
 
 
 	virtual void setGraphics(graphics::Graphics *graphics) = 0;
 	virtual void setGraphics(graphics::Graphics *graphics) = 0;
@@ -220,41 +248,20 @@ public:
 	virtual bool showMessageBox(const std::string &title, const std::string &message, MessageBoxType type, bool attachtowindow) = 0;
 	virtual bool showMessageBox(const std::string &title, const std::string &message, MessageBoxType type, bool attachtowindow) = 0;
 	virtual int showMessageBox(const MessageBoxData &data) = 0;
 	virtual int showMessageBox(const MessageBoxData &data) = 0;
 
 
-	virtual void requestAttention(bool continuous) = 0;
-
-	static bool getConstant(const char *in, Setting &out);
-	static bool getConstant(Setting in, const char *&out);
+	virtual void showFileDialog(const FileDialogData &data, FileDialogCallback callback, void *context) = 0;
 
 
-	static bool getConstant(const char *in, FullscreenType &out);
-	static bool getConstant(FullscreenType in, const char *&out);
-	static std::vector<std::string> getConstants(FullscreenType);
-
-	static bool getConstant(const char *in, MessageBoxType &out);
-	static bool getConstant(MessageBoxType in, const char *&out);
-	static std::vector<std::string> getConstants(MessageBoxType);
+	virtual void requestAttention(bool continuous) = 0;
 
 
-	static bool getConstant(const char *in, DisplayOrientation &out);
-	static bool getConstant(DisplayOrientation in, const char *&out);
-	static std::vector<std::string> getConstants(DisplayOrientation);
+	STRINGMAP_CLASS_DECLARE(Setting);
+	STRINGMAP_CLASS_DECLARE(FullscreenType);
+	STRINGMAP_CLASS_DECLARE(MessageBoxType);
+	STRINGMAP_CLASS_DECLARE(FileDialogType);
+	STRINGMAP_CLASS_DECLARE(DisplayOrientation);
 
 
 protected:
 protected:
 
 
 	Window(const char *name);
 	Window(const char *name);
 
 
-private:
-
-	static StringMap<Setting, SETTING_MAX_ENUM>::Entry settingEntries[];
-	static StringMap<Setting, SETTING_MAX_ENUM> settings;
-
-	static StringMap<FullscreenType, FULLSCREEN_MAX_ENUM>::Entry fullscreenTypeEntries[];
-	static StringMap<FullscreenType, FULLSCREEN_MAX_ENUM> fullscreenTypes;
-
-	static StringMap<MessageBoxType, MESSAGEBOX_MAX_ENUM>::Entry messageBoxTypeEntries[];
-	static StringMap<MessageBoxType, MESSAGEBOX_MAX_ENUM> messageBoxTypes;
-
-	static StringMap<DisplayOrientation, ORIENTATION_MAX_ENUM>::Entry orientationEntries[];
-	static StringMap<DisplayOrientation, ORIENTATION_MAX_ENUM> orientations;
-
 }; // Window
 }; // Window
 
 
 struct WindowSettings
 struct WindowSettings

+ 138 - 2
src/modules/window/sdl/Window.cpp

@@ -26,6 +26,7 @@
 #	include "graphics/vulkan/Vulkan.h"
 #	include "graphics/vulkan/Vulkan.h"
 #endif
 #endif
 #include "Window.h"
 #include "Window.h"
+#include "filesystem/Filesystem.h"
 
 
 #ifdef LOVE_ANDROID
 #ifdef LOVE_ANDROID
 #include "common/android.h"
 #include "common/android.h"
@@ -86,7 +87,7 @@ Window::Window()
 	, displayedWindowError(false)
 	, displayedWindowError(false)
 	, contextAttribs()
 	, contextAttribs()
 {
 {
-	if (!SDL_InitSubSystem(SDL_INIT_VIDEO))
+	if (!SDL_InitSubSystem(SDL_INIT_VIDEO | SDL_INIT_EVENTS))
 		throw love::Exception("Could not initialize SDL video subsystem (%s)", SDL_GetError());
 		throw love::Exception("Could not initialize SDL video subsystem (%s)", SDL_GetError());
 
 
 	// Make sure the screensaver doesn't activate by default.
 	// Make sure the screensaver doesn't activate by default.
@@ -97,13 +98,15 @@ Window::Window()
 	// on some setups. More investigation is needed before enabling it.
 	// on some setups. More investigation is needed before enabling it.
 	canUseDwmFlush = SDL_GetHintBoolean("LOVE_GRAPHICS_VSYNC_DWM", false);
 	canUseDwmFlush = SDL_GetHintBoolean("LOVE_GRAPHICS_VSYNC_DWM", false);
 #endif
 #endif
+
+	dialogEventId = SDL_RegisterEvents(1);
 }
 }
 
 
 Window::~Window()
 Window::~Window()
 {
 {
 	close(false);
 	close(false);
 	graphics.set(nullptr);
 	graphics.set(nullptr);
-	SDL_QuitSubSystem(SDL_INIT_VIDEO);
+	SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
 }
 }
 
 
 void Window::setGraphics(graphics::Graphics *graphics)
 void Window::setGraphics(graphics::Graphics *graphics)
@@ -1490,6 +1493,139 @@ int Window::showMessageBox(const MessageBoxData &data)
 	return pressedbutton;
 	return pressedbutton;
 }
 }
 
 
+// As of a SDL3 prerelease, a lot of SDL file dialog parameters need to persist
+// until the callback completes, so we store them here.
+// This is also used to retrieve some useful info to pass to love's own callback,
+// and to send that along to SDL events (see below).
+class FileDialogState : public love::Object
+{
+public:
+
+	void *context;
+	Window::FileDialogCallback callback;
+	Uint32 dialogEventId;
+	Window::FileDialogData data;
+	std::vector<SDL_DialogFileFilter> sdlFilters;
+	SDL_PropertiesID props;
+
+	Optional<std::string> err;
+	std::vector<std::string> files;
+	int filterIndex;
+};
+
+static void SDLCALL fileDialogCallbackSDL(void *userdata, const char *const *filelist, int filter)
+{
+	auto state = (FileDialogState *) userdata;
+	if (state == nullptr)
+		return;
+
+	auto fs = Module::getInstance<filesystem::Filesystem>(Module::M_FILESYSTEM);
+
+	if (filelist != nullptr)
+	{
+		// SDL's file list only lasts until the end of the callback, so we copy it.
+		for (int i = 0; filelist[i] != nullptr; i++)
+		{
+			std::string file(filelist[i]);
+			if (fs != nullptr)
+				file = fs->canonicalizeRealPath(file);
+			state->files.push_back(file);
+		}
+	}
+	else
+	{
+		state->err.set(SDL_GetError());
+	}
+
+	state->filterIndex = filter;
+
+	SDL_DestroyProperties(state->props);
+
+	// The SDL dialog callback isn't guaranteed to be called on the main thread,
+	// whereas SDL event polling will happen there. This is needed because Lua states
+	// aren't thread safe.
+	SDL_Event event = {};
+	event.type = state->dialogEventId;
+	event.user.data1 = state;
+
+	SDL_PushEvent(&event);
+}
+
+void Window::handleSDLEvent(const SDL_Event &event)
+{
+	if (event.type == dialogEventId)
+	{
+		// Releases itself when it goes out of scope.
+		StrongRef<FileDialogState> state((FileDialogState *) event.user.data1, Acquire::NORETAIN);
+
+		const char *filtername = state->filterIndex >= 0
+			? state->data.filters[state->filterIndex].name.c_str()
+			: nullptr;
+
+		state->callback(state->context, state->files, filtername, state->err.hasValue ? state->err.value.c_str() : nullptr);
+	}
+}
+
+void Window::showFileDialog(const FileDialogData &data, FileDialogCallback callback, void *context)
+{
+	SDL_FileDialogType sdltype = SDL_FILEDIALOG_OPENFILE;
+	switch (data.type)
+	{
+	case FILEDIALOG_OPENFILE:
+	default:
+		sdltype = SDL_FILEDIALOG_OPENFILE;
+		break;
+	case FILEDIALOG_OPENFOLDER:
+		sdltype = SDL_FILEDIALOG_OPENFOLDER;
+		break;
+	case FILEDIALOG_SAVEFILE:
+		sdltype = SDL_FILEDIALOG_SAVEFILE;
+		break;
+	}
+
+	auto state = new FileDialogState();
+	state->callback = callback;
+	state->context = context;
+	state->dialogEventId = dialogEventId;
+	state->data = data;
+
+	for (const auto &filter : state->data.filters)
+	{
+		SDL_DialogFileFilter f = {};
+		f.name = filter.name.c_str();
+		f.pattern = filter.pattern.c_str();
+		state->sdlFilters.push_back(f);
+	}
+
+	// We destroy this in the dialog callback, since it needs to persist until then (until that's fixed in SDL code).
+	state->props = SDL_CreateProperties();
+
+	if (!data.title.empty())
+		SDL_SetStringProperty(state->props, SDL_PROP_FILE_DIALOG_TITLE_STRING, state->data.title.c_str());
+
+	if (!data.acceptLabel.empty())
+		SDL_SetStringProperty(state->props, SDL_PROP_FILE_DIALOG_ACCEPT_STRING, state->data.acceptLabel.c_str());
+
+	if (!data.cancelLabel.empty())
+		SDL_SetStringProperty(state->props, SDL_PROP_FILE_DIALOG_CANCEL_STRING, state->data.cancelLabel.c_str());
+
+	if (!data.defaultName.empty())
+		SDL_SetStringProperty(state->props, SDL_PROP_FILE_DIALOG_LOCATION_STRING, state->data.defaultName.c_str());
+
+	if (data.attachToWindow)
+		SDL_SetPointerProperty(state->props, SDL_PROP_FILE_DIALOG_WINDOW_POINTER, window);
+
+	if (!state->sdlFilters.empty())
+	{
+		SDL_SetPointerProperty(state->props, SDL_PROP_FILE_DIALOG_FILTERS_POINTER, state->sdlFilters.data());
+		SDL_SetNumberProperty(state->props, SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER, state->sdlFilters.size());
+	}
+
+	SDL_SetBooleanProperty(state->props, SDL_PROP_FILE_DIALOG_MANY_BOOLEAN, data.multiSelect);
+
+	SDL_ShowFileDialogWithProperties(sdltype, fileDialogCallbackSDL, state, state->props);
+}
+
 void Window::requestAttention(bool continuous)
 void Window::requestAttention(bool continuous)
 {
 {
 #if defined(LOVE_WINDOWS) && !defined(LOVE_WINDOWS_UWP)
 #if defined(LOVE_WINDOWS) && !defined(LOVE_WINDOWS_UWP)

+ 6 - 0
src/modules/window/sdl/Window.h

@@ -129,8 +129,12 @@ public:
 	bool showMessageBox(const std::string &title, const std::string &message, MessageBoxType type, bool attachtowindow) override;
 	bool showMessageBox(const std::string &title, const std::string &message, MessageBoxType type, bool attachtowindow) override;
 	int showMessageBox(const MessageBoxData &data) override;
 	int showMessageBox(const MessageBoxData &data) override;
 
 
+	void showFileDialog(const FileDialogData &data, FileDialogCallback callback, void *context) override;
+
 	void requestAttention(bool continuous) override;
 	void requestAttention(bool continuous) override;
 
 
+	void handleSDLEvent(const SDL_Event &event);
+
 private:
 private:
 
 
 	struct ContextAttribs
 	struct ContextAttribs
@@ -185,6 +189,8 @@ private:
 
 
 	StrongRef<graphics::Graphics> graphics;
 	StrongRef<graphics::Graphics> graphics;
 
 
+	Uint32 dialogEventId;
+
 }; // Window
 }; // Window
 
 
 } // sdl
 } // sdl

+ 106 - 0
src/modules/window/wrap_Window.cpp

@@ -20,6 +20,7 @@
 
 
 #include "wrap_Window.h"
 #include "wrap_Window.h"
 #include "sdl/Window.h"
 #include "sdl/Window.h"
+#include "common/Reference.h"
 
 
 namespace love
 namespace love
 {
 {
@@ -647,6 +648,110 @@ int w_showMessageBox(lua_State *L)
 	return 1;
 	return 1;
 }
 }
 
 
+static void fileDialogCallback(void *context, const std::vector<std::string> &files, const char *filtername, const char *errstr)
+{
+	auto r = (Reference *)context;
+	lua_State *L = r->getPinnedL();
+
+	r->push(L);
+
+	lua_createtable(L, (int)files.size(), 0);
+	for (size_t i = 0; i < files.size(); i++)
+	{
+		lua_pushstring(L, files[i].c_str());
+		lua_rawseti(L, -2, (int)i + 1);
+	}
+
+	if (filtername != nullptr)
+		lua_pushstring(L, filtername);
+	else
+		lua_pushnil(L);
+
+	if (errstr != nullptr)
+		lua_pushstring(L, errstr);
+	else
+		lua_pushnil(L);
+
+	int err = lua_pcall(L, 3, 0, 0);
+
+	delete r;
+
+	// Unfortunately, this eats the stack trace, too bad.
+	if (err != 0)
+		throw love::Exception("Error in file dialog callback: %s", luax_tostring(L, -1));
+}
+
+int w_showFileDialog(lua_State *L)
+{
+	Window::FileDialogData data = {};
+
+	const char *typestr = luaL_checkstring(L, 1);
+	if (!Window::getConstant(typestr, data.type))
+		return luax_enumerror(L, "file dialog type", Window::getConstants(data.type), typestr);
+
+	luaL_checktype(L, 2, LUA_TFUNCTION);
+
+	if (!lua_isnoneornil(L, 3))
+	{
+		luaL_checktype(L, 3, LUA_TTABLE);
+
+		lua_getfield(L, 3, "title");
+		if (!lua_isnoneornil(L, -1))
+			data.title = luax_checkstring(L, -1);
+		lua_pop(L, 1);
+
+		lua_getfield(L, 3, "acceptlabel");
+		if (!lua_isnoneornil(L, -1))
+			data.acceptLabel = luax_checkstring(L, -1);
+		lua_pop(L, 1);
+
+		lua_getfield(L, 3, "cancellabel");
+		if (!lua_isnoneornil(L, -1))
+			data.cancelLabel = luax_checkstring(L, -1);
+		lua_pop(L, 1);
+
+		lua_getfield(L, 3, "defaultname");
+		if (!lua_isnoneornil(L, -1))
+			data.defaultName = luax_checkstring(L, -1);
+		lua_pop(L, 1);
+
+		lua_getfield(L, 3, "filters");
+		if (!lua_isnoneornil(L, -1))
+		{
+			luaL_checktype(L, -1, LUA_TTABLE);
+
+			lua_pushnil(L);
+			while (lua_next(L, -2))
+			{
+				Window::FileDialogFilter filter = {};
+				filter.name = luax_checkstring(L, -2);
+				filter.pattern = luax_checkstring(L, -1);
+				data.filters.push_back(filter);
+				lua_pop(L, 1);
+			}
+		}
+		lua_pop(L, 1);
+
+		lua_getfield(L, 3, "multiselect");
+		if (!lua_isnoneornil(L, -1))
+			data.multiSelect = luax_checkboolean(L, -1);
+		lua_pop(L, 1);
+
+		lua_getfield(L, 3, "attachtowindow");
+		if (!lua_isnoneornil(L, -1))
+			data.attachToWindow = luax_checkboolean(L, -1);
+		lua_pop(L, 1);
+	}
+
+	// Save the callback function as a Reference.
+	lua_pushvalue(L, 2);
+	Reference *r = new Reference(L);
+	lua_pop(L, 1);
+
+	instance()->showFileDialog(data, fileDialogCallback, r);
+	return 0;
+}
+
 int w_requestAttention(lua_State *L)
 int w_requestAttention(lua_State *L)
 {
 {
 	bool continuous = luax_optboolean(L, 1, false);
 	bool continuous = luax_optboolean(L, 1, false);
@@ -701,6 +806,7 @@ static const luaL_Reg functions[] =
 	{ "isMaximized", w_isMaximized },
 	{ "isMaximized", w_isMaximized },
 	{ "isMinimized", w_isMinimized },
 	{ "isMinimized", w_isMinimized },
 	{ "showMessageBox", w_showMessageBox },
 	{ "showMessageBox", w_showMessageBox },
+	{ "showFileDialog", w_showFileDialog },
 	{ "requestAttention", w_requestAttention },
 	{ "requestAttention", w_requestAttention },
 	{ "getPointer", w_getPointer },
 	{ "getPointer", w_getPointer },
 	{ 0, 0 }
 	{ 0, 0 }