diff --git a/include/SDL3/SDL_dialog.h b/include/SDL3/SDL_dialog.h index ddb9e24d5fe31..32243f0e5b26f 100644 --- a/include/SDL3/SDL_dialog.h +++ b/include/SDL3/SDL_dialog.h @@ -332,6 +332,369 @@ extern SDL_DECLSPEC void SDLCALL SDL_ShowFileDialogWithProperties(SDL_FileDialog #define SDL_PROP_FILE_DIALOG_ACCEPT_STRING "SDL.filedialog.accept" #define SDL_PROP_FILE_DIALOG_CANCEL_STRING "SDL.filedialog.cancel" +/** + * Result of dialogs. + * + * This is passed as an argument for the callback of many dialog-related + * functions. + * + * \since This enum is available since SDL 3.4.0. + */ +typedef enum SDL_DialogResult +{ + /** An error occured. The dialog may or may not have been shown to the user + before the error happened, but the chosen input couldn't be obtained. */ + SDL_DIALOGRESULT_ERROR, + /** The user has canceled the dialog, or the program aborted the dialog. */ + SDL_DIALOGRESULT_CANCEL, + /** The user finished the input and confirmed the result. */ + SDL_DIALOGRESULT_SUCCESS +} SDL_DialogResult; + +/** + * Callback used by input dialog functions. + * + * \param userdata an app-provided pointer, for the callback's use. + * \param input the text input by the user. + * \param result the success status of the dialog. + * + * \since This datatype is available since SDL 3.4.0. + * + * \sa SDL_DialogResult + * \sa SDL_ShowInputDialogWithProperties + */ +typedef void (SDLCALL *SDL_DialogInputCallback)(void *userdata, const char *input, SDL_DialogResult result); + +/** + * Create and launch a text input dialog with the specified properties. + * + * This is an asynchronous function; it will return immediately, and the + * result will be passed to the callback. + * + * The callback will be invoked with the string input by the user, an empty + * string if the user canceled the dialog, and NULL if an error occured. + * + * Note that the callback may be called from a different thread than the one + * the function was invoked on. + * + * These are the supported properties: + * + * - `SDL_PROP_INPUT_DIALOG_WINDOW_POINTER`: the window that the dialog should + * be modal for. + * - `SDL_PROP_INPUT_DIALOG_TITLE_STRING`: the title for the dialog. + * - `SDL_PROP_INPUT_DIALOG_PROMPT_STRING`: the prompt for the dialog. + * - `SDL_PROP_INPUT_DIALOG_DEFAULT_STRING`: the default value to fill in the + * dialog. + * - `SDL_PROP_INPUT_DIALOG_ACCEPT_STRING`: the label that the accept button + * should have. + * - `SDL_PROP_INPUT_DIALOG_CANCEL_STRING`: the label that the cancel button + * should have. + * - `SDL_PROP_INPUT_DIALOG_PASSWORD_BOOLEAN`: if true, the text field will + * treat its contents as a password, which usually involves displaying one + * character (such as an asterisk) in place of all characters in the field. + * Defaults to false. + * + * Note that each platform may or may not support any of the properties. + * + * \param callback a function pointer to be invoked when the user finishes + * entering the prompt. + * \param userdata an optional pointer to pass extra data to the callback when + * it will be invoked. + * \param props the properties to use. + * + * \threadsafety This function should be called only from the main thread. The + * callback may be invoked from the same thread or from a + * different one, depending on the OS's constraints. + * + * \since This function is available since SDL 3.4.0. + */ +extern SDL_DECLSPEC void SDLCALL SDL_ShowInputDialogWithProperties(SDL_DialogInputCallback callback, void *userdata, SDL_PropertiesID props); + +#define SDL_PROP_INPUT_DIALOG_WINDOW_POINTER "SDL.inputdialog.window" +#define SDL_PROP_INPUT_DIALOG_TITLE_STRING "SDL.inputdialog.title" +#define SDL_PROP_INPUT_DIALOG_PROMPT_STRING "SDL.inputdialog.prompt" +#define SDL_PROP_INPUT_DIALOG_DEFAULT_STRING "SDL.inputdialog.default" +#define SDL_PROP_INPUT_DIALOG_ACCEPT_STRING "SDL.inputdialog.accept" +#define SDL_PROP_INPUT_DIALOG_CANCEL_STRING "SDL.inputdialog.cancel" +#define SDL_PROP_INPUT_DIALOG_PASSWORD_BOOLEAN "SDL.inputdialog.password" + +/** + * A progress dialog. + * + * \since This datatype is available since SDL 3.4.0. + * + * \sa SDL_ShowProgressDialogWithProperties + */ +typedef struct SDL_ProgressDialog SDL_ProgressDialog; + +/** + * Callback used by progress dialog functions. + * + * \param userdata an app-provided pointer, for the callback's use. + * \param result the success status of the dialog. + * + * \since This datatype is available since SDL 3.4.0. + * + * \sa SDL_ShowProgressDialogWithProperties + */ +typedef void (SDLCALL *SDL_DialogProgressCallback)(void *userdata, SDL_DialogResult result); + +/** + * Create and launch a text input dialog with the specified properties. + * + * This is an asynchronous function; it will return immediately, and the + * result will be passed to the callback. + * + * Note that the callback may be called from a different thread than the one + * the function was invoked on. + * + * These are the supported properties: + * + * - `SDL_PROP_PROGRESS_DIALOG_WINDOW_POINTER`: the window that the dialog + * should be modal for. + * - `SDL_PROP_PROGRESS_DIALOG_TITLE_STRING`: the title for the dialog. + * - `SDL_PROP_PROGRESS_DIALOG_PROMPT_STRING`: the prompt for the dialog. + * - `SDL_PROP_PROGRESS_DIALOG_ACCEPT_STRING`: the label that the accept button + * should have. + * - `SDL_PROP_PROGRESS_DIALOG_CANCEL_STRING`: the label that the cancel button + * should have. + * - `SDL_PROP_PROGRESS_DIALOG_CAN_CANCEL_BOOLEAN`: if true, the user can cancel + * the dialog anytime with a "Cancel" button. It may be possible to cancel the + * dialog even if this property is set to false. Defaults to true. + * - `SDL_PROP_PROGRESS_DIALOG_PROGRESS_NUMBER`: a float betweeon 0 and 1 + * representing the value of the progress bar. Ignored if the property + * `SDL_PROP_PROGRESS_DIALOG_INDEFINITE_BOOLEAN` is set to true. + * - `SDL_PROP_PROGRESS_DIALOG_INDEFINITE_BOOLEAN`: If true, the progress bar + * won't show any meaningful value and will adopt a generic progress-less + * loading behavior. Defaults to false. + * - `SDL_PROP_PROGRESS_DIALOG_SHOW_TIME_REMAINING_BOOLEAN`: If true, the dialog + * will show an estimate of the remaining time for the progress bar to finish. + * Defaults to false. + * - `SDL_PROP_PROGRESS_DIALOG_AUTO_CLOSE_BOOLEAN`: If true, the dialog will be + * closed automatically when the progress bar reaches 100%. Defaults to false. + * + * Note that each platform may or may not support any of the properties. + * + * \param callback a function pointer to be invoked when the progress finishes, + * successfully or not. + * \param userdata an optional pointer to pass extra data to the callback when + * it will be invoked. + * \param props the properties to use. + * + * \returns a progress dialog handle. + * + * \threadsafety This function should be called only from the main thread. The + * callback may be invoked from the same thread or from a + * different one, depending on the OS's constraints. + * + * \since This function is available since SDL 3.4.0. + * + * \sa SDL_DialogProgressCallback + * \sa SDL_UpdateProgressDialog + * \sa SDL_DestroyProgressDialog + */ +extern SDL_DECLSPEC SDL_ProgressDialog* SDLCALL SDL_ShowProgressDialogWithProperties(SDL_DialogProgressCallback callback, void *userdata, SDL_PropertiesID props); + +#define SDL_PROP_PROGRESS_DIALOG_WINDOW_POINTER "SDL.progressdialog.window" +#define SDL_PROP_PROGRESS_DIALOG_TITLE_STRING "SDL.progressdialog.title" +#define SDL_PROP_PROGRESS_DIALOG_PROMPT_STRING "SDL.progressdialog.prompt" +#define SDL_PROP_PROGRESS_DIALOG_ACCEPT_STRING "SDL.progressdialog.accept" +#define SDL_PROP_PROGRESS_DIALOG_CANCEL_STRING "SDL.progressdialog.cancel" +#define SDL_PROP_PROGRESS_DIALOG_CAN_CANCEL_BOOLEAN "SDL.progressdialog.can_cancel" +#define SDL_PROP_PROGRESS_DIALOG_INDEFINITE_BOOLEAN "SDL.progressdialog.indefinite" +#define SDL_PROP_PROGRESS_DIALOG_SHOW_TIME_REMAINING_BOOLEAN "SDL.progressdialog.show_time_remaining" +#define SDL_PROP_PROGRESS_DIALOG_AUTO_CLOSE_BOOLEAN "SDL.progressdialog.auto_close" + +/** + * Update a progress dialog to a new progress status. + * + * For auto-closing progress dialogs, calling this function with a progress of 1 + * (corresponding to 100%) will close the dialog and invoke the callback with + * success status. To close the dialog with error status, use + * SDL_CloseProgressDialog() instead. + * + * For indefinite progress dialogs, this function has no effect unless progress + * is 1, in which case it will report a successful completion. + * + * \param dialog the progress dialog. + * \param progress the new progress for the dialog, which is a float between + * 0 and 1 inclusively. + * \param new_prompt a new prompt, or NULL to keep the same prompt. + * + * \threadsafety This function is not thread-safe. + * + * \since This function is available since SDL 3.4.0. + * + * \sa SDL_ShowProgressDialogWithProperties + * \sa SDL_DestroyProgressDialog + */ +extern SDL_DECLSPEC void SDLCALL SDL_UpdateProgressDialog(SDL_ProgressDialog* dialog, float progress, const char *new_prompt); + +/** + * Close and destroy a progress dialog, invoking the callback with error + * status if it hasn't yet been called. + * + * It is safe to call this function while the dialog is still open; it will + * abort the dialog if it hasn't already completed. + * + * \param dialog the dialog to be destroyed. + * + * \threadsafety This function is not thread-safe. + * + * \since This function is available since SDL 3.4.0. + * + * \sa SDL_ShowProgressDialogWithProperties + * \sa SDL_UpdateProgressDialog + */ +extern SDL_DECLSPEC void SDLCALL SDL_DestroyProgressDialog(SDL_ProgressDialog* dialog); + +/** + * Callback used by color dialog functions. + * + * \param userdata an app-provided pointer, for the callback's use. + * \param color the color chosen by the user. + * \param result the success status of the dialog. + * + * \since This datatype is available since SDL 3.4.0. + * + * \sa SDL_DialogResult + * \sa SDL_ShowColorPickerDialogWithProperties + */ +typedef void (SDLCALL *SDL_DialogColorCallback)(void *userdata, SDL_Color color, SDL_DialogResult result); + +/** + * Create and launch a color picker dialog with the specified properties. + * + * This is an asynchronous function; it will return immediately, and the + * result will be passed to the callback. + * + * The callback will be invoked with the color chosen by the user, or black if + * the dialog was canceled or an error occured. + * + * Note that the callback may be called from a different thread than the one + * the function was invoked on. + * + * These are the supported properties: + * + * - `SDL_PROP_COLOR_DIALOG_WINDOW_POINTER`: the window that the dialog should + * be modal for. + * - `SDL_PROP_COLOR_DIALOG_TITLE_STRING`: the title for the dialog. + * - `SDL_PROP_COLOR_DIALOG_PROMPT_STRING`: the prompt for the dialog. + * - `SDL_PROP_COLOR_DIALOG_DEFAULT_POINTER`: the default value for the dialog. + * Must be a pointer to SDL_Color; NULL has the same effect as not setting + * this property. + * - `SDL_PROP_COLOR_DIALOG_ACCEPT_STRING`: the label that the accept button + * should have. + * - `SDL_PROP_COLOR_DIALOG_CANCEL_STRING`: the label that the cancel button + * should have. + * + * Note that each platform may or may not support any of the properties. + * + * If the platform's native color picker does not support selecting an alpha + * value, and a non-NULL default color is provided, the resulting color will + * have the same alpha value as the default color. Otherwise, it will be opaque. + * + * \param callback a function pointer to be invoked when the user finishes + * entering the prompt. + * \param userdata an optional pointer to pass extra data to the callback when + * it will be invoked. + * \param props the properties to use. + * + * \threadsafety This function should be called only from the main thread. The + * callback may be invoked from the same thread or from a + * different one, depending on the OS's constraints. + * + * \since This function is available since SDL 3.4.0. + * + * \sa SDL_Color + * \sa SDL_DialogColorCallback + */ +extern SDL_DECLSPEC void SDLCALL SDL_ShowColorPickerDialogWithProperties(SDL_DialogColorCallback callback, void *userdata, SDL_PropertiesID props); + +#define SDL_PROP_COLOR_DIALOG_WINDOW_POINTER "SDL.colordialog.window" +#define SDL_PROP_COLOR_DIALOG_TITLE_STRING "SDL.colordialog.title" +#define SDL_PROP_COLOR_DIALOG_PROMPT_STRING "SDL.colordialog.prompt" +#define SDL_PROP_COLOR_DIALOG_DEFAULT_POINTER "SDL.colordialog.default" +#define SDL_PROP_COLOR_DIALOG_ACCEPT_STRING "SDL.colordialog.accept" +#define SDL_PROP_COLOR_DIALOG_CANCEL_STRING "SDL.colordialog.cancel" + +typedef struct SDL_Date +{ + Uint16 y; + Uint8 m; + Uint8 d; +} SDL_Date; + +/** + * Callback used by date picker dialog functions. + * + * \param userdata an app-provided pointer, for the callback's use. + * \param date the date chosen by the user. + * \param result the success status of the dialog. + * + * \since This datatype is available since SDL 3.4.0. + * + * \sa SDL_DialogResult + * \sa SDL_ShowDatePickerDialogWithProperties + */ +typedef void (SDLCALL *SDL_DialogDateCallback)(void *userdata, SDL_Date date, SDL_DialogResult result); + +/** + * Create and launch a date picker dialog with the specified properties. + * + * This function only supports dates from the Gregorian calendar, with years + * 1-9999, months 1-12 and days 1-31. Field can be set to 0 to retain the + * default value. + * + * This is an asynchronous function; it will return immediately, and the + * result will be passed to the callback. + * + * The callback will be invoked with the date chosen by the user, or { 0, 0, 0 } + * if the dialog was canceled or an error occured. + * + * Note that the callback may be called from a different thread than the one + * the function was invoked on. + * + * These are the supported properties: + * + * - `SDL_PROP_DATE_DIALOG_WINDOW_POINTER`: the window that the dialog should + * be modal for. + * - `SDL_PROP_DATE_DIALOG_TITLE_STRING`: the title for the dialog. + * - `SDL_PROP_DATE_DIALOG_PROMPT_STRING`: the prompt for the dialog. + * - `SDL_PROP_DATE_DIALOG_DEFAULT_POINTER`: the default value for the dialog. + * Should be a pointer to SDL_Date. Fields can be set to 0 to keep the default + * value. + * - `SDL_PROP_DATE_DIALOG_ACCEPT_STRING`: the label that the accept button + * should have. + * - `SDL_PROP_DATE_DIALOG_CANCEL_STRING`: the label that the cancel button + * should have. + * + * Note that each platform may or may not support any of the properties. + * + * \param callback a function pointer to be invoked when the user finishes + * entering the prompt. + * \param userdata an optional pointer to pass extra data to the callback when + * it will be invoked. + * \param props the properties to use. + * + * \threadsafety This function should be called only from the main thread. The + * callback may be invoked from the same thread or from a + * different one, depending on the OS's constraints. + * + * \since This function is available since SDL 3.4.0. + * + * \sa SDL_Date + * \sa SDL_DialogDateCallback + */ +extern SDL_DECLSPEC void SDLCALL SDL_ShowDatePickerDialogWithProperties(SDL_DialogDateCallback callback, void *userdata, SDL_PropertiesID props); + +#define SDL_PROP_DATE_DIALOG_WINDOW_POINTER "SDL.datedialog.window" +#define SDL_PROP_DATE_DIALOG_TITLE_STRING "SDL.datedialog.title" +#define SDL_PROP_DATE_DIALOG_PROMPT_STRING "SDL.datedialog.prompt" +#define SDL_PROP_DATE_DIALOG_DEFAULT_POINTER "SDL.datedialog.default" +#define SDL_PROP_DATE_DIALOG_ACCEPT_STRING "SDL.datedialog.accept" +#define SDL_PROP_DATE_DIALOG_CANCEL_STRING "SDL.datedialog.cancel" + /* Ends C function definitions when using C++ */ #ifdef __cplusplus } diff --git a/src/dialog/SDL_dialog.c b/src/dialog/SDL_dialog.c index a77e4433fd676..03e169f50c2f2 100644 --- a/src/dialog/SDL_dialog.c +++ b/src/dialog/SDL_dialog.c @@ -129,3 +129,109 @@ void SDL_ShowOpenFolderDialog(SDL_DialogFileCallback callback, void *userdata, S SDL_DestroyProperties(props); #endif } + +// TODO: Dialogs after this should be implemented with XDG Portals + +void SDL_ShowInputDialogWithProperties(SDL_DialogInputCallback callback, void *userdata, SDL_PropertiesID props) +{ + if (!callback) { + return; + } +#ifdef SDL_DIALOG_DISABLED + SDL_SetError("SDL not built with dialog support"); + callback(userdata, NULL, SDL_DIALOGRESULT_ERROR); +#else + SDL_SYS_ShowInputDialogWithProperties(callback, userdata, props); +#endif +} + +SDL_ProgressDialog* SDL_ShowProgressDialogWithProperties(SDL_DialogProgressCallback callback, void *userdata, SDL_PropertiesID props) +{ + if (!callback) { + return NULL; + } +#ifdef SDL_DIALOG_DISABLED + SDL_SetError("SDL not built with dialog support"); + callback(userdata, NULL, SDL_DIALOGRESULT_ERROR); + return NULL; +#else + return SDL_SYS_ShowProgressDialogWithProperties(callback, userdata, props); +#endif +} + +void SDL_UpdateProgressDialog(SDL_ProgressDialog* dialog, float progress, const char *new_prompt) +{ +#ifdef SDL_DIALOG_DISABLED + SDL_SetError("SDL not built with dialog support"); +#else + if (!dialog) { + SDL_InvalidParamError("dialog"); + return; + } + if (progress < 0.0f || progress > 1.0f) { + SDL_InvalidParamError("progress"); + return; + } + SDL_SYS_UpdateProgressDialog(dialog, progress, new_prompt); +#endif +} + +void SDL_DestroyProgressDialog(SDL_ProgressDialog* dialog) +{ +#ifdef SDL_DIALOG_DISABLED + SDL_SetError("SDL not built with dialog support"); +#else + if (!dialog) { + SDL_InvalidParamError("dialog"); + return; + } + SDL_SYS_DestroyProgressDialog(dialog); +#endif +} + +void SDL_ShowColorPickerDialogWithProperties(SDL_DialogColorCallback callback, void *userdata, SDL_PropertiesID props) +{ + if (!callback) { + return; + } + +#ifdef SDL_DIALOG_DISABLED + SDL_Color c; + c.r = 0; + c.g = 0; + c.b = 0; + c.a = 0; + + SDL_SetError("SDL not built with dialog support"); + callback(userdata, c, SDL_DIALOGRESULT_ERROR); +#else + SDL_SYS_ShowColorPickerDialogWithProperties(callback, userdata, props); +#endif +} + +void SDL_ShowDatePickerDialogWithProperties(SDL_DialogDateCallback callback, void *userdata, SDL_PropertiesID props) +{ + if (!callback) { + return; + } + + SDL_Date d; + d.y = 0; + d.m = 0; + d.d = 0; + +#ifdef SDL_DIALOG_DISABLED + SDL_SetError("SDL not built with dialog support"); + callback(userdata, d, SDL_DIALOGRESULT_ERROR); +#else + SDL_Date *date = SDL_GetPointerProperty(props, SDL_PROP_DATE_DIALOG_DEFAULT_POINTER, NULL); + + // A value of 0 is "null" for that field + if (date && (date->y > 9999 || date->m > 12 || date->d > 31)) { + SDL_SetError("Invalid default date: %d-%d-%d", date->y, date->m, date->d); + callback(userdata, d, SDL_DIALOGRESULT_ERROR); + } + + SDL_SYS_ShowDatePickerDialogWithProperties(callback, userdata, props); +#endif +} diff --git a/src/dialog/SDL_dialog.h b/src/dialog/SDL_dialog.h index beee7ddf79277..fd767e2283207 100644 --- a/src/dialog/SDL_dialog.h +++ b/src/dialog/SDL_dialog.h @@ -20,3 +20,9 @@ */ void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props); +void SDL_SYS_ShowInputDialogWithProperties(SDL_DialogInputCallback callback, void *userdata, SDL_PropertiesID props); +SDL_ProgressDialog* SDL_SYS_ShowProgressDialogWithProperties(SDL_DialogProgressCallback callback, void *userdata, SDL_PropertiesID props); +void SDL_SYS_UpdateProgressDialog(SDL_ProgressDialog* dialog, float progress, const char *new_prompt); +void SDL_SYS_DestroyProgressDialog(SDL_ProgressDialog* dialog); +void SDL_SYS_ShowColorPickerDialogWithProperties(SDL_DialogColorCallback callback, void *userdata, SDL_PropertiesID props); +void SDL_SYS_ShowDatePickerDialogWithProperties(SDL_DialogDateCallback callback, void *userdata, SDL_PropertiesID props); diff --git a/src/dialog/dummy/SDL_dummydialog.c b/src/dialog/dummy/SDL_dummydialog.c index 121a090f1e014..39365501fffdd 100644 --- a/src/dialog/dummy/SDL_dummydialog.c +++ b/src/dialog/dummy/SDL_dummydialog.c @@ -26,8 +26,57 @@ void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFileCallback callback, void *userdata, SDL_PropertiesID props) { - SDL_Unsupported(); - callback(userdata, NULL, -1); + SDL_Unsupported(); + callback(userdata, NULL, -1); +} + +void SDL_SYS_ShowInputDialogWithProperties(SDL_DialogInputCallback callback, void *userdata, SDL_PropertiesID props) +{ + SDL_Unsupported(); + callback(userdata, NULL, SDL_DIALOGRESULT_ERROR); +} + +SDL_ProgressDialog* SDL_SYS_ShowProgressDialogWithProperties(SDL_DialogProgressCallback callback, void *userdata, SDL_PropertiesID props) +{ + SDL_Unsupported(); + callback(userdata, SDL_DIALOGRESULT_ERROR); + + // In case the callback calls SDL_SetError() + SDL_Unsupported(); + return NULL; +} + +void SDL_SYS_UpdateProgressDialog(SDL_ProgressDialog* dialog, float progress, const char *new_prompt) +{ + SDL_Unsupported(); +} + +void SDL_SYS_DestroyProgressDialog(SDL_ProgressDialog* dialog) +{ + SDL_Unsupported(); +} + +void SDL_SYS_ShowColorPickerDialogWithProperties(SDL_DialogColorCallback callback, void *userdata, SDL_PropertiesID props) +{ + SDL_Color color; + color.r = 0; + color.g = 0; + color.b = 0; + color.a = 0; + + SDL_Unsupported(); + callback(userdata, color, SDL_DIALOGRESULT_ERROR); +} + +void SDL_SYS_ShowDatePickerDialogWithProperties(SDL_DialogDateCallback callback, void *userdata, SDL_PropertiesID props) +{ + SDL_Date date; + date.y = 0; + date.m = 0; + date.d = 0; + + SDL_Unsupported(); + callback(userdata, date, SDL_DIALOGRESULT_ERROR); } #endif // SDL_DIALOG_DUMMY diff --git a/src/dialog/unix/SDL_zenitydialog.c b/src/dialog/unix/SDL_zenitydialog.c index 4632c8e16a2b6..29e776ed0fa3c 100644 --- a/src/dialog/unix/SDL_zenitydialog.c +++ b/src/dialog/unix/SDL_zenitydialog.c @@ -215,16 +215,14 @@ static zenityArgs *create_zenity_args(SDL_FileDialogType type, SDL_DialogFileCal // TODO: Zenity survives termination of the parent -static void run_zenity(SDL_DialogFileCallback callback, void *userdata, void *argv) +static char *exec_zenity(void *argv, SDL_DialogResult* dialog_result, size_t* bytes) { SDL_Process *process = NULL; SDL_Environment *env = NULL; int status = -1; - size_t bytes_read = 0; - char *container = NULL; - size_t narray = 1; - char **array = NULL; - bool result = false; + char *output = NULL; + + *dialog_result = SDL_DIALOGRESULT_ERROR; env = SDL_CreateEnvironment(true); if (!env) { @@ -252,8 +250,39 @@ static void run_zenity(SDL_DialogFileCallback callback, void *userdata, void *ar goto done; } - container = SDL_ReadProcess(process, &bytes_read, &status); - if (!container) { + output = SDL_ReadProcess(process, bytes, &status); + if (!output) { + goto done; + } + + // 0 = dialog completed, 1 = dialog canceled, other = error + if (status == 0) { + *dialog_result = SDL_DIALOGRESULT_SUCCESS; + } else if (status == 1) { + *dialog_result = SDL_DIALOGRESULT_CANCEL; + } else { + SDL_SetError("Could not run zenity: exit code %d", status); + *dialog_result = SDL_DIALOGRESULT_ERROR; + } + +done: + SDL_DestroyEnvironment(env); + SDL_DestroyProcess(process); + + return output; +} + +static void run_zenity(SDL_DialogFileCallback callback, void *userdata, void *argv) +{ + SDL_DialogResult dialog_result; + size_t bytes_read = 0; + char *container = NULL; + size_t narray = 1; + char **array = NULL; + bool result = false; + + container = exec_zenity(argv, &dialog_result, &bytes_read); + if (dialog_result == SDL_DIALOGRESULT_ERROR) { goto done; } @@ -281,21 +310,13 @@ static void run_zenity(SDL_DialogFileCallback callback, void *userdata, void *ar } } - // 0 = the user chose one or more files, 1 = the user canceled the dialog - if (status == 0 || status == 1) { - callback(userdata, (const char *const *)array, -1); - } else { - SDL_SetError("Could not run zenity: exit code %d", status); - callback(userdata, NULL, -1); - } + callback(userdata, (const char *const *)array, -1); result = true; done: SDL_free(array); SDL_free(container); - SDL_DestroyEnvironment(env); - SDL_DestroyProcess(process); if (!result) { callback(userdata, NULL, -1); @@ -364,3 +385,972 @@ bool SDL_Zenity_detect(void) } return (status == 0); } + +static void run_input_zenity(SDL_DialogInputCallback callback, void *userdata, void *argv) +{ + SDL_DialogResult dialog_result; + size_t bytes_read = 0; + char *input = NULL; + bool result = false; + + input = exec_zenity(argv, &dialog_result, &bytes_read); + if (!input) { + goto done; + } + + for (int i = 0; i < bytes_read; i++) { + if (input[i] == '\n') { + input[i] = '\0'; + break; + } + } + + callback(userdata, input, dialog_result); + result = true; + +done: + SDL_free(input); + + if (!result) { + callback(userdata, NULL, SDL_DIALOGRESULT_ERROR); + } +} + +typedef struct +{ + SDL_DialogInputCallback callback; + void *userdata; + void *argv; + + char *title; + char *prompt; + char *default_val; + char *accept; + char *cancel; +} zenityInputArgs; + +static void free_zenity_input_args(zenityInputArgs *args) +{ + SDL_free(args->title); + SDL_free(args->prompt); + SDL_free(args->default_val); + SDL_free(args->accept); + SDL_free(args->cancel); + SDL_free(args->argv); + SDL_free(args); +} + +static zenityInputArgs *create_zenity_input_args(SDL_DialogInputCallback callback, void *userdata, SDL_PropertiesID props) +{ + zenityInputArgs *args = SDL_calloc(1, sizeof(zenityInputArgs)); + if (!args) { + return NULL; + } + + const char **argv = SDL_calloc(sizeof(*argv), + 1 /* zenity */ + + 1 /* --entry | --password */ + + 2 /* --title [title] */ + + 2 /* --text [prompt] */ + + 2 /* --entry-text [default_val] */ + + 2 /* --ok-label [label] */ + + 2 /* --cancel-label [label] */ + + 1 /* NULL */); + + if (!argv) { + goto cleanup; + } + + args->argv = argv; + args->callback = callback; + args->userdata = userdata; + + /* Properties can be destroyed as soon as the function returns; copy over what we need. */ +#define COPY_STRING_PROPERTY(dst, prop) \ + { \ + const char *str = SDL_GetStringProperty(props, prop, NULL); \ + if (str) { \ + dst = SDL_strdup(str); \ + if (!dst) { \ + goto cleanup; \ + } \ + } \ + } + + COPY_STRING_PROPERTY(args->title, SDL_PROP_INPUT_DIALOG_TITLE_STRING); + COPY_STRING_PROPERTY(args->prompt, SDL_PROP_INPUT_DIALOG_PROMPT_STRING); + COPY_STRING_PROPERTY(args->default_val, SDL_PROP_INPUT_DIALOG_DEFAULT_STRING); + COPY_STRING_PROPERTY(args->accept, SDL_PROP_INPUT_DIALOG_ACCEPT_STRING); + COPY_STRING_PROPERTY(args->cancel, SDL_PROP_INPUT_DIALOG_CANCEL_STRING); +#undef COPY_STRING_PROPERTY + + int argc = 0; + argv[argc++] = "zenity"; + + if (SDL_GetBooleanProperty(props, SDL_PROP_INPUT_DIALOG_PASSWORD_BOOLEAN, false)) { + argv[argc++] = "--password"; + } else { + argv[argc++] = "--entry"; + } + + if (args->title) { + argv[argc++] = "--title"; + argv[argc++] = args->title; + } + + if (args->prompt) { + argv[argc++] = "--text"; + argv[argc++] = args->prompt; + } + + if (args->default_val) { + argv[argc++] = "--entry-text"; + argv[argc++] = args->default_val; + } + + if (args->accept) { + argv[argc++] = "--ok-label"; + argv[argc++] = args->accept; + } + + if (args->cancel) { + argv[argc++] = "--cancel-label"; + argv[argc++] = args->cancel; + } + + argv[argc++] = NULL; + return args; + +cleanup: + free_zenity_input_args(args); + return NULL; +} + +static int run_zenity_input_thread(void *ptr) +{ + zenityInputArgs *args = ptr; + run_input_zenity(args->callback, args->userdata, args->argv); + free_zenity_input_args(args); + return 0; +} + +void SDL_SYS_ShowInputDialogWithProperties(SDL_DialogInputCallback callback, void *userdata, SDL_PropertiesID props) +{ + zenityInputArgs *args = create_zenity_input_args(callback, userdata, props); + if (!args) { + callback(userdata, NULL, SDL_DIALOGRESULT_ERROR); + return; + } + + SDL_Thread *thread = SDL_CreateThread(run_zenity_input_thread, "SDL_ZenityInputDialog", (void *)args); + + if (!thread) { + free_zenity_input_args(args); + callback(userdata, NULL, SDL_DIALOGRESULT_ERROR); + return; + } + + SDL_DetachThread(thread); +} + +struct SDL_ProgressDialog +{ + SDL_Process* proc; + + // SDL_ProgressDialog will be returned before the SDL_Process is created; + // those variables handle the deferring of updating the progress value. + enum { + SDL_ZENITY_NOTSTARTED = 0, + SDL_ZENITY_STARTING, + SDL_ZENITY_RUNNING + } status; + bool cancel; + // The mutex is used for: + // - Controlling early calls to SDL_UpdateProgressDialog while the process + // is launching; and + // - Controlling when the thread and the caller wish to release this struct + // at the same time. + SDL_Mutex* mutex; + bool destroyed_by_caller; // When the thread wishes to destroy this struct, + // it will set `proc` to NULL. + float deferred_set_progress; + char *deferred_set_prompt; +}; + +SDL_ProgressDialog* create_zenity_progress_info(void) +{ + SDL_ProgressDialog* dialog = SDL_calloc(1, sizeof(SDL_ProgressDialog)); + + if (!dialog) { + return NULL; + } + + dialog->mutex = SDL_CreateMutex(); + + if (!dialog->mutex) { + SDL_free(dialog); + return NULL; + } + + return dialog; +} + +void free_zenity_progress_info(SDL_ProgressDialog* dialog) +{ + SDL_DestroyMutex(dialog->mutex); + + if (dialog->proc) { + SDL_KillProcess(dialog->proc, true); + SDL_DestroyProcess(dialog->proc); + } + + SDL_free(dialog); +} + +void zenity_set_dialog_status(SDL_ProgressDialog* dialog, float progress, const char *new_prompt) +{ + int size; + char *str = NULL; + int written; + + if (!dialog || !dialog->proc || progress < 0.0f || progress > 1.0f) { + return; + } + + // "# " + new_prompt + "\n" + "100\n\0" + size = (new_prompt ? SDL_strlen(new_prompt) + 3 : 0) + 5; + str = SDL_malloc(size); + + if (!str) { + goto done; + } + + SDL_IOStream* in = SDL_GetProcessInput(dialog->proc); + + if (!in) { + goto done; + } + + if (new_prompt) { + written = SDL_snprintf(str, size, "# %s\n%d\n", new_prompt, (int) SDL_roundf(progress * 100.0f)); + } else { + written = SDL_snprintf(str, size, "%d\n", (int) SDL_roundf(progress * 100.0f)); + } + + if (written < size - 3 || written > size - 1) { + goto done; + } + + SDL_WriteIO(in, str, written); + // FIXME: Can't flush stdin? + //SDL_FlushIO(in); + +done: + SDL_free(str); +} + +static void run_progress_zenity(SDL_DialogProgressCallback callback, void *userdata, SDL_ProgressDialog* dialog, void *argv) +{ + SDL_Environment *env = NULL; + SDL_Process *process = NULL; + int exitcode = -1; + + env = SDL_CreateEnvironment(true); + if (!env) { + goto done; + } + + /* Recent versions of Zenity have different exit codes, but picks up + different codes from the environment */ + SDL_SetEnvironmentVariable(env, "ZENITY_OK", "0", true); + SDL_SetEnvironmentVariable(env, "ZENITY_CANCEL", "1", true); + SDL_SetEnvironmentVariable(env, "ZENITY_ESC", "1", true); + SDL_SetEnvironmentVariable(env, "ZENITY_EXTRA", "2", true); + SDL_SetEnvironmentVariable(env, "ZENITY_ERROR", "2", true); + SDL_SetEnvironmentVariable(env, "ZENITY_TIMEOUT", "2", true); + + SDL_PropertiesID props = SDL_CreateProperties(); + SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ARGS_POINTER, argv); + SDL_SetPointerProperty(props, SDL_PROP_PROCESS_CREATE_ENVIRONMENT_POINTER, env); + SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDIN_NUMBER, SDL_PROCESS_STDIO_APP); + SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDOUT_NUMBER, SDL_PROCESS_STDIO_NULL); + SDL_SetNumberProperty(props, SDL_PROP_PROCESS_CREATE_STDERR_NUMBER, SDL_PROCESS_STDIO_NULL); + process = SDL_CreateProcessWithProperties(props); + SDL_DestroyProperties(props); + if (!process) { + goto done; + } + + dialog->proc = process; + + SDL_LockMutex(dialog->mutex); + dialog->status = SDL_ZENITY_STARTING; + + if (dialog->destroyed_by_caller) { + // Force because Zenity ignores signals otherwise + SDL_KillProcess(process, true); + } else { + zenity_set_dialog_status(dialog, dialog->deferred_set_progress, dialog->deferred_set_prompt); + } + SDL_free(dialog->deferred_set_prompt); + + dialog->status = SDL_ZENITY_RUNNING; + SDL_UnlockMutex(dialog->mutex); + + SDL_WaitProcess(dialog->proc, true, &exitcode); + + if (dialog->cancel || exitcode == 1) { + callback(userdata, SDL_DIALOGRESULT_CANCEL); + } else if (exitcode) { + callback(userdata, SDL_DIALOGRESULT_ERROR); + } else { + callback(userdata, SDL_DIALOGRESULT_SUCCESS); + } + +done: + // SDL_ProcessDialog* can be destroyed either by this thread or by the + // SDL_DestroyProgressDialog function, whichever happens last. + SDL_LockMutex(dialog->mutex); + SDL_DestroyProcess(process); + dialog->proc = NULL; + if (dialog->destroyed_by_caller) { + // The caller has already finished with the dialog, so it's safe to + // unlock it before we're done. + SDL_UnlockMutex(dialog->mutex); + free_zenity_progress_info(dialog); + } else { + // In the condition above, the mutex will have been destroyed, so it's + // no longer safe to unlock it. + SDL_UnlockMutex(dialog->mutex); + } + + SDL_DestroyEnvironment(env); +} + +typedef struct +{ + SDL_DialogProgressCallback callback; + void *userdata; + void *argv; + + char *title; + char *prompt; + char *default_val; + char *accept; + char *cancel; + + SDL_ProgressDialog* dialog_info; +} zenityProgressArgs; + +static void free_zenity_progress_args(zenityProgressArgs *args) +{ + SDL_free(args->title); + SDL_free(args->prompt); + SDL_free(args->default_val); + SDL_free(args->accept); + SDL_free(args->cancel); + SDL_free(args->argv); + // Don't free args->dialog_info; this will be freed by the caller + SDL_free(args); +} + +static zenityProgressArgs *create_zenity_progress_args(SDL_DialogProgressCallback callback, void *userdata, SDL_PropertiesID props) +{ + zenityProgressArgs *args = SDL_calloc(1, sizeof(zenityProgressArgs)); + if (!args) { + return NULL; + } + + const char **argv = SDL_calloc(sizeof(*argv), + 2 /* zenity --progress */ + + 2 /* --title [title] */ + + 2 /* --text [prompt] */ + + 2 /* --ok-label [label] */ + + 2 /* --cancel-label [label] */ + + 4 /* --no-cancel --pulsate --time-remaining --auto-close */ + + 1 /* NULL */); + + if (!argv) { + goto cleanup; + } + + args->argv = argv; + args->callback = callback; + args->userdata = userdata; + + args->dialog_info = create_zenity_progress_info(); + if (!args->dialog_info) { + goto cleanup; + } + + /* Properties can be destroyed as soon as the function returns; copy over what we need. */ +#define COPY_STRING_PROPERTY(dst, prop) \ + { \ + const char *str = SDL_GetStringProperty(props, prop, NULL); \ + if (str) { \ + dst = SDL_strdup(str); \ + if (!dst) { \ + goto cleanup; \ + } \ + } \ + } + + COPY_STRING_PROPERTY(args->title, SDL_PROP_INPUT_DIALOG_TITLE_STRING); + COPY_STRING_PROPERTY(args->prompt, SDL_PROP_INPUT_DIALOG_PROMPT_STRING); + COPY_STRING_PROPERTY(args->default_val, SDL_PROP_INPUT_DIALOG_DEFAULT_STRING); + COPY_STRING_PROPERTY(args->accept, SDL_PROP_INPUT_DIALOG_ACCEPT_STRING); + COPY_STRING_PROPERTY(args->cancel, SDL_PROP_INPUT_DIALOG_CANCEL_STRING); +#undef COPY_STRING_PROPERTY + + int argc = 0; + argv[argc++] = "zenity"; + argv[argc++] = "--progress"; + + if (args->title) { + argv[argc++] = "--title"; + argv[argc++] = args->title; + } + + if (args->prompt) { + argv[argc++] = "--text"; + argv[argc++] = args->prompt; + } + + if (args->accept) { + argv[argc++] = "--ok-label"; + argv[argc++] = args->accept; + } + + if (args->cancel) { + argv[argc++] = "--cancel-label"; + argv[argc++] = args->cancel; + } + + if (!SDL_GetBooleanProperty(props, SDL_PROP_PROGRESS_DIALOG_CAN_CANCEL_BOOLEAN, true)) { + argv[argc++] = "--no-cancel"; + } + + if (SDL_GetBooleanProperty(props, SDL_PROP_PROGRESS_DIALOG_INDEFINITE_BOOLEAN, false)) { + argv[argc++] = "--pulsate"; + } + + if (SDL_GetBooleanProperty(props, SDL_PROP_PROGRESS_DIALOG_SHOW_TIME_REMAINING_BOOLEAN, false)) { + argv[argc++] = "--time-remaining"; + } + + if (SDL_GetBooleanProperty(props, SDL_PROP_PROGRESS_DIALOG_AUTO_CLOSE_BOOLEAN, false)) { + argv[argc++] = "--auto-close"; + } + + argv[argc++] = NULL; + return args; + +cleanup: + free_zenity_progress_args(args); + return NULL; +} + +static int run_zenity_progress_thread(void *ptr) +{ + zenityProgressArgs *args = ptr; + run_progress_zenity(args->callback, args->userdata, args->dialog_info, args->argv); + free_zenity_progress_args(args); + return 0; +} + +SDL_ProgressDialog* SDL_SYS_ShowProgressDialogWithProperties(SDL_DialogProgressCallback callback, void *userdata, SDL_PropertiesID props) +{ + zenityProgressArgs *args = create_zenity_progress_args(callback, userdata, props); + if (!args) { + callback(userdata, SDL_DIALOGRESULT_ERROR); + return NULL; + } + + SDL_Thread *thread = SDL_CreateThread(run_zenity_progress_thread, "SDL_ZenityProgressDialog", (void *)args); + + if (!thread) { + free_zenity_progress_args(args); + callback(userdata, SDL_DIALOGRESULT_ERROR); + return NULL; + } + + SDL_DetachThread(thread); + + return args->dialog_info; +} + +void SDL_SYS_UpdateProgressDialog(SDL_ProgressDialog* dialog, float progress, const char *new_prompt) +{ + if (!dialog->proc) { + // The dialog has already finished. + return; + } + + // Ordering is important to avoid race conditions + dialog->deferred_set_progress = progress; + if (new_prompt) { + dialog->deferred_set_prompt = SDL_strdup(new_prompt); + } + + // If Zenity hasn't yet started, the value will be picked up when it will + // have started. + if (dialog->status == SDL_ZENITY_STARTING) + { + SDL_LockMutex(dialog->mutex); + zenity_set_dialog_status(dialog, progress, new_prompt); + SDL_UnlockMutex(dialog->mutex); + } + else if (dialog->status == SDL_ZENITY_RUNNING) + { + zenity_set_dialog_status(dialog, progress, new_prompt); + } +} + +void SDL_SYS_DestroyProgressDialog(SDL_ProgressDialog* dialog) +{ + dialog->cancel = true; + + // SDL_ProcessDialog* can be destroyed either by the thread or by this + // function, whichever happens last. + SDL_LockMutex(dialog->mutex); + dialog->destroyed_by_caller = true; + + if (dialog->status == SDL_ZENITY_NOTSTARTED) { + // The program will never be started + SDL_UnlockMutex(dialog->mutex); + } else if (dialog->proc) { + // Force-kill, because Zenity seemingly ignores killing otherwise + SDL_KillProcess(dialog->proc, true); + SDL_UnlockMutex(dialog->mutex); + } else { + // The thread has already finished with the dialog, so it's safe to + // unlock it before we're done. + SDL_UnlockMutex(dialog->mutex); + free_zenity_progress_info(dialog); + } +} + +static void run_color_zenity(SDL_DialogColorCallback callback, void *userdata, void *argv) +{ + SDL_DialogResult dialog_result; + size_t bytes_read = 0; + char *input = NULL; + bool result = false; + SDL_Color color; + int r, g, b; + float a; + + color.r = 0; + color.g = 0; + color.b = 0; + color.a = 0; + + input = exec_zenity(argv, &dialog_result, &bytes_read); + + if (!input) { + goto done; + } + + if (dialog_result == SDL_DIALOGRESULT_SUCCESS) { + if (SDL_sscanf(input, "rgba(%d,%d,%d,%f)", &r, &g, &b, &a) == 4) { + color.r = r; + color.g = g; + color.b = b; + color.a = (Uint8) (a * 255.0f); + } else if (SDL_sscanf(input, "rgb(%d,%d,%d)", &r, &g, &b) == 3) { + color.r = r; + color.g = g; + color.b = b; + color.a = SDL_ALPHA_OPAQUE; + } else { + SDL_SetError("Unexpected rgb format from Zenity: %s", input); + } + } + + callback(userdata, color, dialog_result); + result = true; + +done: + SDL_free(input); + + if (!result) { + callback(userdata, color, SDL_DIALOGRESULT_ERROR); + } +} + +typedef struct +{ + SDL_DialogColorCallback callback; + void *userdata; + void *argv; + + char *title; + char *prompt; + char *default_val; + char *accept; + char *cancel; +} zenityColorArgs; + +static void free_zenity_color_args(zenityColorArgs *args) +{ + SDL_free(args->title); + SDL_free(args->prompt); + SDL_free(args->default_val); + SDL_free(args->accept); + SDL_free(args->cancel); + SDL_free(args->argv); + SDL_free(args); +} + +static zenityColorArgs *create_zenity_color_args(SDL_DialogColorCallback callback, void *userdata, SDL_PropertiesID props) +{ + zenityColorArgs *args = SDL_calloc(1, sizeof(zenityColorArgs)); + if (!args) { + return NULL; + } + + const char **argv = SDL_calloc(sizeof(*argv), + 2 /* zenity --color-selection */ + + 2 /* --title [title] */ + + 2 /* --text [prompt] */ + + 2 /* --color [default_val] */ + + 2 /* --ok-label [label] */ + + 2 /* --cancel-label [label] */ + + 1 /* NULL */); + + if (!argv) { + goto cleanup; + } + + args->argv = argv; + args->callback = callback; + args->userdata = userdata; + + /* Properties can be destroyed as soon as the function returns; copy over what we need. */ +#define COPY_STRING_PROPERTY(dst, prop) \ + { \ + const char *str = SDL_GetStringProperty(props, prop, NULL); \ + if (str) { \ + dst = SDL_strdup(str); \ + if (!dst) { \ + goto cleanup; \ + } \ + } \ + } + + COPY_STRING_PROPERTY(args->title, SDL_PROP_COLOR_DIALOG_TITLE_STRING); + COPY_STRING_PROPERTY(args->prompt, SDL_PROP_COLOR_DIALOG_PROMPT_STRING); + COPY_STRING_PROPERTY(args->accept, SDL_PROP_COLOR_DIALOG_ACCEPT_STRING); + COPY_STRING_PROPERTY(args->cancel, SDL_PROP_COLOR_DIALOG_CANCEL_STRING); +#undef COPY_STRING_PROPERTY + + int argc = 0; + argv[argc++] = "zenity"; + argv[argc++] = "--color-selection"; + + if (args->title) { + argv[argc++] = "--title"; + argv[argc++] = args->title; + } + + if (args->prompt) { + argv[argc++] = "--text"; + argv[argc++] = args->prompt; + } + + SDL_Color *col = SDL_GetPointerProperty(props, SDL_PROP_COLOR_DIALOG_DEFAULT_POINTER, NULL); + if (col) { + args->default_val = SDL_malloc(32 * sizeof(char)); // "rgba(255,255,255,0.99999999999)" + '\0' + + if (!args->default_val) { + goto cleanup; + } + + int written = SDL_snprintf(args->default_val, 32 * sizeof(char), "rgba(%d,%d,%d,%f)", col->r, col->g, col->b, ((float) col->a) / 255.0f); + + if (written < 13 || written > 32) { + goto cleanup; + } + + argv[argc++] = "--color"; + argv[argc++] = args->default_val; + } + + if (args->accept) { + argv[argc++] = "--ok-label"; + argv[argc++] = args->accept; + } + + if (args->cancel) { + argv[argc++] = "--cancel-label"; + argv[argc++] = args->cancel; + } + + argv[argc++] = NULL; + return args; + +cleanup: + free_zenity_color_args(args); + return NULL; +} + +static int run_zenity_color_thread(void *ptr) +{ + zenityColorArgs *args = ptr; + run_color_zenity(args->callback, args->userdata, args->argv); + free_zenity_color_args(args); + return 0; +} + +void SDL_SYS_ShowColorPickerDialogWithProperties(SDL_DialogColorCallback callback, void *userdata, SDL_PropertiesID props) +{ + SDL_Color dummy; + dummy.r = 0; + dummy.g = 0; + dummy.b = 0; + dummy.a = 0; + + zenityColorArgs *args = create_zenity_color_args(callback, userdata, props); + if (!args) { + callback(userdata, dummy, SDL_DIALOGRESULT_ERROR); + return; + } + + SDL_Thread *thread = SDL_CreateThread(run_zenity_color_thread, "SDL_ZenityColorDialog", (void *)args); + + if (!thread) { + free_zenity_color_args(args); + callback(userdata, dummy, SDL_DIALOGRESULT_ERROR); + return; + } + + SDL_DetachThread(thread); +} + +static void run_date_zenity(SDL_DialogDateCallback callback, void *userdata, void *argv) +{ + SDL_DialogResult dialog_result; + size_t bytes_read = 0; + char *input = NULL; + bool result = false; + SDL_Date date; + int y, m, d; + + date.y = 0; + date.m = 0; + date.d = 0; + + input = exec_zenity(argv, &dialog_result, &bytes_read); + + if (!input) { + goto done; + } + + if (dialog_result == SDL_DIALOGRESULT_SUCCESS) { + if (SDL_sscanf(input, "%d-%d-%d", &y, &m, &d) == 3) { + date.y = y; + date.m = m; + date.d = d; + } else { + SDL_SetError("Unexpected date format from Zenity: %s", input); + } + } + + callback(userdata, date, dialog_result); + result = true; + +done: + SDL_free(input); + + if (!result) { + callback(userdata, date, SDL_DIALOGRESULT_ERROR); + } +} + +typedef struct +{ + SDL_DialogDateCallback callback; + void *userdata; + void *argv; + + char *title; + char *prompt; + char *default_day; + char *default_month; + char *default_year; + char *accept; + char *cancel; +} zenityDateArgs; + +static void free_zenity_date_args(zenityDateArgs *args) +{ + SDL_free(args->title); + SDL_free(args->prompt); + SDL_free(args->default_day); + SDL_free(args->default_month); + SDL_free(args->default_year); + SDL_free(args->accept); + SDL_free(args->cancel); + SDL_free(args->argv); + SDL_free(args); +} + +static zenityDateArgs *create_zenity_date_args(SDL_DialogDateCallback callback, void *userdata, SDL_PropertiesID props) +{ + zenityDateArgs *args = SDL_calloc(1, sizeof(zenityDateArgs)); + if (!args) { + return NULL; + } + + const char **argv = SDL_calloc(sizeof(*argv), + 2 /* zenity --color-selection */ + + 2 /* --title [title] */ + + 2 /* --text [prompt] */ + + 6 /* --year [y] --month [m] --day [d] */ + + 2 /* --ok-label [label] */ + + 2 /* --cancel-label [label] */ + + 1 /* NULL */); + + if (!argv) { + goto cleanup; + } + + args->argv = argv; + args->callback = callback; + args->userdata = userdata; + + /* Properties can be destroyed as soon as the function returns; copy over what we need. */ +#define COPY_STRING_PROPERTY(dst, prop) \ + { \ + const char *str = SDL_GetStringProperty(props, prop, NULL); \ + if (str) { \ + dst = SDL_strdup(str); \ + if (!dst) { \ + goto cleanup; \ + } \ + } \ + } + + COPY_STRING_PROPERTY(args->title, SDL_PROP_DATE_DIALOG_TITLE_STRING); + COPY_STRING_PROPERTY(args->prompt, SDL_PROP_DATE_DIALOG_PROMPT_STRING); + COPY_STRING_PROPERTY(args->accept, SDL_PROP_DATE_DIALOG_ACCEPT_STRING); + COPY_STRING_PROPERTY(args->cancel, SDL_PROP_DATE_DIALOG_CANCEL_STRING); +#undef COPY_STRING_PROPERTY + + int argc = 0; + argv[argc++] = "zenity"; + argv[argc++] = "--calendar"; + + if (args->title) { + argv[argc++] = "--title"; + argv[argc++] = args->title; + } + + if (args->prompt) { + argv[argc++] = "--text"; + argv[argc++] = args->prompt; + } + + SDL_Date *date = SDL_GetPointerProperty(props, SDL_PROP_DATE_DIALOG_DEFAULT_POINTER, NULL); + if (date) { + if (date->y != 0) { + args->default_year = SDL_malloc(5 * sizeof(char)); // "9999" + '\0' (Zenity doesn't support years past 9999) + + if (!args->default_year) { + goto cleanup; + } + + int written = SDL_snprintf(args->default_year, 5 * sizeof(char), "%d", date->y); + + if (written < 1 || written > 4) { + goto cleanup; + } + + argv[argc++] = "--year"; + argv[argc++] = args->default_year; + } + + if (date->m != 0) { + args->default_month = SDL_malloc(3 * sizeof(char)); // "12" + '\0' + + if (!args->default_month) { + goto cleanup; + } + + int written = SDL_snprintf(args->default_month, 3 * sizeof(char), "%d", date->m); + + if (written < 1 || written > 2) { + goto cleanup; + } + + argv[argc++] = "--month"; + argv[argc++] = args->default_month; + } + + if (date->d != 0) { + args->default_day = SDL_malloc(3 * sizeof(char)); // "31" + '\0' + + if (!args->default_day) { + goto cleanup; + } + + int written = SDL_snprintf(args->default_day, 3 * sizeof(char), "%d", date->d); + + if (written < 1 || written > 2) { + goto cleanup; + } + + argv[argc++] = "--day"; + argv[argc++] = args->default_day; + } + } + + if (args->accept) { + argv[argc++] = "--ok-label"; + argv[argc++] = args->accept; + } + + if (args->cancel) { + argv[argc++] = "--cancel-label"; + argv[argc++] = args->cancel; + } + + argv[argc++] = NULL; + return args; + +cleanup: + free_zenity_date_args(args); + return NULL; +} + +static int run_zenity_date_thread(void *ptr) +{ + zenityDateArgs *args = ptr; + run_date_zenity(args->callback, args->userdata, args->argv); + free_zenity_date_args(args); + return 0; +} + +void SDL_SYS_ShowDatePickerDialogWithProperties(SDL_DialogDateCallback callback, void *userdata, SDL_PropertiesID props) +{ + SDL_Date dummy; + dummy.y = 0; + dummy.m = 0; + dummy.d = 0; + + zenityDateArgs *args = create_zenity_date_args(callback, userdata, props); + if (!args) { + callback(userdata, dummy, SDL_DIALOGRESULT_ERROR); + return; + } + + SDL_Thread *thread = SDL_CreateThread(run_zenity_date_thread, "SDL_ZenityDateDialog", (void *)args); + + if (!thread) { + free_zenity_date_args(args); + callback(userdata, dummy, SDL_DIALOGRESULT_ERROR); + return; + } + + SDL_DetachThread(thread); +} diff --git a/src/dialog/windows/SDL_windowsdialog.c b/src/dialog/windows/SDL_windowsdialog.c index 364753cfdfaa4..c9870b0d778c7 100644 --- a/src/dialog/windows/SDL_windowsdialog.c +++ b/src/dialog/windows/SDL_windowsdialog.c @@ -617,3 +617,117 @@ void SDL_SYS_ShowFileDialogWithProperties(SDL_FileDialogType type, SDL_DialogFil break; }; } + +void SDL_SYS_ShowInputDialogWithProperties(SDL_DialogInputCallback callback, void *userdata, SDL_PropertiesID props) +{ + SDL_Unsupported(); + callback(userdata, NULL, SDL_DIALOGRESULT_ERROR); +} + +SDL_ProgressDialog* SDL_SYS_ShowProgressDialogWithProperties(SDL_DialogProgressCallback callback, void *userdata, SDL_PropertiesID props) +{ + SDL_Unsupported(); + callback(userdata, SDL_DIALOGRESULT_ERROR); + + // In case the callback calls SDL_SetError() + SDL_Unsupported(); + return NULL; +} + +void SDL_SYS_UpdateProgressDialog(SDL_ProgressDialog* dialog, float progress, const char *new_prompt) +{ + SDL_Unsupported(); +} + +void SDL_SYS_DestroyProgressDialog(SDL_ProgressDialog* dialog) +{ + SDL_Unsupported(); +} + +void SDL_SYS_ShowColorPickerDialogWithProperties(SDL_DialogColorCallback callback, void *userdata, SDL_PropertiesID props) +{ + SDL_Window *window = SDL_GetPointerProperty(props, SDL_PROP_COLOR_DIALOG_WINDOW_POINTER, NULL); + SDL_Color *default_color = SDL_GetPointerProperty(props, SDL_PROP_COLOR_DIALOG_DEFAULT_POINTER, NULL); + SDL_Color result; + + result.r = 0; + result.g = 0; + result.b = 0; + result.a = 0; + + typedef BOOL (WINAPI *pfnChooseColorW)(LPCHOOSECOLOR); + typedef DWORD (WINAPI *pfnCommDlgExtendedError)(void); + HMODULE lib = LoadLibraryW(L"Comdlg32.dll"); + pfnChooseColorW pChooseColor = NULL; + pfnCommDlgExtendedError pCommDlgExtendedError = NULL; + + if (lib) { + pChooseColor = (pfnChooseColorW) GetProcAddress(lib, "ChooseColorW"); + pCommDlgExtendedError = (pfnCommDlgExtendedError) GetProcAddress(lib, "CommDlgExtendedError"); + } else { + SDL_SetError("Couldn't load Comdlg32.dll"); + callback(userdata, result, SDL_DIALOGRESULT_ERROR); + return; + } + + if (!pChooseColor) { + SDL_SetError("Couldn't load ChooseColor from library"); + callback(userdata, result, SDL_DIALOGRESULT_ERROR); + return; + } + + if (!pCommDlgExtendedError) { + SDL_SetError("Couldn't load CommDlgExtendedError from library"); + callback(userdata, result, SDL_DIALOGRESULT_ERROR); + return; + } + + // Custom color history is generally expected to persist across invocations + static COLORREF acrCustClr[16]; + + CHOOSECOLOR cc; + + ZeroMemory(&cc, sizeof(cc)); + cc.lStructSize = sizeof(cc); + cc.lpCustColors = acrCustClr; + cc.Flags = CC_FULLOPEN | CC_RGBINIT; + + if (window) { + HWND hwnd = (HWND) SDL_GetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL); + if (hwnd) { + cc.hwndOwner = hwnd; + } + } + + if (default_color) { + cc.rgbResult = RGB(default_color->r, default_color->g, default_color->b); + } + + if (pChooseColor(&cc)) { + result.r = GetRValue(cc.rgbResult); + result.g = GetGValue(cc.rgbResult); + result.b = GetBValue(cc.rgbResult); + result.a = SDL_ALPHA_OPAQUE; + callback(userdata, result, SDL_DIALOGRESULT_SUCCESS); + } else { + DWORD error = pCommDlgExtendedError(); + // Error code 0 means the user clicked the cancel button. + if (error == 0) { + callback(userdata, result, SDL_DIALOGRESULT_CANCEL); + } else { + WIN_SetError("Error while invoking color picker dialog"); + callback(userdata, result, SDL_DIALOGRESULT_ERROR); + } + } +} + +void SDL_SYS_ShowDatePickerDialogWithProperties(SDL_DialogDateCallback callback, void *userdata, SDL_PropertiesID props) +{ + SDL_Date date; + date.y = 0; + date.m = 0; + date.d = 0; + + SDL_Unsupported(); + callback(userdata, date, SDL_DIALOGRESULT_ERROR); +} diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index 91bd4300c732f..e8ed1c79cb39c 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -1254,6 +1254,12 @@ SDL3_0.0.0 { SDL_SetAudioIterationCallbacks; SDL_GetEventDescription; SDL_PutAudioStreamDataNoCopy; + SDL_ShowInputDialogWithProperties; + SDL_ShowProgressDialogWithProperties; + SDL_UpdateProgressDialog; + SDL_DestroyProgressDialog; + SDL_ShowColorPickerDialogWithProperties; + SDL_ShowDatePickerDialogWithProperties; # extra symbols go here (don't modify this line) local: *; }; diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h index fcc0bad7b25ff..80e899cc9361b 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -1279,3 +1279,9 @@ #define SDL_SetAudioIterationCallbacks SDL_SetAudioIterationCallbacks_REAL #define SDL_GetEventDescription SDL_GetEventDescription_REAL #define SDL_PutAudioStreamDataNoCopy SDL_PutAudioStreamDataNoCopy_REAL +#define SDL_ShowInputDialogWithProperties SDL_ShowInputDialogWithProperties_REAL +#define SDL_ShowProgressDialogWithProperties SDL_ShowProgressDialogWithProperties_REAL +#define SDL_UpdateProgressDialog SDL_UpdateProgressDialog_REAL +#define SDL_DestroyProgressDialog SDL_DestroyProgressDialog_REAL +#define SDL_ShowColorPickerDialogWithProperties SDL_ShowColorPickerDialogWithProperties_REAL +#define SDL_ShowDatePickerDialogWithProperties SDL_ShowDatePickerDialogWithProperties_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index 60e0dfea5728f..061903a99e6e2 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -1287,3 +1287,9 @@ SDL_DYNAPI_PROC(bool,SDL_PutAudioStreamPlanarData,(SDL_AudioStream *a,const void SDL_DYNAPI_PROC(bool,SDL_SetAudioIterationCallbacks,(SDL_AudioDeviceID a,SDL_AudioIterationCallback b,SDL_AudioIterationCallback c,void *d),(a,b,c,d),return) SDL_DYNAPI_PROC(int,SDL_GetEventDescription,(const SDL_Event *a,char *b,int c),(a,b,c),return) SDL_DYNAPI_PROC(bool,SDL_PutAudioStreamDataNoCopy,(SDL_AudioStream *a,const void *b,int c,SDL_AudioStreamDataCompleteCallback d,void *e),(a,b,c,d,e),return) +SDL_DYNAPI_PROC(void,SDL_ShowInputDialogWithProperties,(SDL_DialogInputCallback a,void *b,SDL_PropertiesID c),(a,b,c),) +SDL_DYNAPI_PROC(SDL_ProgressDialog*,SDL_ShowProgressDialogWithProperties,(SDL_DialogProgressCallback a,void *b,SDL_PropertiesID c),(a,b,c),return) +SDL_DYNAPI_PROC(void,SDL_UpdateProgressDialog,(SDL_ProgressDialog *a,float b,const char *c),(a,b,c),) +SDL_DYNAPI_PROC(void,SDL_DestroyProgressDialog,(SDL_ProgressDialog *a),(a),) +SDL_DYNAPI_PROC(void,SDL_ShowColorPickerDialogWithProperties,(SDL_DialogColorCallback a,void *b,SDL_PropertiesID c),(a,b,c),) +SDL_DYNAPI_PROC(void,SDL_ShowDatePickerDialogWithProperties,(SDL_DialogDateCallback a,void *b,SDL_PropertiesID c),(a,b,c),) diff --git a/test/testdialog.c b/test/testdialog.c index dcff5bd9f00d5..31b22620346a5 100644 --- a/test/testdialog.c +++ b/test/testdialog.c @@ -22,7 +22,7 @@ const SDL_DialogFileFilter filters[] = { { "PNG images", "png" } }; -static void SDLCALL callback(void *userdata, const char * const *files, int filter) { +static void SDLCALL file_callback(void *userdata, const char * const *files, int filter) { if (files) { const char* filter_name = "(filter fetching unsupported)"; @@ -45,16 +45,109 @@ static void SDLCALL callback(void *userdata, const char * const *files, int filt } } +static void SDLCALL input_callback(void *userdata, const char *input, SDL_DialogResult result) { + switch (result) + { + case SDL_DIALOGRESULT_ERROR: + SDL_Log("Error: %s\n", SDL_GetError()); + break; + + case SDL_DIALOGRESULT_CANCEL: + SDL_Log("Cancel\n"); + break; + + case SDL_DIALOGRESULT_SUCCESS: + SDL_Log("'%s'\n", input); + break; + + default: + SDL_Log("Unknown result: %d\n", result); + } +} + +static bool progress_done = false; + +static void SDLCALL progress_callback(void* dialog, SDL_DialogResult result) +{ + switch (result) + { + case SDL_DIALOGRESULT_ERROR: + SDL_Log("Error: %s\n", SDL_GetError()); + break; + + case SDL_DIALOGRESULT_CANCEL: + SDL_Log("Cancel\n"); + break; + + case SDL_DIALOGRESULT_SUCCESS: + SDL_Log("Success\n"); + break; + + default: + SDL_Log("Unknown result: %d\n", result); + } + + progress_done = true; +} + +static void SDLCALL color_callback(void* userdata, SDL_Color c, SDL_DialogResult result) +{ + switch (result) + { + case SDL_DIALOGRESULT_ERROR: + SDL_Log("Error: %s\n", SDL_GetError()); + break; + + case SDL_DIALOGRESULT_CANCEL: + SDL_Log("Cancel\n"); + break; + + case SDL_DIALOGRESULT_SUCCESS: + SDL_Log("%d, %d, %d, %d\n", c.r, c.g, c.b, c.a); + break; + + default: + SDL_Log("Unknown result: %d\n", result); + } +} + +static void SDLCALL date_callback(void* userdata, SDL_Date d, SDL_DialogResult result) +{ + switch (result) + { + case SDL_DIALOGRESULT_ERROR: + SDL_Log("Error: %s\n", SDL_GetError()); + break; + + case SDL_DIALOGRESULT_CANCEL: + SDL_Log("Cancel\n"); + break; + + case SDL_DIALOGRESULT_SUCCESS: + SDL_Log("%04d-%02d-%02d\n", d.y, d.m, d.d); + break; + + default: + SDL_Log("Unknown result: %d\n", result); + } +} + int main(int argc, char *argv[]) { SDL_Window *w; SDL_Renderer *r; SDLTest_CommonState *state; - const SDL_FRect open_file_rect = { 50, 50, 220, 140 }; - const SDL_FRect save_file_rect = { 50, 290, 220, 140 }; - const SDL_FRect open_folder_rect = { 370, 50, 220, 140 }; + const SDL_FRect open_file_rect = { 50, 50, 220, 70 }; + const SDL_FRect save_file_rect = { 370, 50, 220, 70 }; + const SDL_FRect open_folder_rect = { 50, 140, 220, 70 }; + const SDL_FRect input_rect = { 370, 140, 220, 70 }; + const SDL_FRect progress_rect = { 50, 230, 220, 70 }; + const SDL_FRect color_rect = { 370, 230, 220, 70 }; + const SDL_FRect date_rect = { 50, 320, 220, 70 }; int i; const char *initial_path = NULL; + SDL_ProgressDialog* progress_dialog = NULL; + int progress; /* Initialize test framework */ state = SDLTest_CommonCreateState(argv, 0); @@ -112,11 +205,37 @@ int main(int argc, char *argv[]) * - Nonzero if the user is allowed to choose multiple entries (not for SDL_ShowSaveFileDialog) */ if (SDL_PointInRectFloat(&p, &open_file_rect)) { - SDL_ShowOpenFileDialog(callback, NULL, w, filters, SDL_arraysize(filters), initial_path, 1); + SDL_ShowOpenFileDialog(file_callback, NULL, w, filters, SDL_arraysize(filters), initial_path, 1); } else if (SDL_PointInRectFloat(&p, &open_folder_rect)) { - SDL_ShowOpenFolderDialog(callback, NULL, w, initial_path, 1); + SDL_ShowOpenFolderDialog(file_callback, NULL, w, initial_path, 1); } else if (SDL_PointInRectFloat(&p, &save_file_rect)) { - SDL_ShowSaveFileDialog(callback, NULL, w, filters, SDL_arraysize(filters), initial_path); + SDL_ShowSaveFileDialog(file_callback, NULL, w, filters, SDL_arraysize(filters), initial_path); + } else if (SDL_PointInRectFloat(&p, &input_rect)) { + SDL_PropertiesID props = SDL_CreateProperties(); + SDL_SetPointerProperty(props, SDL_PROP_INPUT_DIALOG_WINDOW_POINTER, w); + SDL_ShowInputDialogWithProperties(input_callback, NULL, props); + SDL_DestroyProperties(props); + } else if (SDL_PointInRectFloat(&p, &progress_rect)) { + if (progress_dialog) { + continue; + } + SDL_PropertiesID props = SDL_CreateProperties(); + SDL_SetPointerProperty(props, SDL_PROP_PROGRESS_DIALOG_WINDOW_POINTER, w); + progress_dialog = SDL_ShowProgressDialogWithProperties(progress_callback, NULL, props); + SDL_DestroyProperties(props); + progress = 0; + progress_done = false; + /* Check creating and immediately destroying a progress dialog */ + } else if (SDL_PointInRectFloat(&p, &color_rect)) { + SDL_PropertiesID props = SDL_CreateProperties(); + SDL_SetPointerProperty(props, SDL_PROP_COLOR_DIALOG_WINDOW_POINTER, w); + SDL_ShowColorPickerDialogWithProperties(color_callback, NULL, props); + SDL_DestroyProperties(props); + } else if (SDL_PointInRectFloat(&p, &date_rect)) { + SDL_PropertiesID props = SDL_CreateProperties(); + SDL_SetPointerProperty(props, SDL_PROP_DATE_DIALOG_WINDOW_POINTER, w); + SDL_ShowDatePickerDialogWithProperties(date_callback, NULL, props); + SDL_DestroyProperties(props); } } } @@ -137,12 +256,37 @@ int main(int argc, char *argv[]) SDL_SetRenderDrawColor(r, 0, 0, 255, SDL_ALPHA_OPAQUE); SDL_RenderFillRect(r, &open_folder_rect); + SDL_SetRenderDrawColor(r, 255, 255, 0, SDL_ALPHA_OPAQUE); + SDL_RenderFillRect(r, &input_rect); + + SDL_SetRenderDrawColor(r, 255, 0, 255, SDL_ALPHA_OPAQUE); + SDL_RenderFillRect(r, &progress_rect); + + SDL_SetRenderDrawColor(r, 0, 255, 255, SDL_ALPHA_OPAQUE); + SDL_RenderFillRect(r, &color_rect); + + SDL_SetRenderDrawColor(r, 255, 255, 255, SDL_ALPHA_OPAQUE); + SDL_RenderFillRect(r, &date_rect); + SDL_SetRenderDrawColor(r, 0, 0, 0, SDL_ALPHA_OPAQUE); SDLTest_DrawString(r, open_file_rect.x+5, open_file_rect.y+open_file_rect.h/2, "Open File..."); SDLTest_DrawString(r, save_file_rect.x+5, save_file_rect.y+save_file_rect.h/2, "Save File..."); SDLTest_DrawString(r, open_folder_rect.x+5, open_folder_rect.y+open_folder_rect.h/2, "Open Folder..."); + SDLTest_DrawString(r, input_rect.x+5, input_rect.y+input_rect.h/2, "Input test..."); + SDLTest_DrawString(r, progress_rect.x+5, progress_rect.y+progress_rect.h/2, "Progress..."); + SDLTest_DrawString(r, color_rect.x+5, color_rect.y+color_rect.h/2, "Choose color..."); + SDLTest_DrawString(r, date_rect.x+5, date_rect.y+date_rect.h/2, "Choose date..."); SDL_RenderPresent(r); + + if (progress_dialog) { + if (progress_done) { + SDL_DestroyProgressDialog(progress_dialog); + progress_dialog = NULL; + } else if (progress < 30) { + SDL_UpdateProgressDialog(progress_dialog, ((float) ++progress) / 30.0f, NULL); + } + } } SDLTest_CleanupTextDrawing();