diff --git a/beestation.dme b/beestation.dme index 992a7d69213ca..cb1420c2a7d17 100644 --- a/beestation.dme +++ b/beestation.dme @@ -3534,7 +3534,11 @@ #include "code\modules\tgui\states.dm" #include "code\modules\tgui\status_composers.dm" #include "code\modules\tgui\tgui.dm" +#include "code\modules\tgui\tgui_alert.dm" #include "code\modules\tgui\tgui_input_emoji.dm" +#include "code\modules\tgui\tgui_input_list.dm" +#include "code\modules\tgui\tgui_input_number.dm" +#include "code\modules\tgui\tgui_input_text.dm" #include "code\modules\tgui\tgui_select_picture.dm" #include "code\modules\tgui\tgui_window.dm" #include "code\modules\tgui\states\admin.dm" diff --git a/code/datums/components/cult_ritual_item.dm b/code/datums/components/cult_ritual_item.dm index 3c4e20c717680..5a795a11f77d2 100644 --- a/code/datums/components/cult_ritual_item.dm +++ b/code/datums/components/cult_ritual_item.dm @@ -348,7 +348,7 @@ to_chat(cultist, "\"I am already here. There is no need to try to summon me now.\"") return FALSE var/confirm_final = alert(cultist, "This is the FINAL step to summon Nar'Sie; it is a long, painful ritual and the crew will be alerted to your presence.", "Are you prepared for the final battle?", "My life for Nar'Sie!", "No") - if(confirm_final == "No") + if(confirm_final == "No" || !confirm_final) to_chat(cultist, "You decide to prepare further before scribing the rune.") return if(!check_if_in_ritual_site(cultist, cult_team)) diff --git a/code/game/machinery/computer/launchpad_control.dm b/code/game/machinery/computer/launchpad_control.dm index 6805bc354f44c..b82081e41e7c4 100644 --- a/code/game/machinery/computer/launchpad_control.dm +++ b/code/game/machinery/computer/launchpad_control.dm @@ -134,7 +134,7 @@ current_pad.display_name = new_name . = TRUE if("remove") - if(usr && alert(usr, "Are you sure?", "Unlink Launchpad", "I'm Sure", "Abort") != "Abort") + if(usr && alert(usr, "Are you sure?", "Unlink Launchpad", "I'm Sure", "Abort") == "I'm Sure") launchpads -= current_pad selected_id = null . = TRUE diff --git a/code/game/machinery/computer/law.dm b/code/game/machinery/computer/law.dm index 7039d59e679e8..8d6bbbad8331e 100644 --- a/code/game/machinery/computer/law.dm +++ b/code/game/machinery/computer/law.dm @@ -40,7 +40,7 @@ current = null return M.install(current.laws, user) - if(alert("Do you wish to scramble the upload code?", "Scramble Code", "Yes", "No") == "No") + if(alert("Do you wish to scramble the upload code?", "Scramble Code", "Yes", "No") != "Yes") return message_admins("[ADMIN_LOOKUPFLW(usr)] has scrambled the upload code [GLOB.upload_code]!") GLOB.upload_code = random_code(4) diff --git a/code/game/machinery/cryopod.dm b/code/game/machinery/cryopod.dm index 86c6c8894e0c2..a6c53074c8fb3 100644 --- a/code/game/machinery/cryopod.dm +++ b/code/game/machinery/cryopod.dm @@ -362,7 +362,7 @@ GLOBAL_LIST_EMPTY(cryopod_computers) to_chat(user, "You can't put [target] into [src]. They're conscious.") return else if(target.client) - if(alert(target,"Would you like to enter cryosleep?",,"Yes","No") == "No") + if(alert(target,"Would you like to enter cryosleep?",,"Yes","No") != "Yes") return var/generic_plsnoleave_message = " Please adminhelp before leaving the round, even if there are no administrators online!" diff --git a/code/game/machinery/launch_pad.dm b/code/game/machinery/launch_pad.dm index e7e67ac9dc79f..c3fe413c9c803 100644 --- a/code/game/machinery/launch_pad.dm +++ b/code/game/machinery/launch_pad.dm @@ -391,7 +391,7 @@ . = TRUE if("remove") . = TRUE - if(usr && alert(usr, "Are you sure?", "Unlink Launchpad", "I'm Sure", "Abort") != "Abort") + if(usr && alert(usr, "Are you sure?", "Unlink Launchpad", "I'm Sure", "Abort") == "I'm Sure") pad = null if("launch") sending = TRUE diff --git a/code/game/objects/items/implants/implant_explosive.dm b/code/game/objects/items/implants/implant_explosive.dm index 3df869644276f..be032cd839c2d 100644 --- a/code/game/objects/items/implants/implant_explosive.dm +++ b/code/game/objects/items/implants/implant_explosive.dm @@ -29,13 +29,13 @@ /obj/item/implant/explosive/activate(cause) . = ..() if(!cause || !imp_in || active) - return 0 + return FALSE if(cause == "action_button" && !popup) popup = TRUE var/response = alert(imp_in, "Are you sure you want to activate your [name]? This will cause you to explode!", "[name] Confirmation", "Yes", "No") popup = FALSE - if(response == "No") - return 0 + if(response != "Yes") + return FALSE heavy = round(heavy) medium = round(medium) weak = round(weak) @@ -49,7 +49,7 @@ if(imp_in) imp_in.gib(1) qdel(src) - return + return TRUE timed_explosion() /obj/item/implant/explosive/implant(mob/living/target, mob/user, silent = FALSE, force = FALSE) diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index 5f1743ad63faa..2824c3ccfcc57 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -320,11 +320,10 @@ if (!usr.client.holder) return var/confirm = alert("End the round and restart the game world?", "End Round", "Yes", "Cancel") - if(confirm == "Cancel") + if(confirm != "Yes") return - if(confirm == "Yes") - SSticker.force_ending = 1 - SSblackbox.record_feedback("tally", "admin_verb", 1, "End Round") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! + SSticker.force_ending = 1 + SSblackbox.record_feedback("tally", "admin_verb", 1, "End Round") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! /datum/admins/proc/dynamic_mode_options(mob/user) var/dat = {" diff --git a/code/modules/admin/permissionedit.dm b/code/modules/admin/permissionedit.dm index f7d252fdd94bb..c3c4625b25036 100644 --- a/code/modules/admin/permissionedit.dm +++ b/code/modules/admin/permissionedit.dm @@ -168,7 +168,7 @@ use_db = FALSE else use_db = alert("Permanent changes are saved to the database for future rounds, temporary changes will affect only the current round", "Permanent or Temporary?", "Permanent", "Temporary", "Cancel") - if(use_db == "Cancel") + if(use_db == "Cancel" || !use_db) return if(use_db == "Permanent") use_db = TRUE diff --git a/code/modules/admin/secrets.dm b/code/modules/admin/secrets.dm index 823cae8dd95ed..c9e4c76c456bb 100644 --- a/code/modules/admin/secrets.dm +++ b/code/modules/admin/secrets.dm @@ -134,7 +134,7 @@ GLOBAL_DATUM_INIT(admin_secrets, /datum/admin_secrets, new) if(!check_rights(R_ADMIN)) return var/delete_mobs = alert("Clear all mobs?","Confirm","Yes","No","Cancel") - if(delete_mobs == "Cancel") + if(delete_mobs == "Cancel" || !delete_mobs) return log_admin("[key_name(usr)] reset the thunderdome to default with delete_mobs==[delete_mobs].", 1) @@ -454,7 +454,7 @@ GLOBAL_DATUM_INIT(admin_secrets, /datum/admin_secrets, new) if(animetype =="Yes") droptype = alert("Make the uniforms undroppable?",,"Yes","No","Cancel") - if(animetype == "Cancel" || droptype == "Cancel") + if(animetype == "Cancel" || droptype == "Cancel" || !animetype || (!droptype && animetype == "Yes")) return SSblackbox.record_feedback("nested tally", "admin_secrets_fun_used", 1, list("Chinese Cartoons")) message_admins("[key_name_admin(usr)] made everything kawaii.") @@ -695,7 +695,7 @@ GLOBAL_DATUM_INIT(admin_secrets, /datum/admin_secrets, new) if("flipmovement") if(!check_rights(R_FUN)) return - if(alert("Flip all movement controls?","Confirm","Yes","Cancel") == "Cancel") + if(alert("Flip all movement controls?","Confirm","Yes","Cancel") != "Yes") return var/list/movement_keys = SSinput.movement_keys for(var/i in 1 to movement_keys.len) @@ -707,7 +707,7 @@ GLOBAL_DATUM_INIT(admin_secrets, /datum/admin_secrets, new) if("randommovement") if(!check_rights(R_FUN)) return - if(alert("Randomize all movement controls?","Confirm","Yes","Cancel") == "Cancel") + if(alert("Randomize all movement controls?","Confirm","Yes","Cancel") != "Yes") return var/list/movement_keys = SSinput.movement_keys for(var/i in 1 to movement_keys.len) @@ -719,7 +719,7 @@ GLOBAL_DATUM_INIT(admin_secrets, /datum/admin_secrets, new) if("custommovement") if(!check_rights(R_FUN)) return - if(alert("Are you sure you want to change every movement key?","Confirm","Yes","Cancel") == "Cancel") + if(alert("Are you sure you want to change every movement key?","Confirm","Yes","Cancel") != "Yes") return var/list/movement_keys = SSinput.movement_keys var/list/new_movement = list() @@ -740,7 +740,7 @@ GLOBAL_DATUM_INIT(admin_secrets, /datum/admin_secrets, new) if("resetmovement") if(!check_rights(R_FUN)) return - if(alert("Are you sure you want to reset movement keys to default?","Confirm","Yes","Cancel") == "Cancel") + if(alert("Are you sure you want to reset movement keys to default?","Confirm","Yes","Cancel") != "Yes") return SSinput.setup_default_movement_keys() message_admins("[key_name_admin(usr)] has reset all movement keys.") @@ -820,7 +820,7 @@ GLOBAL_DATUM_INIT(admin_secrets, /datum/admin_secrets, new) if(E) E.processing = FALSE if(E.announceWhen>0) - if(alert(usr, "Would you like to alert the crew?", "Alert", "Yes", "No") == "No") + if(alert(usr, "Would you like to alert the crew?", "Alert", "Yes", "No") != "Yes") E.announceChance = 0 E.processing = TRUE if (usr) diff --git a/code/modules/admin/sql_ban_system.dm b/code/modules/admin/sql_ban_system.dm index 489c49b7ccfed..fb7da927dc0dc 100644 --- a/code/modules/admin/sql_ban_system.dm +++ b/code/modules/admin/sql_ban_system.dm @@ -819,7 +819,7 @@ to_chat(usr, "Failed to establish database connection.") return var/target = ban_target_string(player_key, player_ip, player_cid) - if(alert(usr, "Please confirm unban of [target] from [role].", "Unban confirmation", "Yes", "No") == "No") + if(alert(usr, "Please confirm unban of [target] from [role].", "Unban confirmation", "Yes", "No") != "Yes") return var/kn = key_name(usr) var/kna = key_name_admin(usr) diff --git a/code/modules/admin/stickyban.dm b/code/modules/admin/stickyban.dm index c3cabea87f0bb..936b5af674c60 100644 --- a/code/modules/admin/stickyban.dm +++ b/code/modules/admin/stickyban.dm @@ -61,7 +61,7 @@ if (!ban) to_chat(usr, "Error: No sticky ban for [ckey] found!") return - if (alert("Are you sure you want to remove the sticky ban on [ckey]?","Are you sure","Yes","No") == "No") + if (alert("Are you sure you want to remove the sticky ban on [ckey]?","Are you sure","Yes","No") != "Yes") return if (!get_stickyban_from_ckey(ckey)) to_chat(usr, "Error: The ban disappeared.") @@ -98,7 +98,7 @@ to_chat(usr, "Error: [alt] is not linked to [ckey]'s sticky ban!") return - if (alert("Are you sure you want to disassociate [alt] from [ckey]'s sticky ban? \nNote: Nothing stops byond from re-linking them, Use \[E] to exempt them","Are you sure","Yes","No") == "No") + if (alert("Are you sure you want to disassociate [alt] from [ckey]'s sticky ban? \nNote: Nothing stops byond from re-linking them, Use \[E] to exempt them","Are you sure","Yes","No") != "Yes") return //we have to do this again incase something changes @@ -180,7 +180,7 @@ to_chat(usr, "Error: [alt] is not linked to [ckey]'s sticky ban!") return - if (alert("Are you sure you want to exempt [alt] from [ckey]'s sticky ban?","Are you sure","Yes","No") == "No") + if (alert("Are you sure you want to exempt [alt] from [ckey]'s sticky ban?","Are you sure","Yes","No") != "Yes") return //we have to do this again incase something changes @@ -230,7 +230,7 @@ to_chat(usr, "Error: [alt] is not exempt from [ckey]'s sticky ban!") return - if (alert("Are you sure you want to unexempt [alt] from [ckey]'s sticky ban?","Are you sure","Yes","No") == "No") + if (alert("Are you sure you want to unexempt [alt] from [ckey]'s sticky ban?","Are you sure","Yes","No") != "Yes") return //we have to do this again incase something changes @@ -272,7 +272,7 @@ var/ckey = data["ckey"] - if (alert("Are you sure you want to put [ckey]'s stickyban on timeout until next round (or removed)?","Are you sure","Yes","No") == "No") + if (alert("Are you sure you want to put [ckey]'s stickyban on timeout until next round (or removed)?","Are you sure","Yes","No") != "Yes") return var/ban = get_stickyban_from_ckey(ckey) if (!ban) @@ -298,7 +298,7 @@ return var/ckey = data["ckey"] - if (alert("Are you sure you want to lift the timeout on [ckey]'s stickyban?","Are you sure","Yes","No") == "No") + if (alert("Are you sure you want to lift the timeout on [ckey]'s stickyban?","Are you sure","Yes","No") != "Yes") return var/ban = get_stickyban_from_ckey(ckey) @@ -323,7 +323,7 @@ if (!data["ckey"]) return var/ckey = data["ckey"] - if (alert("Are you sure you want to revert the sticky ban on [ckey] to its state at round start (or last edit)?","Are you sure","Yes","No") == "No") + if (alert("Are you sure you want to revert the sticky ban on [ckey] to its state at round start (or last edit)?","Are you sure","Yes","No") != "Yes") return var/ban = get_stickyban_from_ckey(ckey) if (!ban) diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 6a634fcadf9bd..eddb7c1e82ba2 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -1702,10 +1702,9 @@ if(!check_rights(R_ADMIN)) return var/confirm = alert("Are you sure you want to reboot the server?", "Confirm Reboot", "Yes", "No") - if(confirm == "No") + if(confirm != "Yes") return - if(confirm == "Yes") - restart() + restart() else if(href_list["check_teams"]) if(!check_rights(R_ADMIN)) diff --git a/code/modules/admin/verbs/_help.dm b/code/modules/admin/verbs/_help.dm index 8e6a3f3de711c..89a7ddb110157 100644 --- a/code/modules/admin/verbs/_help.dm +++ b/code/modules/admin/verbs/_help.dm @@ -129,7 +129,7 @@ if("claim") if(ticket.claimee) var/confirm = alert("This ticket is already claimed, override claim?", null,"Yes", "No") - if(confirm == "No") + if(confirm != "Yes") return claim_ticket = CLAIM_OVERRIDE if("reject") diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm index 8d968302b9615..39ca82623804f 100644 --- a/code/modules/admin/verbs/debug.dm +++ b/code/modules/admin/verbs/debug.dm @@ -506,7 +506,7 @@ But you can call procs that are of type /mob/living/carbon/human/proc/ for that else H = M if(H.l_store || H.r_store || H.s_store) //saves a lot of time for admins and coders alike - if(alert("Drop Items in Pockets? No will delete them.", "Robust quick dress shop", "Yes", "No") == "No") + if(alert("Drop Items in Pockets? No will delete them.", "Robust quick dress shop", "Yes", "No") != "Yes") delete_pocket = TRUE SSblackbox.record_feedback("tally", "admin_verb", 1, "Select Equipment") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! @@ -776,7 +776,7 @@ But you can call procs that are of type /mob/living/carbon/human/proc/ for that if (response == "Jump") usr.forceMove(get_turf(exists[template])) return - else if (response == "Cancel") + else if (response != "Place Another") return var/len = GLOB.ruin_landmarks.len @@ -800,7 +800,7 @@ But you can call procs that are of type /mob/living/carbon/human/proc/ for that if(ruin_size < 10 || ruin_size >= 200) return var/response = alert(src, "This will place the ruin at your current location.", "Spawn Ruin", "Spawn Ruin", "Cancel") - if (response == "Cancel") + if (response != "Spawn Ruin") return var/border_size = (world.maxx - ruin_size) / 2 generate_space_ruin(mob.x, mob.y, mob.z, border_size, border_size) diff --git a/code/modules/admin/verbs/healall.dm b/code/modules/admin/verbs/healall.dm index ae4fac4e0fe26..9033e62386e97 100644 --- a/code/modules/admin/verbs/healall.dm +++ b/code/modules/admin/verbs/healall.dm @@ -8,7 +8,7 @@ if(!check_rights(R_FUN)) to_chat(src, "You need the fun permission to use this command.") return - if(alert(src, "Confirm Heal All?","Are you sure?","Yes","No") == "No") + if(alert(src, "Confirm Heal All?","Are you sure?","Yes","No") != "Yes") return message_admins("[key_name_admin(usr)] healed all living mobs") log_admin("[key_name_admin(usr)] healed all living mobs") diff --git a/code/modules/admin/verbs/mapping.dm b/code/modules/admin/verbs/mapping.dm index 0d9b014cb5b82..63a75cfc1a859 100644 --- a/code/modules/admin/verbs/mapping.dm +++ b/code/modules/admin/verbs/mapping.dm @@ -51,7 +51,8 @@ GLOBAL_LIST_INIT(admin_verbs_debug_mapping, list( /client/proc/show_line_profiling, /client/proc/create_mapping_job_icons, /client/proc/debug_z_levels, - /client/proc/place_ruin + /client/proc/place_ruin, + /client/proc/test_tgui_inputs, )) GLOBAL_PROTECT(admin_verbs_debug_mapping) @@ -375,3 +376,25 @@ GLOBAL_VAR_INIT(say_disabled, FALSE) messages += "" to_chat(src, messages.Join("")) + +/client/proc/test_tgui_inputs() + set name = "Test TGUI Inputs" + set category = "Debug" + var/response = tgui_alert(usr, "Message Here", "Title Here", list("Button 1", "Button 2", "Button 3")) + to_chat(usr, response) + response = tgui_alert(usr, "Message Here", "Title Here", list("Yes", "No")) + to_chat(usr, response) + var/list/L = list() + for (var/obj/machinery/camera/cam in GLOB.cameranet.cameras) + L["[cam.c_tag]"] = cam + response = tgui_input_list(usr, "Message Here", "Title Here", L) + to_chat(usr, response) + response = tgui_input_number(usr, "Message Here", "Title Here", 10, 500, 2) + to_chat(usr, response) + response = tgui_input_text(usr, "Message Here", "Title Here", "Default Text", 32, FALSE) + to_chat(usr, response) + response = tgui_input_text(usr, "Message Here", "Title Here", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore\ + et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit\ + in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id\ + est laborum.", 1024, TRUE) + to_chat(usr, response) diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm index a7626fbc3fed6..392b34326c565 100644 --- a/code/modules/admin/verbs/randomverbs.dm +++ b/code/modules/admin/verbs/randomverbs.dm @@ -639,7 +639,7 @@ Traitors and the like can also be revived with the previous role mostly intact. if ((devastation != -1) || (heavy != -1) || (light != -1) || (flash != -1) || (flames != -1)) if ((devastation > 20) || (heavy > 20) || (light > 20) || (flames > 20)) - if (alert(src, "Are you sure you want to do this? It will laaag.", "Confirmation", "Yes", "No") == "No") + if (alert(src, "Are you sure you want to do this? It will laaag.", "Confirmation", "Yes", "No") != "Yes") return explosion(O, devastation, heavy, light, flash, null, null,flames) @@ -683,7 +683,7 @@ Traitors and the like can also be revived with the previous role mostly intact. return var/confirm = alert(src, "Drop a brain?", "Confirm", "Yes", "No","Cancel") - if(confirm == "Cancel") + if(confirm == "Cancel" || !confirm) return //Due to the delay here its easy for something to have happened to the mob if(!M) @@ -800,7 +800,7 @@ Traitors and the like can also be revived with the previous role mostly intact. var/notifyplayers = alert(src, "Do you want to notify the players?", "Options", "Yes", "No", "Cancel") - if(notifyplayers == "Cancel") + if(notifyplayers == "Cancel" || !notifyplayers) return log_admin("Admin [key_name(src)] has forced the players to have random appearances.") diff --git a/code/modules/admin/view_variables/mass_edit_variables.dm b/code/modules/admin/view_variables/mass_edit_variables.dm index e29580b3b27dc..91232299f9214 100644 --- a/code/modules/admin/view_variables/mass_edit_variables.dm +++ b/code/modules/admin/view_variables/mass_edit_variables.dm @@ -143,7 +143,7 @@ if (VV_NEW_TYPE) var/many = alert(src, "Create only one [value["type"]] and assign each or a new one for each thing", "How Many", "One", "Many", "Cancel") - if (many == "Cancel") + if (many == "Cancel" || !many) return if (many == "Many") many = TRUE diff --git a/code/modules/admin/view_variables/modify_variables.dm b/code/modules/admin/view_variables/modify_variables.dm index dbc4af543f105..a74e588019b92 100644 --- a/code/modules/admin/view_variables/modify_variables.dm +++ b/code/modules/admin/view_variables/modify_variables.dm @@ -179,7 +179,7 @@ GLOBAL_PROTECT(VVpixelmovement) return var/assoc = 0 var/prompt = alert(src, "Do you want to edit the key or its assigned value?", "Associated List", "Key", "Assigned Value", "Cancel") - if (prompt == "Cancel") + if (prompt == "Cancel" || !prompt) return if (prompt == "Assigned Value") assoc = 1 diff --git a/code/modules/antagonists/brainwashing/brainwashing.dm b/code/modules/antagonists/brainwashing/brainwashing.dm index f0ce4205bfd10..57d082ec819ad 100644 --- a/code/modules/antagonists/brainwashing/brainwashing.dm +++ b/code/modules/antagonists/brainwashing/brainwashing.dm @@ -88,7 +88,7 @@ log_objective(C, objective, admin) while(alert(admin,"Add another objective?","More Brainwashing","Yes","No") == "Yes") - if(alert(admin,"Confirm Brainwashing?","Are you sure?","Yes","No") == "No") + if(alert(admin,"Confirm Brainwashing?","Are you sure?","Yes","No") != "Yes") return if(!LAZYLEN(objectives)) diff --git a/code/modules/antagonists/changeling/powers/headcrab.dm b/code/modules/antagonists/changeling/powers/headcrab.dm index 4bfb77c9906eb..85cf9dd85961a 100644 --- a/code/modules/antagonists/changeling/powers/headcrab.dm +++ b/code/modules/antagonists/changeling/powers/headcrab.dm @@ -11,7 +11,7 @@ /datum/action/changeling/headcrab/sting_action(mob/user) set waitfor = FALSE - if(alert("Are we sure we wish to kill ourself and create a headslug?",,"Yes", "No") == "No") + if(alert("Are we sure we wish to kill ourself and create a headslug?",,"Yes", "No") != "Yes") return if(isliving(user)) var/mob/living/L = user diff --git a/code/modules/antagonists/clock_cult/mobs/cogscarab.dm b/code/modules/antagonists/clock_cult/mobs/cogscarab.dm index b4115d7a98fa3..8d233a2d20db5 100644 --- a/code/modules/antagonists/clock_cult/mobs/cogscarab.dm +++ b/code/modules/antagonists/clock_cult/mobs/cogscarab.dm @@ -74,7 +74,7 @@ GLOBAL_LIST_INIT(cogscarabs, list()) to_chat(user, "Can't become a cogscarab before the game has started.") return var/be_drone = alert("Become a cogscarab? (Warning, You can no longer be cloned!)",,"Yes","No") - if(be_drone == "No" || QDELETED(src) || !isobserver(user)) + if(be_drone != "Yes" || QDELETED(src) || !isobserver(user)) return var/mob/living/simple_animal/drone/D = new mob_type(get_turf(loc)) if(!D.default_hatmask && seasonal_hats && possible_seasonal_hats.len) diff --git a/code/modules/antagonists/clock_cult/structure/eminence_beacon.dm b/code/modules/antagonists/clock_cult/structure/eminence_beacon.dm index 6b9ae6d5d1b6e..45950cc4c9743 100644 --- a/code/modules/antagonists/clock_cult/structure/eminence_beacon.dm +++ b/code/modules/antagonists/clock_cult/structure/eminence_beacon.dm @@ -21,7 +21,7 @@ to_chat(user, "The Eminence has already been released.") return var/option = alert(user,"Who shall control the Eminence?",,"Yourself","A ghost", "Cancel") - if(option == "Cancel") + if(option != "A ghost") return else if(option == "Yourself") hierophant_message("[user] has elected themselves to become the Eminence. Interact with [src] to object.", span="") diff --git a/code/modules/antagonists/nukeop/equipment/nuclear_challenge.dm b/code/modules/antagonists/nukeop/equipment/nuclear_challenge.dm index 1738534592f93..526ea0f510175 100644 --- a/code/modules/antagonists/nukeop/equipment/nuclear_challenge.dm +++ b/code/modules/antagonists/nukeop/equipment/nuclear_challenge.dm @@ -27,7 +27,7 @@ if(!check_allowed(user)) return - if(are_you_sure == "No") + if(are_you_sure != "Yes") to_chat(user, "On second thought, the element of surprise isn't so bad after all.") return diff --git a/code/modules/antagonists/traitor/equipment/Malf_Modules.dm b/code/modules/antagonists/traitor/equipment/Malf_Modules.dm index 415c6e270cc2b..6e04609ec07ca 100644 --- a/code/modules/antagonists/traitor/equipment/Malf_Modules.dm +++ b/code/modules/antagonists/traitor/equipment/Malf_Modules.dm @@ -666,7 +666,7 @@ GLOBAL_LIST_INIT(blacklisted_malf_machines, typecacheof(list( if(!owner_AI.can_place_transformer(src)) return active = TRUE - if(alert(owner, "Are you sure you want to place the machine here?", "Are you sure?", "Yes", "No") == "No") + if(alert(owner, "Are you sure you want to place the machine here?", "Are you sure?", "Yes", "No") != "Yes") active = FALSE return if(!owner_AI.can_place_transformer(src)) diff --git a/code/modules/awaymissions/corpse.dm b/code/modules/awaymissions/corpse.dm index 9216004a441ef..9e120c9403c56 100644 --- a/code/modules/awaymissions/corpse.dm +++ b/code/modules/awaymissions/corpse.dm @@ -51,7 +51,7 @@ to_chat(user, "You have died recently, you must wait [(user.client.next_ghost_role_tick - world.time)/10] seconds until you can use a ghost spawner.") return var/ghost_role = alert("Become [mob_name]? (Warning, You can no longer be cloned!)",,"Yes","No") - if(ghost_role == "No" || !loc) + if(ghost_role != "Yes" || !loc) return log_game("[key_name(user)] became [mob_name]") create(ckey = user.ckey) @@ -598,7 +598,7 @@ //ATTACK HAND IGNORING PARENT RETURN VALUE /obj/effect/mob_spawn/human/alive/space_bar_patron/attack_hand(mob/user) var/despawn = alert("Return to cryosleep? (Warning, Your mob will be deleted!)",,"Yes","No") - if(despawn == "No" || !loc || !Adjacent(user)) + if(despawn != "Yes" || !loc || !Adjacent(user)) return user.visible_message("[user.name] climbs back into cryosleep...") qdel(user) diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index 1782c012af447..db56eab834537 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -1045,23 +1045,22 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( addtimer(CALLBACK(src, .proc/restore_account_identifier), 20) //Don't DoS DB queries, asshole var/confirm = alert("Do NOT share the verification ID in the following popup. Understand?", "Important Warning", "Yes", "Cancel") - if(confirm == "Cancel") + if(confirm != "Yes") return - if(confirm == "Yes") - var/uuid = fetch_uuid() - if(!uuid) - alert("Failed to fetch your verification ID. Try again later. If problems persist, tell an admin.", "Account Verification", "Okay") - log_sql("Failed to fetch UUID for [key_name(src)]") - else - var/dat - dat += "

