diff --git a/code/__defines/cooldowns.dm b/code/__defines/cooldowns.dm new file mode 100644 index 0000000000..c3855d7ab3 --- /dev/null +++ b/code/__defines/cooldowns.dm @@ -0,0 +1,15 @@ +/* + * Cooldown system based on storing world.time on a variable, plus the cooldown time. + * Better performance over timer cooldowns, lower control. Same functionality. +*/ + +#define COOLDOWN_DECLARE(cd_index) var/##cd_index = 0 + +#define COOLDOWN_START(cd_source, cd_index, cd_time) (cd_source.cd_index = world.time + (cd_time)) + +//Returns true if the cooldown has run its course, false otherwise +#define COOLDOWN_FINISHED(cd_source, cd_index) (cd_source.cd_index < world.time) + +#define COOLDOWN_RESET(cd_source, cd_index) cd_source.cd_index = 0 + +#define COOLDOWN_TIMELEFT(cd_source, cd_index) (max(0, cd_source.cd_index - world.time)) \ No newline at end of file diff --git a/code/__defines/tgui.dm b/code/__defines/tgui.dm index c261e5ecdc..1215382ce7 100644 --- a/code/__defines/tgui.dm +++ b/code/__defines/tgui.dm @@ -5,6 +5,8 @@ /// Maximum ping timeout allowed to detect zombie windows #define TGUI_PING_TIMEOUT 4 SECONDS +/// Used for rate-limiting to prevent DoS by excessively refreshing a TGUI window +#define TGUI_REFRESH_FULL_UPDATE_COOLDOWN 5 SECONDS /// Window does not exist #define TGUI_WINDOW_CLOSED 0 diff --git a/code/game/machinery/newscaster.dm b/code/game/machinery/newscaster.dm index 4a40ad8ed2..eab08e2e43 100644 --- a/code/game/machinery/newscaster.dm +++ b/code/game/machinery/newscaster.dm @@ -447,7 +447,7 @@ GLOBAL_LIST_BOILERPLATE(allCasters, /obj/machinery/newscaster) return TRUE if("set_new_message") - msg = sanitize(tgui_input_message(usr, "Write your Feed story", "Network Channel Handler")) + msg = sanitize(tgui_input_text(usr, "Write your Feed story", "Network Channel Handler", multiline = TRUE)) return TRUE if("set_new_title") diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index 0f96629318..39a0ffd66d 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -617,7 +617,7 @@ var/global/floorIsLava = 0 set desc="Announce your desires to the world" if(!check_rights(0)) return - var/message = tgui_input_message(usr, "Global message to send:", "Admin Announce") + var/message = tgui_input_text(usr, "Global message to send:", "Admin Announce", multiline = TRUE) if(message) if(!check_rights(R_SERVER,0)) message = sanitize(message, 500, extra = 0) diff --git a/code/modules/client/preference_setup/general/01_basic.dm b/code/modules/client/preference_setup/general/01_basic.dm index 4211bb8add..1469c8d8f2 100644 --- a/code/modules/client/preference_setup/general/01_basic.dm +++ b/code/modules/client/preference_setup/general/01_basic.dm @@ -140,7 +140,7 @@ return TOPIC_REFRESH else if(href_list["metadata"]) - var/new_metadata = sanitize(tgui_input_message(user, "Enter any information you'd like others to see, such as Roleplay-preferences:", "Game Preference" , html_decode(pref.metadata)), extra = 0) //VOREStation Edit + var/new_metadata = sanitize(tgui_input_text(user, "Enter any information you'd like others to see, such as Roleplay-preferences:", "Game Preference" , html_decode(pref.metadata), multiline = TRUE), extra = 0) //VOREStation Edit if(new_metadata && CanUseTopic(user)) pref.metadata = new_metadata return TOPIC_REFRESH diff --git a/code/modules/client/preference_setup/global/01_ui.dm b/code/modules/client/preference_setup/global/01_ui.dm index 68c569c04e..2875c443b6 100644 --- a/code/modules/client/preference_setup/global/01_ui.dm +++ b/code/modules/client/preference_setup/global/01_ui.dm @@ -3,30 +3,34 @@ sort_order = 1 /datum/category_item/player_setup_item/player_global/ui/load_preferences(var/savefile/S) - S["UI_style"] >> pref.UI_style - S["UI_style_color"] >> pref.UI_style_color - S["UI_style_alpha"] >> pref.UI_style_alpha - S["ooccolor"] >> pref.ooccolor - S["tooltipstyle"] >> pref.tooltipstyle - S["client_fps"] >> pref.client_fps - S["ambience_freq"] >> pref.ambience_freq - S["ambience_chance"] >> pref.ambience_chance - S["tgui_fancy"] >> pref.tgui_fancy - S["tgui_lock"] >> pref.tgui_lock - S["tgui_input_mode"] >> pref.tgui_input_mode + S["UI_style"] >> pref.UI_style + S["UI_style_color"] >> pref.UI_style_color + S["UI_style_alpha"] >> pref.UI_style_alpha + S["ooccolor"] >> pref.ooccolor + S["tooltipstyle"] >> pref.tooltipstyle + S["client_fps"] >> pref.client_fps + S["ambience_freq"] >> pref.ambience_freq + S["ambience_chance"] >> pref.ambience_chance + S["tgui_fancy"] >> pref.tgui_fancy + S["tgui_lock"] >> pref.tgui_lock + S["tgui_input_mode"] >> pref.tgui_input_mode + S["tgui_large_buttons"] >> pref.tgui_large_buttons + S["tgui_swapped_buttons"] >> pref.tgui_swapped_buttons /datum/category_item/player_setup_item/player_global/ui/save_preferences(var/savefile/S) - S["UI_style"] << pref.UI_style - S["UI_style_color"] << pref.UI_style_color - S["UI_style_alpha"] << pref.UI_style_alpha - S["ooccolor"] << pref.ooccolor - S["tooltipstyle"] << pref.tooltipstyle - S["client_fps"] << pref.client_fps - S["ambience_freq"] << pref.ambience_freq - S["ambience_chance"] << pref.ambience_chance - S["tgui_fancy"] << pref.tgui_fancy - S["tgui_lock"] << pref.tgui_lock - S["tgui_input_mode"] << pref.tgui_input_mode + S["UI_style"] << pref.UI_style + S["UI_style_color"] << pref.UI_style_color + S["UI_style_alpha"] << pref.UI_style_alpha + S["ooccolor"] << pref.ooccolor + S["tooltipstyle"] << pref.tooltipstyle + S["client_fps"] << pref.client_fps + S["ambience_freq"] << pref.ambience_freq + S["ambience_chance"] << pref.ambience_chance + S["tgui_fancy"] << pref.tgui_fancy + S["tgui_lock"] << pref.tgui_lock + S["tgui_input_mode"] << pref.tgui_input_mode + S["tgui_large_buttons"] << pref.tgui_large_buttons + S["tgui_swapped_buttons"] << pref.tgui_swapped_buttons /datum/category_item/player_setup_item/player_global/ui/sanitize_preferences() pref.UI_style = sanitize_inlist(pref.UI_style, all_ui_styles, initial(pref.UI_style)) @@ -40,6 +44,8 @@ pref.tgui_fancy = sanitize_integer(pref.tgui_fancy, 0, 1, initial(pref.tgui_fancy)) pref.tgui_lock = sanitize_integer(pref.tgui_lock, 0, 1, initial(pref.tgui_lock)) pref.tgui_input_mode = sanitize_integer(pref.tgui_input_mode, 0, 1, initial(pref.tgui_input_mode)) + pref.tgui_large_buttons = sanitize_integer(pref.tgui_large_buttons, 0, 1, initial(pref.tgui_large_buttons)) + pref.tgui_swapped_buttons = sanitize_integer(pref.tgui_swapped_buttons, 0, 1, initial(pref.tgui_swapped_buttons)) /datum/category_item/player_setup_item/player_global/ui/content(var/mob/user) . = "UI Style: [pref.UI_style]
" @@ -52,7 +58,9 @@ . += "Ambience Chance: [pref.ambience_chance]
" . += "tgui Window Mode: [(pref.tgui_fancy) ? "Fancy (default)" : "Compatible (slower)"]
" . += "tgui Window Placement: [(pref.tgui_lock) ? "Primary Monitor" : "Free (default)"]
" - . += "Input Mode (Say, Me, Whisper, Subtle): [(pref.tgui_input_mode) ? "TGUI" : "BYOND (default)"]
" + . += "TGUI Input Framework: [(pref.tgui_input_mode) ? "Enabled" : "Disabled (default)"]
" + . += "TGUI Large Buttons: [(pref.tgui_large_buttons) ? "Enabled (default)" : "Disabled"]
" + . += "TGUI Swapped Buttons: [(pref.tgui_swapped_buttons) ? "Enabled" : "Disabled (default)"]
" if(can_select_ooc_color(user)) . += "OOC Color:" if(pref.ooccolor == initial(pref.ooccolor)) @@ -126,6 +134,14 @@ pref.tgui_input_mode = !pref.tgui_input_mode return TOPIC_REFRESH + else if(href_list["tgui_large_buttons"]) + pref.tgui_large_buttons = !pref.tgui_large_buttons + return TOPIC_REFRESH + + else if(href_list["tgui_swapped_buttons"]) + pref.tgui_swapped_buttons = !pref.tgui_swapped_buttons + return TOPIC_REFRESH + else if(href_list["reset"]) switch(href_list["reset"]) if("ui") diff --git a/code/modules/client/preference_setup/vore/02_size.dm b/code/modules/client/preference_setup/vore/02_size.dm index 95a944e931..6624abb1e0 100644 --- a/code/modules/client/preference_setup/vore/02_size.dm +++ b/code/modules/client/preference_setup/vore/02_size.dm @@ -87,7 +87,7 @@ return TOPIC_REFRESH else if(href_list["weight_gain"]) - var/weight_gain_rate = tgui_input_num(user, "Choose your character's rate of weight gain between 100% \ + var/weight_gain_rate = tgui_input_number(user, "Choose your character's rate of weight gain between 100% \ (full realism body fat gain) and 0% (no body fat gain).\n\ (If you want to disable weight gain, set this to 0.01 to round it to 0%.)\ ([WEIGHT_CHANGE_MIN]-[WEIGHT_CHANGE_MAX])", "Character Preference", pref.weight_gain) @@ -96,7 +96,7 @@ return TOPIC_REFRESH else if(href_list["weight_loss"]) - var/weight_loss_rate = tgui_input_num(user, "Choose your character's rate of weight loss between 100% \ + var/weight_loss_rate = tgui_input_number(user, "Choose your character's rate of weight loss between 100% \ (full realism body fat loss) and 0% (no body fat loss).\n\ (If you want to disable weight loss, set this to 0.01 round it to 0%.)\ ([WEIGHT_CHANGE_MIN]-[WEIGHT_CHANGE_MAX])", "Character Preference", pref.weight_loss) diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index d401436c3d..c06d0ab35d 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -28,7 +28,9 @@ var/list/preferences_datums = list() var/tgui_fancy = TRUE var/tgui_lock = FALSE - var/tgui_input_mode = FALSE // Say, Me, Whisper, Subtle Input Mode; Disabled by default; FALSE = BYOND, TRUE = TGUI + var/tgui_input_mode = FALSE // All the Input Boxes (Text,Number,List,Alert) + var/tgui_large_buttons = TRUE + var/tgui_swapped_buttons = FALSE //character preferences var/num_languages = 0 //CHOMPEdit diff --git a/code/modules/mob/typing_indicator.dm b/code/modules/mob/typing_indicator.dm index 7ab6de6fe5..197e472e92 100644 --- a/code/modules/mob/typing_indicator.dm +++ b/code/modules/mob/typing_indicator.dm @@ -40,11 +40,7 @@ set hidden = 1 set_typing_indicator(TRUE) - var/message - if(usr.client.prefs.tgui_input_mode) - message = tgui_input_text(usr, "Type your message:", "Say") - else - message = input(usr, "Type your message:", "Say") as text + var/message = tgui_input_text(usr, "Type your message:", "Say") set_typing_indicator(FALSE) if(message) @@ -55,11 +51,7 @@ set hidden = 1 set_typing_indicator(TRUE) - var/message - if(usr.client.prefs.tgui_input_mode) - message = tgui_input_message(usr, "Type your message:", "Emote") - else - message = input(usr, "Type your message:", "Emote") as message + var/message = tgui_input_text(usr, "Type your message:", "Emote", multiline = TRUE) set_typing_indicator(FALSE) if(message) @@ -70,11 +62,7 @@ set name = ".Whisper" set hidden = 1 - var/message - if(usr.client.prefs.tgui_input_mode) - message = tgui_input_text(usr, "Type your message:", "Whisper") - else - message = input(usr, "Type your message:", "Whisper") as text + var/message = tgui_input_text(usr, "Type your message:", "Whisper") if(message) whisper(message) @@ -83,11 +71,7 @@ set name = ".Subtle" set hidden = 1 - var/message - if(usr.client.prefs.tgui_input_mode) - message = tgui_input_message(usr, "Type your message:", "Subtle") - else - message = input(usr, "Type your message:", "Subtle") as message + var/message = tgui_input_text(usr, "Type your message:", "Subtle", multiline = TRUE) if(message) me_verb_subtle(message) diff --git a/code/modules/paperwork/adminpaper.dm b/code/modules/paperwork/adminpaper.dm index 1a6077c30a..9cf7f2ddf6 100644 --- a/code/modules/paperwork/adminpaper.dm +++ b/code/modules/paperwork/adminpaper.dm @@ -87,7 +87,7 @@ to_chat(usr, "There isn't enough space left on \the [src] to write anything.") return - var/raw_t = tgui_input_message(usr, "Enter what you want to write:", "Write") + var/raw_t = tgui_input_text(usr, "Enter what you want to write:", "Write", multiline = TRUE) if(!raw_t) return var/t = sanitize(raw_t, free_space, extra = 0) diff --git a/code/modules/tgui/modules/admin/player_notes.dm b/code/modules/tgui/modules/admin/player_notes.dm index 492cc5288b..f60e742325 100644 --- a/code/modules/tgui/modules/admin/player_notes.dm +++ b/code/modules/tgui/modules/admin/player_notes.dm @@ -118,7 +118,7 @@ if("add_player_info") var/key = params["ckey"] - var/add = tgui_input_message(usr, "Write your comment below.", "Add Player Info") + var/add = tgui_input_text(usr, "Write your comment below.", "Add Player Info", multiline = TRUE) if(!add) return notes_add(key,add,usr) diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm index c34ccf63d7..42acb1eac0 100644 --- a/code/modules/tgui/tgui.dm +++ b/code/modules/tgui/tgui.dm @@ -32,8 +32,12 @@ var/closing = FALSE /// The status/visibility of the UI. var/status = STATUS_INTERACTIVE + /// Timed refreshing state + var/refreshing = FALSE /// Topic state used to determine status/interactability. var/datum/tgui_state/state = null + /// Rate limit client refreshes to prevent DoS. + COOLDOWN_DECLARE(refresh_cooldown) /// The map z-level to display. var/map_z_level = 1 /// The Parent UI @@ -176,11 +180,17 @@ /datum/tgui/proc/send_full_update(custom_data, force) if(!user.client || !initialized || closing) return + if(!COOLDOWN_FINISHED(src, refresh_cooldown)) + refreshing = TRUE + addtimer(CALLBACK(src, .proc/send_full_update), TGUI_REFRESH_FULL_UPDATE_COOLDOWN, TIMER_UNIQUE) + return + refreshing = FALSE var/should_update_data = force || status >= STATUS_UPDATE window.send_message("update", get_payload( custom_data, with_data = should_update_data, with_static_data = TRUE)) + COOLDOWN_START(src, refresh_cooldown, TGUI_REFRESH_FULL_UPDATE_COOLDOWN) /** * public @@ -211,6 +221,7 @@ "title" = title, "status" = status, "interface" = interface, + "refreshing" = refreshing, "map" = (using_map && using_map.path) ? using_map.path : "Unknown", "mapZLevel" = map_z_level, "window" = list( @@ -312,6 +323,9 @@ return FALSE switch(type) if("ready") + // Send a full update when the user manually refreshes the UI + if(initialized) + send_full_update() initialized = TRUE if("pingReply") initialized = TRUE diff --git a/code/modules/tgui/tgui_input_text.dm b/code/modules/tgui/tgui_input_text.dm deleted file mode 100644 index e2247a32ce..0000000000 --- a/code/modules/tgui/tgui_input_text.dm +++ /dev/null @@ -1,299 +0,0 @@ -/** - * Creates a TGUI input text window and returns the user's response. - * - * This proc should be used to create alerts that the caller will wait for a response from. - * Arguments: - * * user - The user to show the input box to. - * * message - The content of the input box, shown in the body of the TGUI window. - * * title - The title of the input box, shown on the top of the TGUI window. - * * default - The default value pre-populated in the input box. - * * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout. - */ -/proc/tgui_input_text(mob/user, message, title, default, timeout = 0) - if (istext(user)) - stack_trace("tgui_input_text() received text for user instead of mob") - return - if (!user) - user = usr - if (!istype(user)) - if (istype(user, /client)) - var/client/client = user - user = client.mob - else - return - var/datum/tgui_input_dialog/input = new(user, message, title, default, timeout) - input.input_type = "text" - input.tgui_interact(user) - input.wait() - if (input) - . = input.choice - qdel(input) - -/** - * Creates a TGUI input message window and returns the user's response. - * - * This proc should be used to create alerts that the caller will wait for a response from. - * Arguments: - * * user - The user to show the input box to. - * * message - The content of the input box, shown in the body of the TGUI window. - * * title - The title of the input box, shown on the top of the TGUI window. - * * default - The default value pre-populated in the input box. - * * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout. - */ -/proc/tgui_input_message(mob/user, message, title, default, timeout = 0) - if (istext(user)) - stack_trace("tgui_input_message() received text for user instead of mob") - return - if (!user) - user = usr - if (!istype(user)) - if (istype(user, /client)) - var/client/client = user - user = client.mob - else - return - var/datum/tgui_input_dialog/input = new(user, message, title, default, timeout) - input.input_type = "message" - input.tgui_interact(user) - input.wait() - if (input) - . = input.choice - qdel(input) - -/** - * Creates a TGUI input num window and returns the user's response. - * - * This proc should be used to create alerts that the caller will wait for a response from. - * Arguments: - * * user - The user to show the input box to. - * * message - The content of the input box, shown in the body of the TGUI window. - * * title - The title of the input box, shown on the top of the TGUI window. - * * default - The default value pre-populated in the input box. - * * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout. - */ -/proc/tgui_input_num(mob/user, message, title, default, timeout = 0) - if (istext(user)) - stack_trace("tgui_input_num() received text for user instead of mob") - return - if (!user) - user = usr - if (!istype(user)) - if (istype(user, /client)) - var/client/client = user - user = client.mob - else - return - var/datum/tgui_input_dialog/input = new(user, message, title, default, timeout) - input.input_type = "num" - input.tgui_interact(user) - input.wait() - if (input) - . = input.choice - qdel(input) - -/** - * Creates an asynchronous TGUI input text window with an associated callback. - * - * This proc should be used to create inputs that invoke a callback with the user's chosen option. - * Arguments: - * * user - The user to show the input box to. - * * message - The content of the input box, shown in the body of the TGUI window. - * * title - The title of the input box, shown on the top of the TGUI window. - * * default - The default value pre-populated in the input box. - * * callback - The callback to be invoked when a choice is made. - * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout. - */ -/proc/tgui_input_text_async(mob/user, message, title, default, datum/callback/callback, timeout = 60 SECONDS) - if (istext(user)) - stack_trace("tgui_input_text_async() received text for user instead of mob") - return - if (!user) - user = usr - if (!istype(user)) - if (istype(user, /client)) - var/client/client = user - user = client.mob - else - return - var/datum/tgui_input_dialog/async/input = new(user, message, title, default, callback, timeout) - input.input_type = "text" - input.tgui_interact(user) - -/** - * Creates an asynchronous TGUI input message window with an associated callback. - * - * This proc should be used to create inputs that invoke a callback with the user's chosen option. - * Arguments: - * * user - The user to show the input box to. - * * message - The content of the input box, shown in the body of the TGUI window. - * * title - The title of the input box, shown on the top of the TGUI window. - * * default - The default value pre-populated in the input box. - * * callback - The callback to be invoked when a choice is made. - * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout. - */ -/proc/tgui_input_message_async(mob/user, message, title, default, datum/callback/callback, timeout = 60 SECONDS) - if (istext(user)) - stack_trace("tgui_input_message_async() received text for user instead of mob") - return - if (!user) - user = usr - if (!istype(user)) - if (istype(user, /client)) - var/client/client = user - user = client.mob - else - return - var/datum/tgui_input_dialog/async/input = new(user, message, title, default, callback, timeout) - input.input_type = "message" - input.tgui_interact(user) - -/** - * Creates an asynchronous TGUI input num window with an associated callback. - * - * This proc should be used to create inputs that invoke a callback with the user's chosen option. - * Arguments: - * * user - The user to show the input box to. - * * message - The content of the input box, shown in the body of the TGUI window. - * * title - The title of the input box, shown on the top of the TGUI window. - * * default - The default value pre-populated in the input box. - * * callback - The callback to be invoked when a choice is made. - * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout. - */ -/proc/tgui_input_num_async(mob/user, message, title, default, datum/callback/callback, timeout = 60 SECONDS) - if (istext(user)) - stack_trace("tgui_input_num_async() received text for user instead of mob") - return - if (!user) - user = usr - if (!istype(user)) - if (istype(user, /client)) - var/client/client = user - user = client.mob - else - return - var/datum/tgui_input_dialog/async/input = new(user, message, title, default, callback, timeout) - input.input_type = "num" - input.tgui_interact(user) - -/** - * # tgui_input_dialog - * - * Datum used for instantiating and using a TGUI-controlled input that prompts the user with - * a message and a box for accepting text/message/num input. - */ -/datum/tgui_input_dialog - /// The title of the TGUI window - var/title - /// The textual body of the TGUI window - var/message - /// The default value to initially populate the input box. - var/initial - /// The value that the user input into the input box, null if cancelled. - var/choice - /// The time at which the tgui_text_input was created, for displaying timeout progress. - var/start_time - /// The lifespan of the tgui_text_input, after which the window will close and delete itself. - var/timeout - /// Boolean field describing if the tgui_text_input was closed by the user. - var/closed - /// Indicates the data type we want to collect ("text", "message", "num") - var/input_type = "text" - -/datum/tgui_input_dialog/New(mob/user, message, title, default, timeout) - src.title = title - src.message = message - // TODO - Do we need to sanitize the initial value for illegal characters? - src.initial = default - if (timeout) - src.timeout = timeout - start_time = world.time - QDEL_IN(src, timeout) - -/datum/tgui_input_dialog/Destroy(force, ...) - SStgui.close_uis(src) - . = ..() - -/** - * Waits for a user's response to the tgui_text_input's prompt before returning. Returns early if - * the window was closed by the user. - */ -/datum/tgui_input_dialog/proc/wait() - while (!choice && !closed) - stoplag(1) - -/datum/tgui_input_dialog/tgui_interact(mob/user, datum/tgui/ui) - ui = SStgui.try_update_ui(user, src, ui) - if(!ui) - ui = new(user, src, "InputModal") - ui.open() - -/datum/tgui_input_dialog/tgui_close(mob/user) - . = ..() - closed = TRUE - -/datum/tgui_input_dialog/tgui_state(mob/user) - return GLOB.tgui_always_state - -/datum/tgui_input_dialog/tgui_static_data(mob/user) - . = list( - "title" = title, - "message" = message, - "initial" = initial, - "input_type" = input_type - ) - -/datum/tgui_input_dialog/tgui_data(mob/user) - . = list() - if(timeout) - .["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1) - -/datum/tgui_input_dialog/tgui_act(action, list/params) - . = ..() - if (.) - return - switch(action) - if("choose") - set_choice(params["choice"]) - if(isnull(src.choice)) - return - SStgui.close_uis(src) - return TRUE - if("cancel") - SStgui.close_uis(src) - closed = TRUE - return TRUE - -/datum/tgui_input_dialog/proc/set_choice(choice) - if(input_type == "num") - src.choice = text2num(choice) - return - src.choice = choice - -/** - * # async tgui_text_input - * - * An asynchronous version of tgui_text_input to be used with callbacks instead of waiting on user responses. - */ -/datum/tgui_input_dialog/async - /// The callback to be invoked by the tgui_text_input upon having a choice made. - var/datum/callback/callback - -/datum/tgui_input_dialog/async/New(mob/user, message, title, default, callback, timeout) - ..(user, title, message, default, timeout) - src.callback = callback - -/datum/tgui_input_dialog/async/Destroy(force, ...) - QDEL_NULL(callback) - . = ..() - -/datum/tgui_input_dialog/async/tgui_close(mob/user) - . = ..() - qdel(src) - -/datum/tgui_input_dialog/async/set_choice(choice) - . = ..() - if(!isnull(src.choice)) - callback?.InvokeAsync(src.choice) - -/datum/tgui_input_dialog/async/wait() - return diff --git a/code/modules/tgui/tgui_alert.dm b/code/modules/tgui_input/alert.dm similarity index 74% rename from code/modules/tgui/tgui_alert.dm rename to code/modules/tgui_input/alert.dm index 4ae15da886..a53089e4b0 100644 --- a/code/modules/tgui/tgui_alert.dm +++ b/code/modules/tgui_input/alert.dm @@ -8,8 +8,9 @@ * * title - The of the alert modal, shown on the top of the TGUI window. * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI. * * timeout - The timeout of the alert, after which the modal will close and qdel itself. Set to zero for no timeout. + * * autofocus - The bool that controls if this alert should grab window focus. */ -/proc/tgui_alert(mob/user, message = null, title = null, list/buttons = list("Ok"), timeout = 0) +/proc/tgui_alert(mob/user, message = "", title, list/buttons = list("Ok"), timeout = 0, autofocus = TRUE) if (istext(buttons)) stack_trace("tgui_alert() received text for buttons instead of list") return @@ -24,7 +25,19 @@ user = client.mob else return - var/datum/tgui_alert/alert = new(user, message, title, buttons, timeout) + // A gentle nudge - you should not be using TGUI alert for anything other than a simple message. + if(length(buttons) > 3) + log_tgui(user, "Error: TGUI Alert initiated with too many buttons. Use a list.", "TguiAlert") + return tgui_input_list(user, message, title, buttons, timeout, autofocus) + + // Client does NOT have tgui_input on: Returns regular input + if(!usr.client.prefs.tgui_input_mode) + if(length(buttons) == 2) + return alert(user, message, title, buttons[1], buttons[2]) + if(length(buttons) == 3) + return alert(user, message, title, buttons[1], buttons[2], buttons[3]) + + var/datum/tgui_alert/alert = new(user, message, title, buttons, timeout, autofocus) alert.tgui_interact(user) alert.wait() if (alert) @@ -32,37 +45,7 @@ qdel(alert) /** - * Creates an asynchronous TGUI alert window with an associated callback. - * - * This proc should be used to create alerts that invoke a callback with the user's chosen option. - * Arguments: - * * user - The user to show the alert to. - * * message - The content of the alert, shown in the body of the TGUI window. - * * title - The of the alert modal, shown on the top of the TGUI window. - * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI. - * * callback - The callback to be invoked when a choice is made. - * * timeout - The timeout of the alert, after which the modal will close and qdel itself. Disabled by default, can be set to seconds otherwise. - */ -/proc/tgui_alert_async(mob/user, message = null, title = null, list/buttons = list("Ok"), datum/callback/callback, timeout = 0) - if (istext(buttons)) - stack_trace("tgui_alert() received text for buttons instead of list") - return - if (istext(user)) - stack_trace("tgui_alert() received text for user instead of list") - return - if (!user) - user = usr - if (!istype(user)) - if (istype(user, /client)) - var/client/client = user - user = client.mob - else - return - var/datum/tgui_alert/async/alert = new(user, message, title, buttons, callback, timeout) - alert.tgui_interact(user) - -/** - * # tgui_modal + * # tgui_alert * * Datum used for instantiating and using a TGUI-controlled modal that prompts the user with * a message and has buttons for responses. @@ -80,13 +63,16 @@ var/start_time /// The lifespan of the tgui_modal, after which the window will close and delete itself. var/timeout + /// The bool that controls if this modal should grab window focus + var/autofocus /// Boolean field describing if the tgui_modal was closed by the user. var/closed -/datum/tgui_alert/New(mob/user, message, title, list/buttons, timeout) - src.title = title - src.message = message +/datum/tgui_alert/New(mob/user, message, title, list/buttons, timeout, autofocus) + src.autofocus = autofocus src.buttons = buttons.Copy() + src.message = message + src.title = title if (timeout) src.timeout = timeout start_time = world.time @@ -118,15 +104,21 @@ /datum/tgui_alert/tgui_state(mob/user) return GLOB.tgui_always_state -/datum/tgui_alert/tgui_data(mob/user) - . = list( - "title" = title, - "message" = message, - "buttons" = buttons - ) +/datum/tgui_alert/tgui_static_data(mob/user) + var/list/data = list() + data["autofocus"] = autofocus + data["buttons"] = buttons + data["message"] = message + data["large_buttons"] = usr.client.prefs.tgui_large_buttons + data["swapped_buttons"] = !usr.client.prefs.tgui_swapped_buttons + data["title"] = title + return data +/datum/tgui_alert/tgui_data(mob/user) + var/list/data = list() if(timeout) .["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS)) + return data /datum/tgui_alert/tgui_act(action, list/params) . = ..() @@ -139,10 +131,44 @@ set_choice(params["choice"]) SStgui.close_uis(src) return TRUE + if("cancel") + closed = TRUE + SStgui.close_uis(src) + return TRUE /datum/tgui_alert/proc/set_choice(choice) src.choice = choice +/** + * Creates an asynchronous TGUI alert window with an associated callback. + * + * This proc should be used to create alerts that invoke a callback with the user's chosen option. + * Arguments: + * * user - The user to show the alert to. + * * message - The content of the alert, shown in the body of the TGUI window. + * * title - The of the alert modal, shown on the top of the TGUI window. + * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI. + * * callback - The callback to be invoked when a choice is made. + * * timeout - The timeout of the alert, after which the modal will close and qdel itself. Disabled by default, can be set to seconds otherwise. + */ +/proc/tgui_alert_async(mob/user, message = "", title, list/buttons = list("Ok"), datum/callback/callback, timeout = 0, autofocus = TRUE) + if (istext(buttons)) + stack_trace("tgui_alert() received text for buttons instead of list") + return + if (istext(user)) + stack_trace("tgui_alert() received text for user instead of list") + return + if (!user) + user = usr + if (!istype(user)) + if (istype(user, /client)) + var/client/client = user + user = client.mob + else + return + var/datum/tgui_alert/async/alert = new(user, message, title, buttons, callback, timeout, autofocus) + alert.tgui_interact(user) + /** * # async tgui_modal * @@ -152,8 +178,8 @@ /// The callback to be invoked by the tgui_modal upon having a choice made. var/datum/callback/callback -/datum/tgui_alert/async/New(mob/user, message, title, list/buttons, callback, timeout) - ..(user, message, title, buttons, timeout) +/datum/tgui_alert/async/New(mob/user, message, title, list/buttons, callback, timeout, autofocus) + ..(user, message, title, buttons, timeout, autofocus) src.callback = callback /datum/tgui_alert/async/Destroy(force, ...) diff --git a/code/modules/tgui/tgui_input_list.dm b/code/modules/tgui_input/list.dm similarity index 71% rename from code/modules/tgui/tgui_input_list.dm rename to code/modules/tgui_input/list.dm index 8b1af90196..76ebe411e4 100644 --- a/code/modules/tgui/tgui_input_list.dm +++ b/code/modules/tgui_input/list.dm @@ -6,17 +6,17 @@ * * user - The user to show the input box to. * * message - The content of the input box, shown in the body of the TGUI window. * * title - The title of the input box, shown on the top of the TGUI window. - * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI. + * * items - The options that can be chosen by the user, each string is assigned a button on the UI. * * default - The option with this value will be selected on first paint of the TGUI window. * * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout. */ -/proc/tgui_input_list(mob/user, message, title, list/buttons, default, timeout = 0) +/proc/tgui_input_list(mob/user, message, title = "Select", list/items, default, timeout = 0) if (istext(user)) stack_trace("tgui_alert() received text for user instead of mob") return if (!user) user = usr - if(!length(buttons)) + if(!length(items)) return if (!istype(user)) if (istype(user, /client)) @@ -24,43 +24,16 @@ user = client.mob else return - var/datum/tgui_list_input/input = new(user, message, title, buttons, default, timeout) + /// Client does NOT have tgui_input on: Returns regular input + if(!usr.client.prefs.tgui_input_mode) + return input(user, message, title, default) as null|anything in items + var/datum/tgui_list_input/input = new(user, message, title, items, default, timeout) input.tgui_interact(user) input.wait() if (input) . = input.choice qdel(input) -/** - * Creates an asynchronous TGUI input list window with an associated callback. - * - * This proc should be used to create inputs that invoke a callback with the user's chosen option. - * Arguments: - * * user - The user to show the input box to. - * * message - The content of the input box, shown in the body of the TGUI window. - * * title - The title of the input box, shown on the top of the TGUI window. - * * buttons - The options that can be chosen by the user, each string is assigned a button on the UI. - * * default - The option with this value will be selected on first paint of the TGUI window. - * * callback - The callback to be invoked when a choice is made. - * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout. - */ -/proc/tgui_input_list_async(mob/user, message, title, list/buttons, default, datum/callback/callback, timeout = 60 SECONDS) - if (istext(user)) - stack_trace("tgui_alert() received text for user instead of mob") - return - if (!user) - user = usr - if(!length(buttons)) - return - if (!istype(user)) - if (istype(user, /client)) - var/client/client = user - user = client.mob - else - return - var/datum/tgui_list_input/async/input = new(user, message, title, buttons, default, callback, timeout) - input.tgui_interact(user) - /** * # tgui_list_input * @@ -72,14 +45,14 @@ var/title /// The textual body of the TGUI window var/message - /// The list of buttons (responses) provided on the TGUI window - var/list/buttons - /// Buttons (strings specifically) mapped to the actual value (e.g. a mob or a verb) - var/list/buttons_map - /// Value of the button that should be pre-selected on first paint. - var/initial + /// The list of items (responses) provided on the TGUI window + var/list/items + /// Items (strings specifically) mapped to the actual value (e.g. a mob or a verb) + var/list/items_map /// The button that the user has pressed, null if no selection has been made var/choice + /// The default item to be selected + var/default /// The time at which the tgui_list_input was created, for displaying timeout progress. var/start_time /// The lifespan of the tgui_list_input, after which the window will close and delete itself. @@ -87,29 +60,29 @@ /// Boolean field describing if the tgui_list_input was closed by the user. var/closed -/datum/tgui_list_input/New(mob/user, message, title, list/buttons, default, timeout) +/datum/tgui_list_input/New(mob/user, message, title, list/items, default, timeout) src.title = title src.message = message - src.buttons = list() - src.buttons_map = list() - src.initial = default - var/list/repeat_buttons = list() + src.items = list() + src.items_map = list() + src.default = default + var/list/repeat_items = list() // Gets rid of illegal characters var/static/regex/whitelistedWords = regex(@{"([^\u0020-\u8000]+)"}) - for(var/i in buttons) + for(var/i in items) if(isnull(i)) - stack_trace("Null in a tgui_input_list() buttons") + stack_trace("Null in a tgui_input_list() items") continue var/string_key = whitelistedWords.Replace("[i]", "") //avoids duplicated keys E.g: when areas have the same name - string_key = avoid_assoc_duplicate_keys(string_key, repeat_buttons) + string_key = avoid_assoc_duplicate_keys(string_key, repeat_items) - src.buttons += string_key - src.buttons_map[string_key] = i + src.items += string_key + src.items_map[string_key] = i if (timeout) @@ -119,7 +92,7 @@ /datum/tgui_list_input/Destroy(force, ...) SStgui.close_uis(src) - QDEL_NULL(buttons) + QDEL_NULL(items) . = ..() /** @@ -133,7 +106,7 @@ /datum/tgui_list_input/tgui_interact(mob/user, datum/tgui/ui) ui = SStgui.try_update_ui(user, src, ui) if(!ui) - ui = new(user, src, "ListInput") + ui = new(user, src, "ListInputModal") ui.open() /datum/tgui_list_input/tgui_close(mob/user) @@ -144,27 +117,31 @@ return GLOB.tgui_always_state /datum/tgui_list_input/tgui_static_data(mob/user) - . = list( - "title" = title, - "message" = message, - "buttons" = buttons, - "initial" = initial - ) + var/list/data = list() + data["init_value"] = default || items[1] + data["items"] = items + data["large_buttons"] = usr.client.prefs.tgui_large_buttons + data["message"] = message + data["swapped_buttons"] = !usr.client.prefs.tgui_swapped_buttons + data["title"] = title + return data /datum/tgui_list_input/tgui_data(mob/user) - . = list() + var/list/data = list() if(timeout) .["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1) + return data /datum/tgui_list_input/tgui_act(action, list/params) . = ..() if (.) return switch(action) - if("choose") - if (!(params["choice"] in buttons)) + if("submit") + if (!(params["entry"] in items)) return - set_choice(buttons_map[params["choice"]]) + set_choice(items_map[params["entry"]]) + closed = TRUE SStgui.close_uis(src) return TRUE if("cancel") @@ -175,6 +152,36 @@ /datum/tgui_list_input/proc/set_choice(choice) src.choice = choice +/** + * Creates an asynchronous TGUI input list window with an associated callback. + * + * This proc should be used to create inputs that invoke a callback with the user's chosen option. + * Arguments: + * * user - The user to show the input box to. + * * message - The content of the input box, shown in the body of the TGUI window. + * * title - The title of the input box, shown on the top of the TGUI window. + * * items - The options that can be chosen by the user, each string is assigned a button on the UI. + * * default - The option with this value will be selected on first paint of the TGUI window. + * * callback - The callback to be invoked when a choice is made. + * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout. + */ +/proc/tgui_input_list_async(mob/user, message, title, list/items, default, datum/callback/callback, timeout = 60 SECONDS) + if (istext(user)) + stack_trace("tgui_alert() received text for user instead of mob") + return + if (!user) + user = usr + if(!length(items)) + return + if (!istype(user)) + if (istype(user, /client)) + var/client/client = user + user = client.mob + else + return + var/datum/tgui_list_input/async/input = new(user, message, title, items, default, callback, timeout) + input.tgui_interact(user) + /** * # async tgui_list_input * @@ -184,8 +191,8 @@ /// The callback to be invoked by the tgui_list_input upon having a choice made. var/datum/callback/callback -/datum/tgui_list_input/async/New(mob/user, message, title, list/buttons, default, callback, timeout) - ..(user, title, message, buttons, default, timeout) +/datum/tgui_list_input/async/New(mob/user, message, title, list/items, default, callback, timeout) + ..(user, title, message, items, default, timeout) src.callback = callback /datum/tgui_list_input/async/Destroy(force, ...) diff --git a/code/modules/tgui_input/number.dm b/code/modules/tgui_input/number.dm new file mode 100644 index 0000000000..3f7ffccd96 --- /dev/null +++ b/code/modules/tgui_input/number.dm @@ -0,0 +1,209 @@ +/** + * Creates a TGUI window with a number input. Returns the user's response as num | null. + * + * This proc should be used to create windows for number entry that the caller will wait for a response from. + * If tgui fancy chat is turned off: Will return a normal input. If a max or min value is specified, will + * validate the input inside the UI and ui_act. + * + * Arguments: + * * user - The user to show the number input to. + * * message - The content of the number input, shown in the body of the TGUI window. + * * title - The title of the number input modal, shown on the top of the TGUI window. + * * default - The default (or current) value, shown as a placeholder. Users can press refresh with this. + * * max_value - Specifies a maximum value. If none is set, any number can be entered. Pressing "max" defaults to 1000. + * * min_value - Specifies a minimum value. Often 0. + * * timeout - The timeout of the number input, after which the modal will close and qdel itself. Set to zero for no timeout. + * * round_value - whether the inputted number is rounded down into an integer. + */ +/proc/tgui_input_number(mob/user, message, title = "Number Input", default = 0, max_value = 10000, min_value = 0, timeout = 0, round_value = TRUE) + if (!user) + user = usr + if (!istype(user)) + if (istype(user, /client)) + var/client/client = user + user = client.mob + else + return + // Client does NOT have tgui_input on: Returns regular input + if(!usr.client.prefs.tgui_input_mode) + var/input_number = input(user, message, title, default) as null|num + return clamp(round_value ? round(input_number) : input_number, min_value, max_value) + var/datum/tgui_input_number/number_input = new(user, message, title, default, max_value, min_value, timeout, round_value) + number_input.tgui_interact(user) + number_input.wait() + if (number_input) + . = number_input.entry + qdel(number_input) + +/** + * # tgui_input_number + * + * Datum used for instantiating and using a TGUI-controlled number input that prompts the user with + * a message and has an input for number entry. + */ +/datum/tgui_input_number + /// Boolean field describing if the tgui_input_number was closed by the user. + var/closed + /// The default (or current) value, shown as a default. Users can press reset with this. + var/default + /// The entry that the user has return_typed in. + var/entry + /// The maximum value that can be entered. + var/max_value + /// The prompt's body, if any, of the TGUI window. + var/message + /// The minimum value that can be entered. + var/min_value + /// Whether the submitted number is rounded down into an integer. + var/round_value + /// The time at which the number input was created, for displaying timeout progress. + var/start_time + /// The lifespan of the number input, after which the window will close and delete itself. + var/timeout + /// The title of the TGUI window + var/title + +/datum/tgui_input_number/New(mob/user, message, title, default, max_value, min_value, timeout, round_value) + src.default = default + src.max_value = max_value + src.message = message + src.min_value = min_value + src.title = title + src.round_value = round_value + if (timeout) + src.timeout = timeout + start_time = world.time + QDEL_IN(src, timeout) + /// Checks for empty numbers - bank accounts, etc. + if(max_value == 0) + src.min_value = 0 + if(default) + src.default = 0 + /// Sanity check + if(default < min_value) + src.default = min_value + if(default > max_value) + CRASH("Default value is greater than max value.") + +/datum/tgui_input_number/Destroy(force, ...) + SStgui.close_uis(src) + return ..() + +/** + * Waits for a user's response to the tgui_input_number's prompt before returning. Returns early if + * the window was closed by the user. + */ +/datum/tgui_input_number/proc/wait() + while (!entry && !closed && !QDELETED(src)) + stoplag(1) + +/datum/tgui_input_number/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "NumberInputModal") + ui.open() + +/datum/tgui_input_number/tgui_close(mob/user) + . = ..() + closed = TRUE + +/datum/tgui_input_number/tgui_state(mob/user) + return GLOB.tgui_always_state + +/datum/tgui_input_number/tgui_static_data(mob/user) + var/list/data = list() + data["init_value"] = default // Default is a reserved keyword + data["large_buttons"] = usr.client.prefs.tgui_large_buttons + data["max_value"] = max_value + data["message"] = message + data["min_value"] = min_value + data["swapped_buttons"] = !usr.client.prefs.tgui_swapped_buttons + data["title"] = title + return data + +/datum/tgui_input_number/tgui_data(mob/user) + var/list/data = list() + if(timeout) + data["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS)) + return data + +/datum/tgui_input_number/tgui_act(action, list/params) + . = ..() + if (.) + return + switch(action) + if("submit") + if(!isnum(params["entry"])) + CRASH("A non number was input into tgui input number by [usr]") + var/choice = round_value ? round(params["entry"]) : params["entry"] + if(choice > max_value) + CRASH("A number greater than the max value was input into tgui input number by [usr]") + if(choice < min_value) + CRASH("A number less than the min value was input into tgui input number by [usr]") + set_entry(choice) + closed = TRUE + SStgui.close_uis(src) + return TRUE + if("cancel") + closed = TRUE + SStgui.close_uis(src) + return TRUE + +/datum/tgui_input_number/proc/set_entry(entry) + src.entry = entry + +/** + * Creates an asynchronous TGUI input num window with an associated callback. + * + * This proc should be used to create inputs that invoke a callback with the user's chosen option. + * Arguments: + * * user - The user to show the input box to. + * * message - The content of the input box, shown in the body of the TGUI window. + * * title - The title of the input box, shown on the top of the TGUI window. + * * default - The default value pre-populated in the input box. + * * callback - The callback to be invoked when a choice is made. + * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout. + */ +/proc/tgui_input_number_async(mob/user, message, title, default, datum/callback/callback, timeout = 60 SECONDS) + if (istext(user)) + stack_trace("tgui_input_num_async() received text for user instead of mob") + return + if (!user) + user = usr + if (!istype(user)) + if (istype(user, /client)) + var/client/client = user + user = client.mob + else + return + var/datum/tgui_input_number/async/input = new(user, message, title, default, callback, timeout) + input.tgui_interact(user) + +/** + * # async tgui_text_input + * + * An asynchronous version of tgui_text_input to be used with callbacks instead of waiting on user responses. + */ +/datum/tgui_input_number/async + /// The callback to be invoked by the tgui_text_input upon having a choice made. + var/datum/callback/callback + +/datum/tgui_input_number/async/New(mob/user, message, title, default, callback, timeout) + ..(user, title, message, default, timeout) + src.callback = callback + +/datum/tgui_input_number/async/Destroy(force, ...) + QDEL_NULL(callback) + . = ..() + +/datum/tgui_input_number/async/tgui_close(mob/user) + . = ..() + qdel(src) + +/datum/tgui_input_number/async/set_entry(entry) + . = ..() + if(!isnull(src.entry)) + callback?.InvokeAsync(src.entry) + +/datum/tgui_input_number/async/wait() + return \ No newline at end of file diff --git a/code/modules/tgui_input/text.dm b/code/modules/tgui_input/text.dm new file mode 100644 index 0000000000..474f8e7c3e --- /dev/null +++ b/code/modules/tgui_input/text.dm @@ -0,0 +1,209 @@ +/** + * Creates a TGUI window with a text input. Returns the user's response. + * + * This proc should be used to create windows for text entry that the caller will wait for a response from. + * If tgui fancy chat is turned off: Will return a normal input. If max_length is specified, will return + * stripped_multiline_input. + * + * Arguments: + * * user - The user to show the text input to. + * * message - The content of the text input, shown in the body of the TGUI window. + * * title - The title of the text input modal, shown on the top of the TGUI window. + * * default - The default (or current) value, shown as a placeholder. + * * max_length - Specifies a max length for input. MAX_MESSAGE_LEN is default (1024) + * * multiline - Bool that determines if the input box is much larger. Good for large messages, laws, etc. + * * encode - Toggling this determines if input is filtered via html_encode. Setting this to FALSE gives raw input. + * * timeout - The timeout of the textbox, after which the modal will close and qdel itself. Set to zero for no timeout. + */ +/proc/tgui_input_text(mob/user, message = "", title = "Text Input", default, max_length = MAX_MESSAGE_LEN, multiline = FALSE, encode = TRUE, timeout = 0) + if (istext(user)) + stack_trace("tgui_input_text() received text for user instead of mob") + return + if (!user) + user = usr + if (!istype(user)) + if (istype(user, /client)) + var/client/client = user + user = client.mob + else + return + // Client does NOT have tgui_input on: Returns regular input + if(!usr.client.prefs.tgui_input_mode) + if(encode) + if(multiline) + return stripped_multiline_input(user, message, title, default, max_length) + else + return stripped_input(user, message, title, default, max_length) + else + if(multiline) + return input(user, message, title, default) as message|null + else + return input(user, message, title, default) as text|null + var/datum/tgui_input_text/text_input = new(user, message, title, default, max_length, multiline, encode, timeout) + text_input.tgui_interact(user) + text_input.wait() + if (text_input) + . = text_input.entry + qdel(text_input) + +/** + * tgui_input_text + * + * Datum used for instantiating and using a TGUI-controlled text input that prompts the user with + * a message and has an input for text entry. + */ +/datum/tgui_input_text + /// Boolean field describing if the tgui_input_text was closed by the user. + var/closed + /// The default (or current) value, shown as a default. + var/default + /// Whether the input should be stripped using html_encode + var/encode + /// The entry that the user has return_typed in. + var/entry + /// The maximum length for text entry + var/max_length + /// The prompt's body, if any, of the TGUI window. + var/message + /// Multiline input for larger input boxes. + var/multiline + /// The time at which the text input was created, for displaying timeout progress. + var/start_time + /// The lifespan of the text input, after which the window will close and delete itself. + var/timeout + /// The title of the TGUI window + var/title + +/datum/tgui_input_text/New(mob/user, message, title, default, max_length, multiline, encode, timeout) + src.default = default + src.encode = encode + src.max_length = max_length + src.message = message + src.multiline = multiline + src.title = title + if (timeout) + src.timeout = timeout + start_time = world.time + QDEL_IN(src, timeout) + +/datum/tgui_input_text/Destroy(force, ...) + SStgui.close_uis(src) + . = ..() + +/** + * Waits for a user's response to the tgui_text_input's prompt before returning. Returns early if + * the window was closed by the user. + */ +/datum/tgui_input_text/proc/wait() + while (!entry && !closed) + stoplag(1) + +/datum/tgui_input_text/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "TextInputModal") + ui.open() + +/datum/tgui_input_text/tgui_close(mob/user) + . = ..() + closed = TRUE + +/datum/tgui_input_text/tgui_state(mob/user) + return GLOB.tgui_always_state + +/datum/tgui_input_text/tgui_static_data(mob/user) + var/list/data = list() + data["large_buttons"] = usr.client.prefs.tgui_large_buttons + data["max_length"] = max_length + data["message"] = message + data["multiline"] = multiline + data["placeholder"] = default // Default is a reserved keyword + data["swapped_buttons"] = !usr.client.prefs.tgui_swapped_buttons + data["title"] = title + return data + +/datum/tgui_input_text/tgui_data(mob/user) + var/list/data = list() + if(timeout) + .["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1) + return data + +/datum/tgui_input_text/tgui_act(action, list/params) + . = ..() + if (.) + return + switch(action) + if("submit") + if(length(params["entry"]) > max_length) + return + if(encode && (length(html_encode(params["entry"])) > max_length)) + to_chat(usr, span_notice("Your message was clipped due to special character usage.")) + set_entry(params["entry"]) + closed = TRUE + SStgui.close_uis(src) + return TRUE + if("cancel") + SStgui.close_uis(src) + closed = TRUE + return TRUE + +/datum/tgui_input_text/proc/set_entry(entry) + if(!isnull(entry)) + var/converted_entry = encode ? html_encode(entry) : entry + src.entry = trim(converted_entry, max_length) + +/** + * Creates an asynchronous TGUI input text window with an associated callback. + * + * This proc should be used to create inputs that invoke a callback with the user's chosen option. + * Arguments: + * * user - The user to show the input box to. + * * message - The content of the input box, shown in the body of the TGUI window. + * * title - The title of the input box, shown on the top of the TGUI window. + * * default - The default value pre-populated in the input box. + * * callback - The callback to be invoked when a choice is made. + * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout. + */ +/proc/tgui_input_text_async(mob/user, message, title, default, datum/callback/callback, max_length, multiline, encode, timeout = 60 SECONDS) + if (istext(user)) + stack_trace("tgui_input_text_async() received text for user instead of mob") + return + if (!user) + user = usr + if (!istype(user)) + if (istype(user, /client)) + var/client/client = user + user = client.mob + else + return + var/datum/tgui_input_text/async/input = new(user, message, title, default, callback, max_length, multiline, encode, timeout) + input.tgui_interact(user) + +/** + * # async tgui_text_input + * + * An asynchronous version of tgui_text_input to be used with callbacks instead of waiting on user responses. + */ +/datum/tgui_input_text/async + /// The callback to be invoked by the tgui_text_input upon having a choice made. + var/datum/callback/callback + +/datum/tgui_input_text/async/New(mob/user, message, title, default, callback, max_length, multiline, encode, timeout) + ..(user, title, message, default, max_length, multiline, encode, timeout) + src.callback = callback + +/datum/tgui_input_text/async/Destroy(force, ...) + QDEL_NULL(callback) + . = ..() + +/datum/tgui_input_text/async/tgui_close(mob/user) + . = ..() + qdel(src) + +/datum/tgui_input_text/async/set_entry(entry) + . = ..() + if(!isnull(src.entry)) + callback?.InvokeAsync(src.entry) + +/datum/tgui_input_text/async/wait() + return diff --git a/tgui/packages/tgui/backend.ts b/tgui/packages/tgui/backend.ts index 3e57bca72b..2b3499bfac 100644 --- a/tgui/packages/tgui/backend.ts +++ b/tgui/packages/tgui/backend.ts @@ -227,6 +227,7 @@ type BackendState = { title: string, status: number, interface: string, + refreshing: boolean, window: { key: string, size: [number, number], diff --git a/tgui/packages/tgui/components/RestrictedInput.js b/tgui/packages/tgui/components/RestrictedInput.js new file mode 100644 index 0000000000..0a6e2cb440 --- /dev/null +++ b/tgui/packages/tgui/components/RestrictedInput.js @@ -0,0 +1,154 @@ +import { classes } from 'common/react'; +import { clamp } from 'common/math'; +import { Component, createRef } from 'inferno'; +import { Box } from './Box'; +import { KEY_ESCAPE, KEY_ENTER } from 'common/keycodes'; + +const DEFAULT_MIN = 0; +const DEFAULT_MAX = 10000; + +/** + * Takes a string input and parses integers from it. + * If none: Minimum is set. + * Else: Clamps it to the given range. + */ +const getClampedNumber = (value, minValue, maxValue) => { + const minimum = minValue || DEFAULT_MIN; + const maximum = maxValue || maxValue === 0 ? maxValue : DEFAULT_MAX; + if (!value || !value.length) { + return String(minimum); + } + let parsedValue = parseInt(value.replace(/\D/g, ''), 10); + if (isNaN(parsedValue)) { + return String(minimum); + } else { + return String(clamp(parsedValue, minimum, maximum)); + } +}; + +export class RestrictedInput extends Component { + constructor() { + super(); + this.inputRef = createRef(); + this.state = { + editing: false, + }; + this.handleBlur = (e) => { + const { editing } = this.state; + if (editing) { + this.setEditing(false); + } + }; + this.handleChange = (e) => { + const { maxValue, minValue, onChange } = this.props; + e.target.value = getClampedNumber(e.target.value, minValue, maxValue); + if (onChange) { + onChange(e, +e.target.value); + } + }; + this.handleFocus = (e) => { + const { editing } = this.state; + if (!editing) { + this.setEditing(true); + } + }; + this.handleInput = (e) => { + const { editing } = this.state; + const { onInput } = this.props; + if (!editing) { + this.setEditing(true); + } + if (onInput) { + onInput(e, +e.target.value); + } + }; + this.handleKeyDown = (e) => { + const { maxValue, minValue, onChange, onEnter } = this.props; + if (e.keyCode === KEY_ENTER) { + const safeNum = getClampedNumber(e.target.value, minValue, maxValue); + this.setEditing(false); + if (onChange) { + onChange(e, +safeNum); + } + if (onEnter) { + onEnter(e, +safeNum); + } + e.target.blur(); + return; + } + if (e.keyCode === KEY_ESCAPE) { + if (this.props.onEscape) { + this.props.onEscape(e); + return; + } + this.setEditing(false); + e.target.value = this.props.value; + e.target.blur(); + return; + } + }; + } + + componentDidMount() { + const { maxValue, minValue } = this.props; + const nextValue = this.props.value?.toString(); + const input = this.inputRef.current; + if (input) { + input.value = getClampedNumber(nextValue, minValue, maxValue); + } + if (this.props.autoFocus || this.props.autoSelect) { + setTimeout(() => { + input.focus(); + + if (this.props.autoSelect) { + input.select(); + } + }, 1); + } + } + + componentDidUpdate(prevProps, _) { + const { maxValue, minValue } = this.props; + const { editing } = this.state; + const prevValue = prevProps.value?.toString(); + const nextValue = this.props.value?.toString(); + const input = this.inputRef.current; + if (input && !editing) { + if (nextValue !== prevValue && nextValue !== input.value) { + input.value = getClampedNumber(nextValue, minValue, maxValue); + } + } + } + + setEditing(editing) { + this.setState({ editing }); + } + + render() { + const { props } = this; + const { onChange, onEnter, onInput, value, ...boxProps } = props; + const { className, fluid, monospace, ...rest } = boxProps; + return ( + +
.
+ +
+ ); + } +} diff --git a/tgui/packages/tgui/components/index.js b/tgui/packages/tgui/components/index.js index d27639a6c1..139305c3ca 100644 --- a/tgui/packages/tgui/components/index.js +++ b/tgui/packages/tgui/components/index.js @@ -33,6 +33,7 @@ export { NoticeBox } from './NoticeBox'; export { NumberInput } from './NumberInput'; export { ProgressBar } from './ProgressBar'; export { Popper } from './Popper'; +export { RestrictedInput } from './RestrictedInput'; export { RoundGauge } from './RoundGauge'; export { Section } from './Section'; export { Slider } from './Slider'; diff --git a/tgui/packages/tgui/interfaces/AlertModal.js b/tgui/packages/tgui/interfaces/AlertModal.js deleted file mode 100644 index 2815bd93d5..0000000000 --- a/tgui/packages/tgui/interfaces/AlertModal.js +++ /dev/null @@ -1,137 +0,0 @@ -/** - * @file - * @copyright 2020 bobbahbrown (https://github.com/bobbahbrown) - * @license MIT - */ - -import { Loader } from "./common/Loader"; -import { useBackend } from '../backend'; -import { Component, createRef } from 'inferno'; -import { Box, Flex, Section } from '../components'; -import { Window } from '../layouts'; -import { - KEY_ENTER, - KEY_LEFT, - KEY_RIGHT, - KEY_SPACE, - KEY_TAB, -} from 'common/keycodes'; - -export class AlertModal extends Component { - constructor() { - super(); - - this.buttonRefs = [createRef()]; - this.state = { current: 0 }; - } - - componentDidMount() { - const { data } = useBackend(this.context); - const { buttons } = data; - const { current } = this.state; - const button = this.buttonRefs[current].current; - - // Fill ref array with refs for other buttons - for (let i = 1; i < buttons.length; i++) { - this.buttonRefs.push(createRef()); - } - - setTimeout(() => button.focus(), 1); - } - - setCurrent(current, isArrowKey) { - const { data } = useBackend(this.context); - const { buttons } = data; - - // Mimic alert() behavior for tabs and arrow keys - if (current >= buttons.length) { - current = isArrowKey ? current - 1 : 0; - } else if (current < 0) { - current = isArrowKey ? 0 : buttons.length - 1; - } - - const button = this.buttonRefs[current].current; - - // Prevents an error from occurring on close - if (button) { - setTimeout(() => button.focus(), 1); - } - this.setState({ current }); - } - - render() { - const { act, data } = useBackend(this.context); - const { title, message, buttons, timeout } = data; - const { current } = this.state; - const focusCurrentButton = () => this.setCurrent(current, false); - - const windowHeight = Math.max(150, message.length); - - return ( - 0}> - {timeout && } - -
- - - - - - {message} - - - - - - - {buttons.map((button, buttonIndex) => ( - -
act("choose", { choice: button })} - onKeyDown={e => { - const keyCode = window.event ? e.which : e.keyCode; - - /** - * Simulate a click when pressing space or enter, - * allow keyboard navigation, override tab behavior - */ - if (keyCode === KEY_SPACE || keyCode === KEY_ENTER) { - act("choose", { choice: button }); - } else if ( - keyCode === KEY_LEFT - || (e.shiftKey && keyCode === KEY_TAB) - ) { - this.setCurrent(current - 1, keyCode === KEY_LEFT); - } else if ( - keyCode === KEY_RIGHT || keyCode === KEY_TAB - ) { - this.setCurrent(current + 1, keyCode === KEY_RIGHT); - } - }}> - {button} -
-
- ))} -
-
-
-
-
-
- ); - } - -} diff --git a/tgui/packages/tgui/interfaces/AlertModal.tsx b/tgui/packages/tgui/interfaces/AlertModal.tsx new file mode 100644 index 0000000000..965faf6838 --- /dev/null +++ b/tgui/packages/tgui/interfaces/AlertModal.tsx @@ -0,0 +1,151 @@ +import { Loader } from './common/Loader'; +import { useBackend, useLocalState } from '../backend'; +import { KEY_ENTER, KEY_ESCAPE, KEY_LEFT, KEY_RIGHT, KEY_SPACE, KEY_TAB } from '../../common/keycodes'; +import { Autofocus, Box, Button, Flex, Section, Stack } from '../components'; +import { Window } from '../layouts'; + +type AlertModalData = { + autofocus: boolean; + buttons: string[]; + large_buttons: boolean; + message: string; + swapped_buttons: boolean; + timeout: number; + title: string; +}; + +const KEY_DECREMENT = -1; +const KEY_INCREMENT = 1; + +export const AlertModal = (_, context) => { + const { act, data } = useBackend(context); + const { + autofocus, + buttons = [], + large_buttons, + message = '', + timeout, + title, + } = data; + const [selected, setSelected] = useLocalState(context, 'selected', 0); + // Dynamically sets window dimensions + const windowHeight + = 115 + + (message.length > 30 ? Math.ceil(message.length / 4) : 0) + + (message.length && large_buttons ? 5 : 0); + const windowWidth = 325 + (buttons.length > 2 ? 55 : 0); + const onKey = (direction: number) => { + if (selected === 0 && direction === KEY_DECREMENT) { + setSelected(buttons.length - 1); + } else if (selected === buttons.length - 1 && direction === KEY_INCREMENT) { + setSelected(0); + } else { + setSelected(selected + direction); + } + }; + + return ( + + {!!timeout && } + { + const keyCode = window.event ? e.which : e.keyCode; + /** + * Simulate a click when pressing space or enter, + * allow keyboard navigation, override tab behavior + */ + if (keyCode === KEY_SPACE || keyCode === KEY_ENTER) { + act('choose', { choice: buttons[selected] }); + } else if (keyCode === KEY_ESCAPE) { + act('cancel'); + } else if (keyCode === KEY_LEFT) { + e.preventDefault(); + onKey(KEY_DECREMENT); + } else if (keyCode === KEY_TAB || keyCode === KEY_RIGHT) { + e.preventDefault(); + onKey(KEY_INCREMENT); + } + }}> +
+ + + + {message} + + + + {!!autofocus && } + + + +
+
+
+ ); +}; + +/** + * Displays a list of buttons ordered by user prefs. + * Technically this handles more than 2 buttons, but you + * should just be using a list input in that case. + */ +const ButtonDisplay = (props, context) => { + const { data } = useBackend(context); + const { buttons = [], large_buttons, swapped_buttons } = data; + const { selected } = props; + + return ( + + {buttons?.map((button, index) => + !!large_buttons && buttons.length < 3 ? ( + + + + ) : ( + + + + ) + )} + + ); +}; + +/** + * Displays a button with variable sizing. + */ +const AlertButton = (props, context) => { + const { act, data } = useBackend(context); + const { large_buttons } = data; + const { button, selected } = props; + const buttonWidth = button.length > 7 ? button.length : 7; + + return ( + + ); +}; diff --git a/tgui/packages/tgui/interfaces/InputModal.js b/tgui/packages/tgui/interfaces/InputModal.js deleted file mode 100644 index 6366e775a0..0000000000 --- a/tgui/packages/tgui/interfaces/InputModal.js +++ /dev/null @@ -1,117 +0,0 @@ -/** - * @file - * @copyright 2021 Leshana - * @license MIT - */ - -import { clamp01 } from 'common/math'; -import { useBackend, useLocalState } from '../backend'; -import { Box, Button, Section, Input, Stack, TextArea } from '../components'; -import { KEY_ESCAPE } from 'common/keycodes'; -import { Window } from '../layouts'; -import { createLogger } from '../logging'; - -const logger = createLogger('inputmodal'); - -export const InputModal = (props, context) => { - const { act, data } = useBackend(context); - const { title, message, initial, input_type, timeout } = data; - - // Current Input Value - const [curValue, setCurValue] = useLocalState(context, 'curValue', initial); - - const handleKeyDown = e => { - if (e.keyCode === KEY_ESCAPE) { - e.preventDefault(); - act("cancel"); - return; - } - }; - - let initialHeight, initialWidth; - let modalBody; - switch (input_type) { - case 'text': - case 'num': - initialWidth = 325; - initialHeight = Math.max(150, message.length); - modalBody = ( - { - setCurValue(val); - }} - onEnter={(_e, val) => { - act('choose', { choice: val }); - }} - /> - ); - break; - case 'message': - initialWidth = 450; - initialHeight = 350; - modalBody = ( -