diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm
index cc018287a5e..274d68395bc 100644
--- a/code/__HELPERS/game.dm
+++ b/code/__HELPERS/game.dm
@@ -391,8 +391,9 @@
SEND_SOUND(M, 'sound/misc/notice2.ogg') //Alerting them to their consideration
if(flashwindow)
window_flash(M.client)
- switch(ignore_category ? askuser(M,Question,"Please answer in [DisplayTimeText(poll_time)]!","Yes","No","Never for this round", StealFocus=0, Timeout=poll_time) : askuser(M,Question,"Please answer in [DisplayTimeText(poll_time)]!","Yes","No", StealFocus=0, Timeout=poll_time))
- if(1)
+ var/list/answers = ignore_category ? list("Yes", "No", "Never for this round") : list("Yes", "No")
+ switch(tgui_alert(M, Question, "A limited-time offer!", answers, timeout=poll_time))
+ if("Yes")
to_chat(M, "Choice registered: Yes.")
if(time_passed + poll_time <= world.time)
to_chat(M, "Sorry, you answered too late to be considered!")
@@ -400,10 +401,10 @@
candidates -= M
else
candidates += M
- if(2)
+ if("No")
to_chat(M, "Choice registered: No.")
candidates -= M
- if(3)
+ if("Never for this round")
var/list/L = GLOB.poll_ignore[ignore_category]
if(!L)
GLOB.poll_ignore[ignore_category] = list()
diff --git a/code/__HELPERS/hearted.dm b/code/__HELPERS/hearted.dm
index 58b52603e34..18d45abc18a 100644
--- a/code/__HELPERS/hearted.dm
+++ b/code/__HELPERS/hearted.dm
@@ -28,13 +28,13 @@
if(!heart_winner.client)
return
heart_winner.client.prefs.save_preferences()
- tgalert(heart_winner, "Someone anonymously thanked you for being kind during the last round!", "<3!", "Okay")
+ tgui_alert(heart_winner, "Someone anonymously thanked you for being kind during the last round!", "<3!", list("Okay"))
/// Ask someone if they'd like to award a commendation for the round, 3 tries to get the name they want before we give up
/mob/proc/query_heart(attempt=1)
if(!mind || !client || attempt > 3)
return
- if(attempt == 1 && tgalert(src, "Was there another character you noticed being kind this round that you would like to anonymously thank?", "<3?", "Yes", "No", StealFocus=FALSE, Timeout = 30 SECONDS) != "Yes")
+ if(attempt == 1 && tgui_alert(src, "Was there another character you noticed being kind this round that you would like to anonymously thank?", "<3?", list("Yes", "No"), timeout = 30 SECONDS) != "Yes")
return
var/heart_nominee
@@ -61,7 +61,7 @@
if(heart_contender == src)
continue
- switch(tgalert(src, "Is this the person: [heart_contender.real_name]?", "<3?", "Yes!", "Nope", "Cancel", Timeout = 15 SECONDS))
+ switch(tgui_alert(src, "Is this the person: [heart_contender.real_name]?", "<3?", list("Yes!", "Nope", "Cancel"), timeout = 15 SECONDS))
if("Yes!")
nominate_heart(heart_contender)
return
diff --git a/code/datums/browser.dm b/code/datums/browser.dm
index 6604c058ccc..bfdd8cc9c82 100644
--- a/code/datums/browser.dm
+++ b/code/datums/browser.dm
@@ -153,11 +153,35 @@
opentime = 0
close()
-//designed as a drop in replacement for alert(); functions the same. (outside of needing User specified)
-/proc/tgalert(mob/User, Message, Title, Button1="Ok", Button2, Button3, StealFocus = 1, Timeout = 6000)
+/**
+ * **DEPRECATED: USE tgui_alert(...) INSTEAD**
+ *
+ * Designed as a drop in replacement for alert(); functions the same. (outside of needing User specified)
+ * Arguments:
+ * * User - The user to show the alert to.
+ * * Message - The textual body of the alert.
+ * * Title - The title of the alert's window.
+ * * Button1 - The first button option.
+ * * Button2 - The second button option.
+ * * Button3 - The third button option.
+ * * StealFocus - Boolean operator controlling if the alert will steal the user's window focus.
+ * * Timeout - The timeout of the window, after which no responses will be valid.
+ */
+/proc/tgalert(mob/User, Message, Title, Button1="Ok", Button2, Button3, StealFocus = TRUE, Timeout = 6000)
if (!User)
User = usr
- switch(askuser(User, Message, Title, Button1, Button2, Button3, StealFocus, Timeout))
+ if (!istype(User))
+ if (istype(User, /client))
+ var/client/client = User
+ User = client.mob
+ else
+ return
+
+ // Get user's response using a modal
+ var/datum/browser/modal/alert/A = new(User, Message, Title, Button1, Button2, Button3, StealFocus, Timeout)
+ A.open()
+ A.wait()
+ switch(A.selectedbutton)
if (1)
return Button1
if (2)
@@ -165,20 +189,6 @@
if (3)
return Button3
-//Same shit, but it returns the button number, could at some point support unlimited button amounts.
-/proc/askuser(mob/User,Message, Title, Button1="Ok", Button2, Button3, StealFocus = 1, Timeout = 6000)
- if (!istype(User))
- if (istype(User, /client/))
- var/client/C = User
- User = C.mob
- else
- return
- var/datum/browser/modal/alert/A = new(User, Message, Title, Button1, Button2, Button3, StealFocus, Timeout)
- A.open()
- A.wait()
- if (A.selectedbutton)
- return A.selectedbutton
-
/datum/browser/modal
var/opentime = 0
var/timeout
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index 0a54c755438..bb548e460d5 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -844,7 +844,7 @@
return
if (SSticker.HasRoundStarted())
- if (askuser(usr, "The game has already started. Would you like to save this as the default mode effective next round?", "Save mode", "Yes", "Cancel", Timeout = null) == 1)
+ if (tgui_alert(usr, "The game has already started. Would you like to save this as the default mode effective next round?", "Save mode", list("Yes", "Cancel"), timeout = 0) == "Yes")
SSticker.save_mode(href_list["c_mode2"])
HandleCMode()
return
@@ -853,7 +853,7 @@
message_admins("[key_name_admin(usr)] set the mode as [GLOB.master_mode].")
to_chat(world, "The mode is now: [GLOB.master_mode]", confidential = TRUE)
Game() // updates the main game menu
- if (askuser(usr, "Would you like to save this as the default mode for the server?", "Save mode", "Yes", "No", Timeout = null) == 1)
+ if (tgui_alert(usr, "Would you like to save this as the default mode for the server?", "Save mode", list("Yes", "No"), timeout = 0) == "Yes")
SSticker.save_mode(GLOB.master_mode)
HandleCMode()
@@ -2298,7 +2298,7 @@
to_chat(usr, "The round must be in progress to use this!")
return
var/mob/heart_recepient = locate(href_list["admincommend"])
- if(tgalert(usr, "Are you sure you'd like to anonymously commend [heart_recepient.ckey]? NOTE: This is logged, please use this sparingly and only for actual kind behavior, not as a reward for your friends.", "<3?", "Yes", "No") == "No")
+ if(tgui_alert(usr, "Are you sure you'd like to anonymously commend [heart_recepient.ckey]? NOTE: This is logged, please use this sparingly and only for actual kind behavior, not as a reward for your friends.", "<3?", list("Yes", "No")) == "No")
return
usr.nominate_heart(heart_recepient)
diff --git a/code/modules/admin/view_variables/mass_edit_variables.dm b/code/modules/admin/view_variables/mass_edit_variables.dm
index 532a42611c2..db1536ef00a 100644
--- a/code/modules/admin/view_variables/mass_edit_variables.dm
+++ b/code/modules/admin/view_variables/mass_edit_variables.dm
@@ -5,7 +5,7 @@
var/method = 0 //0 means strict type detection while 1 means this type and all subtypes (IE: /obj/item with this set to 1 will set it to ALL items)
- if(tgalert(src, "Are you sure you'd like to mass-modify every instance of the [var_name] variable?", "This can break everything if you do not know what you are doing.", "Yes", "No") != "Yes")
+ if(tgui_alert(src, "Are you sure you'd like to mass-modify every instance of the [var_name] variable? This can break everything if you do not know what you are doing.", "Slow down, chief!", list("Yes", "No")) != "Yes")
return
if(!check_rights(R_VAREDIT))
diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm
index 881c7fa65b7..5a90e016d93 100644
--- a/code/modules/client/preferences.dm
+++ b/code/modules/client/preferences.dm
@@ -1670,7 +1670,7 @@ GLOBAL_LIST_EMPTY(preferences_datums)
save_preferences()
if("keybindings_reset")
- var/choice = tgalert(user, "Would you prefer 'hotkey' or 'classic' defaults?", "Setup keybindings", "Hotkey", "Classic", "Cancel")
+ var/choice = tgui_alert(user, "Would you prefer 'hotkey' or 'classic' defaults?", "Setup keybindings", list("Hotkey", "Classic", "Cancel"))
if(choice == "Cancel")
ShowChoices(user)
return
diff --git a/code/modules/tgui/tgui_alert.dm b/code/modules/tgui/tgui_alert.dm
new file mode 100644
index 00000000000..1a86cca705b
--- /dev/null
+++ b/code/modules/tgui/tgui_alert.dm
@@ -0,0 +1,160 @@
+/**
+ * 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.
+ */
+/proc/tgui_alert(mob/user, message, title, list/buttons, timeout = 60 SECONDS)
+ if (!user)
+ user = usr
+ if (!istype(user))
+ if (istype(user, /client))
+ var/client/client = user
+ user = client.mob
+ else
+ return
+ var/datum/tgui_modal/alert = new(user, message, title, buttons, timeout)
+ 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. Set to zero for no timeout.
+ */
+/proc/tgui_alert_async(mob/user, message, title, list/buttons, 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
+ var/datum/tgui_modal/async/alert = new(user, message, title, buttons, callback, timeout)
+ 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
+ /// 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)
+ src.title = title
+ src.message = message
+ src.buttons = buttons.Copy()
+ 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)
+ 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(
+ "title" = title,
+ "message" = message,
+ "buttons" = buttons
+ )
+
+ 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))
+ return
+ choice = params["choice"]
+ SStgui.close_uis(src)
+ return TRUE
+
+/**
+ * # 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)
+ ..(user, title, message, buttons, timeout)
+ src.callback = callback
+
+/datum/tgui_modal/async/Destroy(force, ...)
+ QDEL_NULL(callback)
+ . = ..()
+
+/datum/tgui_modal/async/ui_close(mob/user)
+ . = ..()
+ qdel(src)
+
+/datum/tgui_modal/async/ui_act(action, list/params)
+ . = ..()
+ if (!. || choice == null)
+ return
+ callback.InvokeAsync(choice)
+ qdel(src)
+
+/datum/tgui_modal/async/wait()
+ return
diff --git a/interface/interface.dm b/interface/interface.dm
index ba85efb2386..251cb934ff1 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
diff --git a/tgstation.dme b/tgstation.dme
index f57b6b0af9b..42cd597c674 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -3162,6 +3162,7 @@
#include "code\modules\tgui\external.dm"
#include "code\modules\tgui\states.dm"
#include "code\modules\tgui\tgui.dm"
+#include "code\modules\tgui\tgui_alert.dm"
#include "code\modules\tgui\tgui_window.dm"
#include "code\modules\tgui\states\admin.dm"
#include "code\modules\tgui\states\always.dm"
diff --git a/tgui/packages/tgui/interfaces/AlertModal.js b/tgui/packages/tgui/interfaces/AlertModal.js
new file mode 100644
index 00000000000..f33a260c0f9
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/AlertModal.js
@@ -0,0 +1,73 @@
+/**
+ * @file
+ * @copyright 2020 bobbahbrown (https://github.com/bobbahbrown)
+ * @license MIT
+ */
+
+import { clamp01 } from 'common/math';
+import { useBackend } from '../backend';
+import { Box, Button, Flex } from '../components';
+import { Window } from '../layouts';
+
+export const AlertModal = (props, context) => {
+ const { act, data } = useBackend(context);
+ const {
+ title,
+ message,
+ buttons,
+ timeout,
+ } = data;
+
+ return (
+
'+(n?e:ee(e,!0))+"\n":""+(n?e:ee(e,!0))+"\n"}return e}(),t.blockquote=function(){function e(e){return"\n"+e+"\n"}return e}(),t.html=function(){function e(e){return e}return e}(),t.heading=function(){function e(e,t,n,r){return this.options.headerIds?"
"+e+"
\n"}return e}(),t.table=function(){function e(e,t){return t&&(t=""+t+""),""+e+""}return e}(),t.br=function(){function e(){return this.options.xhtml?""+se(c.message+"",!0)+"";throw c}}return pe.options=pe.setOptions=function(e){return ue(pe.defaults,e),fe(pe.defaults),pe},pe.getDefaults=le,pe.defaults=de,pe.use=function(e){var t=ue({},e);if(e.renderer&&function(){var n=pe.defaults.renderer||new te,r=function(){function t(t){var r=n[t];n[t]=function(){for(var o=arguments.length,i=new Array(o),a=0;a
'+(n?e:ee(e,!0))+"\n":""+(n?e:ee(e,!0))+"\n"}return e}(),t.blockquote=function(){function e(e){return"\n"+e+"\n"}return e}(),t.html=function(){function e(e){return e}return e}(),t.heading=function(){function e(e,t,n,r){return this.options.headerIds?"
"+e+"
\n"}return e}(),t.table=function(){function e(e,t){return t&&(t=""+t+""),""+e+""}return e}(),t.br=function(){function e(){return this.options.xhtml?""+se(c.message+"",!0)+"";throw c}}return pe.options=pe.setOptions=function(e){return ue(pe.defaults,e),fe(pe.defaults),pe},pe.getDefaults=le,pe.defaults=de,pe.use=function(e){var t=ue({},e);if(e.renderer&&function(){var n=pe.defaults.renderer||new te,r=function(){function t(t){var r=n[t];n[t]=function(){for(var o=arguments.length,i=new Array(o),a=0;a