Account Identifier

" - dat += "
" - dat += "

Do NOT share this id:

" - dat += "
" - dat += "[uuid]" - - src << browse(dat, "window=accountidentifier;size=600x320") - onclose(src, "accountidentifier") + var/uuid = fetch_uuid() + if(!uuid) + alert("Failed to fetch your verification ID. Try again later. If problems persist, tell an admin.", "Account Verification", "Okay") + log_sql("Failed to fetch UUID for [key_name(src)]") + else + var/dat + dat += "

Account Identifier

" + dat += "
" + dat += "

Do NOT share this id:

" + dat += "
" + dat += "[uuid]" + + src << browse(dat, "window=accountidentifier;size=600x320") + onclose(src, "accountidentifier") /client/proc/restore_account_identifier() add_verb(/client/proc/show_account_identifier) diff --git a/code/modules/events/pirates.dm b/code/modules/events/pirates.dm index 71fd946ae5100..b00f8a3dea977 100644 --- a/code/modules/events/pirates.dm +++ b/code/modules/events/pirates.dm @@ -128,7 +128,7 @@ /obj/machinery/shuttle_scrambler/interact(mob/user) if(!active) - if(alert(user, "Turning the scrambler on will make the shuttle trackable by GPS. Are you sure you want to do it?", "Scrambler", "Yes", "Cancel") == "Cancel") + if(alert(user, "Turning the scrambler on will make the shuttle trackable by GPS. Are you sure you want to do it?", "Scrambler", "Yes", "Cancel") != "Yes") return if(active || !user.canUseTopic(src, BE_CLOSE)) return diff --git a/code/modules/hydroponics/grown/replicapod.dm b/code/modules/hydroponics/grown/replicapod.dm index dac6c141ede9b..69f38af8fb401 100644 --- a/code/modules/hydroponics/grown/replicapod.dm +++ b/code/modules/hydroponics/grown/replicapod.dm @@ -109,7 +109,7 @@ if(!make_podman) // Prevent accidental harvesting. Make sure the user REALLY wants to do this if there's a chance of this coming from a living creature. if(mind || ckey) - if(alert("The pod is currently devoid of soul. There is a possibility that a soul could claim this creature, or you could harvest it for seeds.", "Harvest Seeds?", "Harvest Seeds", "Cancel") == "Cancel") + if(alert("The pod is currently devoid of soul. There is a possibility that a soul could claim this creature, or you could harvest it for seeds.", "Harvest Seeds?", "Harvest Seeds", "Cancel") != "Harvest Seeds") return result // If this plant has already been harvested, return early. diff --git a/code/modules/mob/dead/observer/observer.dm b/code/modules/mob/dead/observer/observer.dm index 29ddac10a7598..999cd61a9be59 100644 --- a/code/modules/mob/dead/observer/observer.dm +++ b/code/modules/mob/dead/observer/observer.dm @@ -644,7 +644,7 @@ This is the proc mobs get to turn into a ghost. Forked from ghostize due to comp return 0 if(can_reenter_corpse && mind && mind.current) - if(alert(src, "Your soul is still tied to your former life as [mind.current.name], if you go forward there is no going back to that life. Are you sure you wish to continue?", "Move On", "Yes", "No") == "No") + if(alert(src, "Your soul is still tied to your former life as [mind.current.name], if you go forward there is no going back to that life. Are you sure you wish to continue?", "Move On", "Yes", "No") != "Yes") return 0 if(target.key) to_chat(src, "Someone has taken this body while you were choosing!") diff --git a/code/modules/mob/living/brain/posibrain.dm b/code/modules/mob/living/brain/posibrain.dm index 8109f1c9ff279..6a292d9634c36 100644 --- a/code/modules/mob/living/brain/posibrain.dm +++ b/code/modules/mob/living/brain/posibrain.dm @@ -100,7 +100,7 @@ GLOBAL_VAR(posibrain_notify_cooldown) to_chat(user, "[src] fizzles slightly. Sadly it doesn't take those who suicided!") return var/posi_ask = alert("Become a [name]? (Warning, You can no longer be cloned, and all past lives will be forgotten!)","Are you positive?","Yes","No") - if(posi_ask == "No" || QDELETED(src)) + if(posi_ask != "Yes" || QDELETED(src)) return if(brainmob.suiciding) //clear suicide status if the old occupant suicided. brainmob.set_suicide(FALSE) diff --git a/code/modules/mob/living/living_sentience.dm b/code/modules/mob/living/living_sentience.dm index 293c8e920ae7a..2e349cbcd38c9 100644 --- a/code/modules/mob/living/living_sentience.dm +++ b/code/modules/mob/living/living_sentience.dm @@ -29,7 +29,7 @@ if(key || !playable || stat) return 0 var/question = alert("Do you want to become [name]?", "[name]", "Yes", "No") - if(question == "No" || !src || QDELETED(src)) + if(question != "Yes" || !src || QDELETED(src)) return TRUE if(key) to_chat(user, "Someone else already took [name].") diff --git a/code/modules/mob/living/simple_animal/friendly/drone/drones_as_items.dm b/code/modules/mob/living/simple_animal/friendly/drone/drones_as_items.dm index e09b6fc4f21dd..b5dd3f01af466 100644 --- a/code/modules/mob/living/simple_animal/friendly/drone/drones_as_items.dm +++ b/code/modules/mob/living/simple_animal/friendly/drone/drones_as_items.dm @@ -57,7 +57,7 @@ to_chat(user, "Can't become a drone before the game has started.") return var/be_drone = alert("Become a drone? (Warning, You can no longer be cloned!)",,"Yes","No") - if(be_drone == "No" || QDELETED(src) || !isobserver(user)) + if(be_drone != "Yes" || QDELETED(src) || !isobserver(user)) return var/mob/living/simple_animal/drone/D = new mob_type(get_turf(loc)) if(!D.default_hatmask && seasonal_hats && possible_seasonal_hats.len) diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm index 66f3ce4c77a49..f5bbe1cf3477f 100644 --- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm +++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm @@ -42,7 +42,7 @@ var/obj/structure/elite_tumor/T = target if(T.mychild == src && T.activity == TUMOR_PASSIVE) var/elite_remove = alert("Re-enter the tumor?", "Despawn yourself?", "Yes", "No") - if(elite_remove == "No" || !src || QDELETED(src)) + if(elite_remove != "Yes" || !src || QDELETED(src)) return T.mychild = null T.activity = TUMOR_INACTIVE diff --git a/code/modules/reagents/chemistry/machinery/chem_dispenser.dm b/code/modules/reagents/chemistry/machinery/chem_dispenser.dm index 5a512ec3385d7..7749795b7ff5d 100644 --- a/code/modules/reagents/chemistry/machinery/chem_dispenser.dm +++ b/code/modules/reagents/chemistry/machinery/chem_dispenser.dm @@ -309,7 +309,7 @@ var/name = stripped_input(usr,"Name","What do you want to name this recipe?", "Recipe", MAX_NAME_LEN) if(!usr.canUseTopic(src, !issilicon(usr))) return - if(saved_recipes[name] && alert("\"[name]\" already exists, do you want to overwrite it?",, "Yes", "No") == "No") + if(saved_recipes[name] && alert("\"[name]\" already exists, do you want to overwrite it?",, "Yes", "No") != "Yes") return if(name && recording_recipe) for(var/reagent in recording_recipe) diff --git a/code/modules/research/destructive_analyzer.dm b/code/modules/research/destructive_analyzer.dm index 42700bfa9e92a..7acaeda553913 100644 --- a/code/modules/research/destructive_analyzer.dm +++ b/code/modules/research/destructive_analyzer.dm @@ -120,7 +120,7 @@ Note: Must be placed within 3 tiles of the R&D Console if(length(worths) && !length(differences)) return FALSE var/choice = input("Are you sure you want to destroy [loaded_item] to [!length(worths) ? "reveal [TN.display_name]" : "boost [TN.display_name] by [json_encode(differences)] point\s"]?") in list("Proceed", "Cancel") - if(choice == "Cancel") + if(choice == "Cancel" || !choice) return FALSE if(QDELETED(loaded_item) || QDELETED(linked_console) || !user.Adjacent(linked_console) || QDELETED(src)) return FALSE @@ -138,7 +138,7 @@ Note: Must be placed within 3 tiles of the R&D Console else if(loaded_item.materials.len) user_mode_string = " for material reclamation" var/choice = input("Are you sure you want to destroy [loaded_item][user_mode_string]?") in list("Proceed", "Cancel") - if(choice == "Cancel") + if(choice != "Proceed") return FALSE if(QDELETED(loaded_item) || QDELETED(linked_console) || !user.Adjacent(linked_console) || QDELETED(src)) return FALSE diff --git a/code/modules/tgui/tgui_alert.dm b/code/modules/tgui/tgui_alert.dm new file mode 100644 index 0000000000000..13006c4e5b6ca --- /dev/null +++ b/code/modules/tgui/tgui_alert.dm @@ -0,0 +1,181 @@ +/** + * Creates a TGUI alert 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 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. + * * 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 = "", title, list/buttons = list("Ok"), timeout = 0, autofocus = 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(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) + 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_modal/alert = new(user, message, title, buttons, timeout, autofocus) + alert.ui_interact(user) + alert.wait() + if (alert) + . = alert.choice + 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. + * * autofocus - The bool that controls if this alert should grab window focus. + */ +/proc/tgui_alert_async(mob/user, message = "", title, list/buttons = list("Ok"), datum/callback/callback, timeout = 0, autofocus = 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(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) + 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_modal/async/alert = new(user, message, title, buttons, callback, timeout, autofocus) + alert.ui_interact(user) + +/** + * # tgui_modal + * + * Datum used for instantiating and using a TGUI-controlled modal that prompts the user with + * a message and has buttons for responses. + */ +/datum/tgui_modal + /// The title of the TGUI window + var/title + /// The textual body of the TGUI window + var/message + /// The list of buttons (responses) provided on the TGUI window + var/list/buttons + /// The button that the user has pressed, null if no selection has been made + var/choice + /// The time at which the tgui_modal was created, for displaying timeout progress. + 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_modal/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 + QDEL_IN(src, timeout) + +/datum/tgui_modal/Destroy(force, ...) + SStgui.close_uis(src) + QDEL_NULL(buttons) + . = ..() + +/** + * Waits for a user's response to the tgui_modal's prompt before returning. Returns early if + * the window was closed by the user. + */ +/datum/tgui_modal/proc/wait() + while (!choice && !closed && !QDELETED(src)) + stoplag(1) + +/datum/tgui_modal/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "AlertModal") + ui.open() + +/datum/tgui_modal/ui_close(mob/user) + . = ..() + closed = TRUE + +/datum/tgui_modal/ui_state(mob/user) + return GLOB.always_state + +/datum/tgui_modal/ui_data(mob/user) + . = list() + .["autofocus"] = autofocus + .["buttons"] = buttons + .["message"] = message + .["preferences"] = list() + .["preferences"]["large_buttons"] = TRUE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large) + .["preferences"]["swapped_buttons"] = TRUE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped) + .["title"] = title + if(timeout) + .["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS)) + +/datum/tgui_modal/ui_act(action, list/params) + . = ..() + if (.) + return + switch(action) + if("choose") + if (!(params["choice"] in buttons)) + CRASH("[usr] entered a non-existent button choice: [params["choice"]]") + set_choice(params["choice"]) + closed = TRUE + SStgui.close_uis(src) + return TRUE + if("cancel") + closed = TRUE + SStgui.close_uis(src) + return TRUE + +/datum/tgui_modal/proc/set_choice(choice) + src.choice = choice + +/** + * # async tgui_modal + * + * An asynchronous version of tgui_modal to be used with callbacks instead of waiting on user responses. + */ +/datum/tgui_modal/async + /// The callback to be invoked by the tgui_modal upon having a choice made. + var/datum/callback/callback + +/datum/tgui_modal/async/New(mob/user, message, title, list/buttons, callback, timeout, autofocus) + ..(user, message, title, buttons, timeout, autofocus) + src.callback = callback + +/datum/tgui_modal/async/Destroy(force, ...) + QDEL_NULL(callback) + . = ..() + +/datum/tgui_modal/async/set_choice(choice) + . = ..() + if(!isnull(src.choice)) + callback?.InvokeAsync(src.choice) + +/datum/tgui_modal/async/wait() + return diff --git a/code/modules/tgui/tgui_input_list.dm b/code/modules/tgui/tgui_input_list.dm new file mode 100644 index 0000000000000..2eff761716b3b --- /dev/null +++ b/code/modules/tgui/tgui_input_list.dm @@ -0,0 +1,202 @@ +/** + * Creates a TGUI input list 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. + * * items - The options that can be chosen by the user, each string is assigned a button on the UI. + * * default - If an option is already preselected on the UI. Current values, etc. + * * 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(mob/user, message, title = "Select", list/items, default, timeout = 0) + if (!user) + user = usr + if(!length(items)) + return + 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(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) + // return input(user, message, title) as null|anything in items + var/datum/tgui_list_input/input = new(user, message, title, items, default, timeout) + input.ui_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. + * * items - The options that can be chosen by the user, each string is assigned a button on the UI. + * * default - If an option is already preselected on the UI. Current values, etc. + * * 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 = "Select", list/items, default, datum/callback/callback, timeout = 60 SECONDS) + if (!user) + user = usr + if(!length(items)) + return + 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(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) + // return input(user, message, title) as null|anything in items + var/datum/tgui_list_input/async/input = new(user, message, title, items, default, callback, timeout) + input.ui_interact(user) + +/** + * # tgui_list_input + * + * Datum used for instantiating and using a TGUI-controlled list input that prompts the user with + * a message and shows a list of selectable options + */ +/datum/tgui_list_input + /// The title of the TGUI window + var/title + /// The textual body of the TGUI window + var/message + /// The list of items (responses) provided on the TGUI window + var/list/items + /// Buttons (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 button 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. + var/timeout + /// 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/items, default, timeout) + src.title = title + src.message = message + 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 items) + if(!i) + 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_items) + + src.items += string_key + src.items_map[string_key] = i + + if (timeout) + src.timeout = timeout + start_time = world.time + QDEL_IN(src, timeout) + +/datum/tgui_list_input/Destroy(force, ...) + SStgui.close_uis(src) + QDEL_NULL(items) + . = ..() + +/** + * Waits for a user's response to the tgui_list_input's prompt before returning. Returns early if + * the window was closed by the user. + */ +/datum/tgui_list_input/proc/wait() + while (!choice && !closed) + stoplag(1) + +/datum/tgui_list_input/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ListInputModal") + ui.open() + +/datum/tgui_list_input/ui_close(mob/user) + . = ..() + closed = TRUE + +/datum/tgui_list_input/ui_state(mob/user) + return GLOB.always_state + +/datum/tgui_list_input/ui_static_data(mob/user) + . = list() + .["items"] = items + +/datum/tgui_list_input/ui_data(mob/user) + . = list() + .["init_value"] = default || items[1] + .["message"] = message + .["preferences"] = list() + .["preferences"]["large_buttons"] = TRUE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large) + .["preferences"]["swapped_buttons"] = TRUE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped) + .["title"] = title + if(timeout) + .["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1) + +/datum/tgui_list_input/ui_act(action, list/params) + . = ..() + if (.) + return + switch(action) + if("submit") + if (!(params["entry"] in items)) + return + set_choice(items_map[params["entry"]]) + closed = TRUE + SStgui.close_uis(src) + return TRUE + if("cancel") + closed = TRUE + SStgui.close_uis(src) + return TRUE + +/datum/tgui_list_input/proc/set_choice(choice) + src.choice = choice + +/** + * # async tgui_list_input + * + * An asynchronous version of tgui_list_input to be used with callbacks instead of waiting on user responses. + */ +/datum/tgui_list_input/async + /// 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/items, default, callback, timeout) + ..(user, message, title, items, default, timeout) + src.callback = callback + +/datum/tgui_list_input/async/Destroy(force, ...) + QDEL_NULL(callback) + . = ..() + +/datum/tgui_list_input/async/set_choice(choice) + . = ..() + if(!isnull(src.choice)) + callback?.InvokeAsync(src.choice) + +/datum/tgui_list_input/async/wait() + return diff --git a/code/modules/tgui/tgui_input_number.dm b/code/modules/tgui/tgui_input_number.dm new file mode 100644 index 0000000000000..19fed34c3eaca --- /dev/null +++ b/code/modules/tgui/tgui_input_number.dm @@ -0,0 +1,207 @@ +/** + * 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(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) + // 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.ui_interact(user) + number_input.wait() + if (number_input) + . = number_input.entry + qdel(number_input) + +/** + * Creates an asynchronous TGUI number input window with an associated callback. + * + * This proc should be used to create number inputs that invoke a callback with the user's entry. + * + * 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. + * * callback - The callback to be invoked when a choice is made. + * * 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_async(mob/user, message, title = "Number Input", default = 0, max_value = 10000, min_value = 0, datum/callback/callback, timeout = 60 SECONDS, 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(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) + // 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/async/number_input = new(user, message, title, default, max_value, min_value, callback, timeout, round_value) + number_input.ui_interact(user) + +/** + * # 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 + /// 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 + /// Whether the submitted number is rounded down into an integer. + var/round_value + + +/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) + . = ..() + +/** + * 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/ui_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/ui_close(mob/user) + . = ..() + closed = TRUE + +/datum/tgui_input_number/ui_state(mob/user) + return GLOB.always_state + +/datum/tgui_input_number/ui_data(mob/user) + . = list() + .["init_value"] = default // Default is a reserved keyword + .["max_value"] = max_value + .["message"] = message + .["min_value"] = min_value + .["preferences"] = list() + .["preferences"]["large_buttons"] = TRUE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large) + .["preferences"]["swapped_buttons"] = TRUE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped) + .["title"] = title + if(timeout) + .["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS)) + +/datum/tgui_input_number/ui_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 + +/** + * # async tgui_input_number + * + * An asynchronous version of tgui_input_number to be used with callbacks instead of waiting on user responses. + */ +/datum/tgui_input_number/async + /// The callback to be invoked by the tgui_input_number upon having a choice made. + var/datum/callback/callback + +/datum/tgui_input_number/async/New(mob/user, message, title, default, max_value, min_value, callback, timeout) + ..(user, message, title, default, max_value, min_value, timeout) + src.callback = callback + +/datum/tgui_input_number/async/Destroy(force, ...) + QDEL_NULL(callback) + . = ..() + +/datum/tgui_input_number/async/set_entry(entry) + . = ..() + if(!isnull(src.entry)) + callback?.InvokeAsync(src.entry) + +/datum/tgui_input_number/async/wait() + return diff --git a/code/modules/tgui/tgui_input_text.dm b/code/modules/tgui/tgui_input_text.dm new file mode 100644 index 0000000000000..3326f78a2cb83 --- /dev/null +++ b/code/modules/tgui/tgui_input_text.dm @@ -0,0 +1,211 @@ +/** + * 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 (!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(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) + 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.ui_interact(user) + text_input.wait() + if (text_input) + . = text_input.entry + qdel(text_input) + +/** + * Creates an asynchronous TGUI text input window with an associated callback. + * + * This proc should be used to create text inputs that invoke a callback with the user's entry. + * 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. + * * multiline - Bool that determines if the input box is much larger. Good for large messages, laws, etc. + * * encode - If toggled, input is filtered via html_encode. Setting this to FALSE gives raw input. + * * callback - The callback to be invoked when a choice is made. + */ +/proc/tgui_input_text_async(mob/user, message = "", title = "Text Input", default, max_length = MAX_MESSAGE_LEN, multiline = FALSE, encode = TRUE, datum/callback/callback, timeout = 60 SECONDS) + 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(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input)) + 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/async/text_input = new(user, message, title, default, max_length, multiline, encode, callback, timeout) + text_input.ui_interact(user) + +/** + * # 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_input_text's prompt before returning. Returns early if + * the window was closed by the user. + */ +/datum/tgui_input_text/proc/wait() + while (!entry && !closed && !QDELETED(src)) + stoplag(1) + +/datum/tgui_input_text/ui_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/ui_close(mob/user) + . = ..() + closed = TRUE + +/datum/tgui_input_text/ui_state(mob/user) + return GLOB.always_state + +/datum/tgui_input_text/ui_data(mob/user) + . = list() + .["max_length"] = max_length + .["message"] = message + .["multiline"] = multiline + .["placeholder"] = default // Default is a reserved keyword + .["preferences"] = list() + .["preferences"]["large_buttons"] = TRUE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large) + .["preferences"]["swapped_buttons"] = TRUE//user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped) + .["title"] = title + if(timeout) + .["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS)) + +/datum/tgui_input_text/ui_act(action, list/params) + . = ..() + if (.) + return + switch(action) + if("submit") + if(max_length) + if(length(params["entry"]) > max_length) + CRASH("[usr] typed a text string longer than the max length") + if(encode && (length(html_encode(params["entry"])) > max_length)) + to_chat(usr, "Input uses special characters, thus reducing the maximum length.") + set_entry(params["entry"]) + closed = TRUE + SStgui.close_uis(src) + return TRUE + if("cancel") + closed = TRUE + SStgui.close_uis(src) + 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) + +/** + * # async tgui_input_text + * + * An asynchronous version of tgui_input_text to be used with callbacks instead of waiting on user responses. + */ +/datum/tgui_input_text/async + // The callback to be invoked by the tgui_input_text upon having a choice made. + var/datum/callback/callback + +/datum/tgui_input_text/async/New(mob/user, message, title, default, max_length, multiline, encode, callback, timeout) + ..(user, message, title, default, max_length, multiline, encode, timeout) + src.callback = callback + +/datum/tgui_input_text/async/Destroy(force, ...) + QDEL_NULL(callback) + . = ..() + +/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/interface/interface.dm b/interface/interface.dm index 58010814f4f0a..8ad6c9dbce6c5 100644 --- a/interface/interface.dm +++ b/interface/interface.dm @@ -63,6 +63,8 @@ if(GLOB.revdata.testmerge.len) message += "
The following experimental changes are active and are probably the cause of any new or sudden issues you may experience. If possible, please try to find a specific thread for your issue instead of posting to the general issue tracker:
" message += GLOB.revdata.GetTestMergeInfo(FALSE) + // We still use tgalert here because some people were concerned that if someone wanted to report that tgui wasn't working + // then the report issue button being tgui-based would be problematic. if(tgalert(src, message, "Report Issue","Yes","No")!="Yes") return var/static/issue_template = rustg_file_read(".github/ISSUE_TEMPLATE.md") diff --git a/tgui/packages/tgui/components/Autofocus.tsx b/tgui/packages/tgui/components/Autofocus.tsx new file mode 100644 index 0000000000000..28945dd7aa481 --- /dev/null +++ b/tgui/packages/tgui/components/Autofocus.tsx @@ -0,0 +1,19 @@ +import { Component, createRef } from 'inferno'; + +export class Autofocus extends Component { + ref = createRef(); + + componentDidMount() { + setTimeout(() => { + this.ref.current?.focus(); + }, 1); + } + + render() { + return ( +
+ {this.props.children} +
+ ); + } +} diff --git a/tgui/packages/tgui/components/RestrictedInput.js b/tgui/packages/tgui/components/RestrictedInput.js new file mode 100644 index 0000000000000..0a6e2cb440c38 --- /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 5ed972519eefa..9797751480aa0 100644 --- a/tgui/packages/tgui/components/index.js +++ b/tgui/packages/tgui/components/index.js @@ -5,6 +5,7 @@ */ export { AnimatedNumber } from './AnimatedNumber'; +export { Autofocus } from './Autofocus'; export { Blink } from './Blink'; export { BlockQuote } from './BlockQuote'; export { Box } from './Box'; @@ -34,6 +35,7 @@ export { OrbitalMapSvg } from './OrbitalMapSvg'; export { ProgressBar } from './ProgressBar'; export { ScrollableBox } from './ScrollableBox'; export { Popper } from './Popper'; +export { RestrictedInput } from './RestrictedInput'; export { Section } from './Section'; export { Slider } from './Slider'; export { Stack } from './Stack'; diff --git a/tgui/packages/tgui/interfaces/AlertModal.tsx b/tgui/packages/tgui/interfaces/AlertModal.tsx new file mode 100644 index 0000000000000..693abe346949c --- /dev/null +++ b/tgui/packages/tgui/interfaces/AlertModal.tsx @@ -0,0 +1,162 @@ +import { Loader } from './common/Loader'; +import { Preferences } from './common/InputButtons'; +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[]; + message: string; + preferences: Preferences; + timeout: number; + title: string; +}; + +const KEY_DECREMENT = -1; +const KEY_INCREMENT = 1; + +export const AlertModal = (_, context) => { + const { act, data } = useBackend(context); + const { + autofocus, + buttons = [], + message, + preferences, + timeout, + title, + } = data; + const { large_buttons } = preferences; + const [selected, setSelected] = useLocalState(context, 'selected', 0); + // Dynamically sets window height + const windowHeight + = 115 + + (message.length > 30 ? Math.ceil(message.length / 3) : 0) + + (message.length && large_buttons ? 5 : 0) + + (buttons.length > 2 ? buttons.length * 25 : 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.shiftKey && keyCode === KEY_TAB) + ) { + onKey(KEY_DECREMENT); + } else if (keyCode === KEY_RIGHT || keyCode === KEY_TAB) { + 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 = [], preferences } = data; + const { selected } = props; + const { large_buttons, swapped_buttons } = preferences; + const buttonDirection + = (buttons.length > 2 ? 'column' : 'row') + + (!swapped_buttons ? '-reverse' : ''); + + 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 { preferences } = data; + const { large_buttons } = preferences; + const { button, selected } = props; + + return ( + + ); +}; diff --git a/tgui/packages/tgui/interfaces/ListInputModal.tsx b/tgui/packages/tgui/interfaces/ListInputModal.tsx new file mode 100644 index 0000000000000..bed0fd862717d --- /dev/null +++ b/tgui/packages/tgui/interfaces/ListInputModal.tsx @@ -0,0 +1,248 @@ +import { Loader } from './common/Loader'; +import { InputButtons, Preferences } from './common/InputButtons'; +import { Button, Input, Section, Stack } from '../components'; +import { + KEY_A, + KEY_DOWN, + KEY_ESCAPE, + KEY_ENTER, + KEY_UP, + KEY_Z, +} from '../../common/keycodes'; +import { Window } from '../layouts'; +import { useBackend, useLocalState } from '../backend'; + +type ListInputData = { + items: string[]; + message: string; + init_value: string; + preferences: Preferences; + timeout: number; + title: string; +}; + +export const ListInputModal = (_, context) => { + const { act, data } = useBackend(context); + const { items = [], message, init_value, preferences, timeout, title } = data; + const { large_buttons } = preferences; + const [selected, setSelected] = useLocalState( + context, + 'selected', + items.indexOf(init_value) + ); + const [searchBarVisible, setSearchBarVisible] = useLocalState( + context, + 'searchBarVisible', + items.length > 9 + ); + const [searchQuery, setSearchQuery] = useLocalState( + context, + 'searchQuery', + '' + ); + // User presses up or down on keyboard + // Simulates clicking an item + const onArrowKey = (key: number) => { + const len = filteredItems.length - 1; + if (key === KEY_DOWN) { + if (selected === null || selected === len) { + setSelected(0); + document!.getElementById('0')?.scrollIntoView(); + } else { + setSelected(selected + 1); + document!.getElementById((selected + 1).toString())?.scrollIntoView(); + } + } else if (key === KEY_UP) { + if (selected === null || selected === 0) { + setSelected(len); + document!.getElementById(len.toString())?.scrollIntoView(); + } else { + setSelected(selected - 1); + document!.getElementById((selected - 1).toString())?.scrollIntoView(); + } + } + }; + // User selects an item with mouse + const onClick = (index: number) => { + if (index === selected) { + return; + } + setSelected(index); + }; + // User presses a letter key and searchbar is visible + const onFocusSearch = () => { + setSearchBarVisible(false); + setSearchBarVisible(true); + }; + // User presses a letter key with no searchbar visible + const onLetterSearch = (key: number) => { + const keyChar = String.fromCharCode(key); + const foundItem = items.find((item) => { + return item?.toLowerCase().startsWith(keyChar?.toLowerCase()); + }); + if (foundItem) { + const foundIndex = items.indexOf(foundItem); + setSelected(foundIndex); + document!.getElementById(foundIndex.toString())?.scrollIntoView(); + } + }; + // User types into search bar + const onSearch = (query: string) => { + if (query === searchQuery) { + return; + } + setSearchQuery(query); + setSelected(0); + document!.getElementById('0')?.scrollIntoView(); + }; + // User presses the search button + const onSearchBarToggle = () => { + setSearchBarVisible(!searchBarVisible); + setSearchQuery(''); + }; + const filteredItems = items.filter((item) => + item?.toLowerCase().includes(searchQuery.toLowerCase()) + ); + // Dynamically changes the window height based on the message. + const windowHeight + = 325 + Math.ceil(message?.length / 3) + (large_buttons ? 5 : 0); + // Grabs the cursor when no search bar is visible. + if (!searchBarVisible) { + setTimeout(() => document!.getElementById(selected.toString())?.focus(), 1); + } + + return ( + + {timeout && } + { + const keyCode = window.event ? event.which : event.keyCode; + if (keyCode === KEY_DOWN || keyCode === KEY_UP) { + event.preventDefault(); + onArrowKey(keyCode); + } + if (keyCode === KEY_ENTER) { + event.preventDefault(); + act('submit', { entry: filteredItems[selected] }); + } + if (!searchBarVisible && keyCode >= KEY_A && keyCode <= KEY_Z) { + event.preventDefault(); + onLetterSearch(keyCode); + } + if (keyCode === KEY_ESCAPE) { + event.preventDefault(); + act('cancel'); + } + }}> +
onSearchBarToggle()} + /> + } + className="ListInput__Section" + fill + title={message}> + + + + + {searchBarVisible && ( + + )} + + + + +
+
+
+ ); +}; + +/** + * Displays the list of selectable items. + * If a search query is provided, filters the items. + */ +const ListDisplay = (props, context) => { + const { act } = useBackend(context); + const { filteredItems, onClick, onFocusSearch, searchBarVisible, selected } + = props; + + return ( +
+ {filteredItems.map((item, index) => { + return ( + + ); + })} +
+ ); +}; + +/** + * Renders a search bar input. + * Closing the bar defaults input to an empty string. + */ +const SearchBar = (props, context) => { + const { act } = useBackend(context); + const { filteredItems, onSearch, searchQuery, selected } = props; + + return ( + { + event.preventDefault(); + act('submit', { entry: filteredItems[selected] }); + }} + onInput={(_, value) => onSearch(value)} + placeholder="Search..." + value={searchQuery} + /> + ); +}; diff --git a/tgui/packages/tgui/interfaces/NumberInputModal.tsx b/tgui/packages/tgui/interfaces/NumberInputModal.tsx new file mode 100644 index 0000000000000..65b30f7b5f83b --- /dev/null +++ b/tgui/packages/tgui/interfaces/NumberInputModal.tsx @@ -0,0 +1,118 @@ +import { Loader } from './common/Loader'; +import { InputButtons, Preferences } from './common/InputButtons'; +import { KEY_ENTER, KEY_ESCAPE } from '../../common/keycodes'; +import { useBackend, useLocalState } from '../backend'; +import { Box, Button, RestrictedInput, Section, Stack } from '../components'; +import { Window } from '../layouts'; + +type NumberInputData = { + max_value: number | null; + message: string; + min_value: number | null; + init_value: number; + preferences: Preferences; + timeout: number; + title: string; +}; + +export const NumberInputModal = (_, context) => { + const { act, data } = useBackend(context); + const { message, init_value, preferences, timeout, title } = data; + const { large_buttons } = preferences; + const [input, setInput] = useLocalState(context, 'input', init_value); + const onChange = (value: number) => { + if (value === input) { + return; + } + setInput(value); + }; + const onClick = (value: number) => { + if (value === input) { + return; + } + setInput(value); + }; + // Dynamically changes the window height based on the message. + const windowHeight + = 130 + + Math.ceil(message.length / 3) + + (message.length && large_buttons ? 5 : 0); + + return ( + + {timeout && } + { + const keyCode = window.event ? event.which : event.keyCode; + if (keyCode === KEY_ENTER) { + act('submit', { entry: input }); + } + if (keyCode === KEY_ESCAPE) { + act('cancel'); + } + }}> +
+ + + {message} + + + + + + + + +
+
+
+ ); +}; + +/** Gets the user input and invalidates if there's a constraint. */ +const InputArea = (props, context) => { + const { act, data } = useBackend(context); + const { min_value, max_value, init_value } = data; + const { input, onClick, onChange } = props; + + return ( + + +