mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-11 18:53:06 +00:00
TGUI Input Framework
This commit is contained in:
15
code/__defines/cooldowns.dm
Normal file
15
code/__defines/cooldowns.dm
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* Cooldown system based on storing world.time on a variable, plus the cooldown time.
|
||||||
|
* Better performance over timer cooldowns, lower control. Same functionality.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define COOLDOWN_DECLARE(cd_index) var/##cd_index = 0
|
||||||
|
|
||||||
|
#define COOLDOWN_START(cd_source, cd_index, cd_time) (cd_source.cd_index = world.time + (cd_time))
|
||||||
|
|
||||||
|
//Returns true if the cooldown has run its course, false otherwise
|
||||||
|
#define COOLDOWN_FINISHED(cd_source, cd_index) (cd_source.cd_index < world.time)
|
||||||
|
|
||||||
|
#define COOLDOWN_RESET(cd_source, cd_index) cd_source.cd_index = 0
|
||||||
|
|
||||||
|
#define COOLDOWN_TIMELEFT(cd_source, cd_index) (max(0, cd_source.cd_index - world.time))
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
/// Maximum ping timeout allowed to detect zombie windows
|
/// Maximum ping timeout allowed to detect zombie windows
|
||||||
#define TGUI_PING_TIMEOUT 4 SECONDS
|
#define TGUI_PING_TIMEOUT 4 SECONDS
|
||||||
|
/// Used for rate-limiting to prevent DoS by excessively refreshing a TGUI window
|
||||||
|
#define TGUI_REFRESH_FULL_UPDATE_COOLDOWN 5 SECONDS
|
||||||
|
|
||||||
/// Window does not exist
|
/// Window does not exist
|
||||||
#define TGUI_WINDOW_CLOSED 0
|
#define TGUI_WINDOW_CLOSED 0
|
||||||
|
|||||||
@@ -447,7 +447,7 @@ GLOBAL_LIST_BOILERPLATE(allCasters, /obj/machinery/newscaster)
|
|||||||
return TRUE
|
return TRUE
|
||||||
|
|
||||||
if("set_new_message")
|
if("set_new_message")
|
||||||
msg = sanitize(tgui_input_message(usr, "Write your Feed story", "Network Channel Handler"))
|
msg = sanitize(tgui_input_text(usr, "Write your Feed story", "Network Channel Handler", multiline = TRUE))
|
||||||
return TRUE
|
return TRUE
|
||||||
|
|
||||||
if("set_new_title")
|
if("set_new_title")
|
||||||
|
|||||||
@@ -617,7 +617,7 @@ var/global/floorIsLava = 0
|
|||||||
set desc="Announce your desires to the world"
|
set desc="Announce your desires to the world"
|
||||||
if(!check_rights(0)) return
|
if(!check_rights(0)) return
|
||||||
|
|
||||||
var/message = tgui_input_message(usr, "Global message to send:", "Admin Announce")
|
var/message = tgui_input_text(usr, "Global message to send:", "Admin Announce", multiline = TRUE)
|
||||||
if(message)
|
if(message)
|
||||||
if(!check_rights(R_SERVER,0))
|
if(!check_rights(R_SERVER,0))
|
||||||
message = sanitize(message, 500, extra = 0)
|
message = sanitize(message, 500, extra = 0)
|
||||||
|
|||||||
@@ -140,7 +140,7 @@
|
|||||||
return TOPIC_REFRESH
|
return TOPIC_REFRESH
|
||||||
|
|
||||||
else if(href_list["metadata"])
|
else if(href_list["metadata"])
|
||||||
var/new_metadata = sanitize(tgui_input_message(user, "Enter any information you'd like others to see, such as Roleplay-preferences:", "Game Preference" , html_decode(pref.metadata)), extra = 0) //VOREStation Edit
|
var/new_metadata = sanitize(tgui_input_text(user, "Enter any information you'd like others to see, such as Roleplay-preferences:", "Game Preference" , html_decode(pref.metadata), multiline = TRUE), extra = 0) //VOREStation Edit
|
||||||
if(new_metadata && CanUseTopic(user))
|
if(new_metadata && CanUseTopic(user))
|
||||||
pref.metadata = new_metadata
|
pref.metadata = new_metadata
|
||||||
return TOPIC_REFRESH
|
return TOPIC_REFRESH
|
||||||
|
|||||||
@@ -3,30 +3,34 @@
|
|||||||
sort_order = 1
|
sort_order = 1
|
||||||
|
|
||||||
/datum/category_item/player_setup_item/player_global/ui/load_preferences(var/savefile/S)
|
/datum/category_item/player_setup_item/player_global/ui/load_preferences(var/savefile/S)
|
||||||
S["UI_style"] >> pref.UI_style
|
S["UI_style"] >> pref.UI_style
|
||||||
S["UI_style_color"] >> pref.UI_style_color
|
S["UI_style_color"] >> pref.UI_style_color
|
||||||
S["UI_style_alpha"] >> pref.UI_style_alpha
|
S["UI_style_alpha"] >> pref.UI_style_alpha
|
||||||
S["ooccolor"] >> pref.ooccolor
|
S["ooccolor"] >> pref.ooccolor
|
||||||
S["tooltipstyle"] >> pref.tooltipstyle
|
S["tooltipstyle"] >> pref.tooltipstyle
|
||||||
S["client_fps"] >> pref.client_fps
|
S["client_fps"] >> pref.client_fps
|
||||||
S["ambience_freq"] >> pref.ambience_freq
|
S["ambience_freq"] >> pref.ambience_freq
|
||||||
S["ambience_chance"] >> pref.ambience_chance
|
S["ambience_chance"] >> pref.ambience_chance
|
||||||
S["tgui_fancy"] >> pref.tgui_fancy
|
S["tgui_fancy"] >> pref.tgui_fancy
|
||||||
S["tgui_lock"] >> pref.tgui_lock
|
S["tgui_lock"] >> pref.tgui_lock
|
||||||
S["tgui_input_mode"] >> pref.tgui_input_mode
|
S["tgui_input_mode"] >> pref.tgui_input_mode
|
||||||
|
S["tgui_large_buttons"] >> pref.tgui_large_buttons
|
||||||
|
S["tgui_swapped_buttons"] >> pref.tgui_swapped_buttons
|
||||||
|
|
||||||
/datum/category_item/player_setup_item/player_global/ui/save_preferences(var/savefile/S)
|
/datum/category_item/player_setup_item/player_global/ui/save_preferences(var/savefile/S)
|
||||||
S["UI_style"] << pref.UI_style
|
S["UI_style"] << pref.UI_style
|
||||||
S["UI_style_color"] << pref.UI_style_color
|
S["UI_style_color"] << pref.UI_style_color
|
||||||
S["UI_style_alpha"] << pref.UI_style_alpha
|
S["UI_style_alpha"] << pref.UI_style_alpha
|
||||||
S["ooccolor"] << pref.ooccolor
|
S["ooccolor"] << pref.ooccolor
|
||||||
S["tooltipstyle"] << pref.tooltipstyle
|
S["tooltipstyle"] << pref.tooltipstyle
|
||||||
S["client_fps"] << pref.client_fps
|
S["client_fps"] << pref.client_fps
|
||||||
S["ambience_freq"] << pref.ambience_freq
|
S["ambience_freq"] << pref.ambience_freq
|
||||||
S["ambience_chance"] << pref.ambience_chance
|
S["ambience_chance"] << pref.ambience_chance
|
||||||
S["tgui_fancy"] << pref.tgui_fancy
|
S["tgui_fancy"] << pref.tgui_fancy
|
||||||
S["tgui_lock"] << pref.tgui_lock
|
S["tgui_lock"] << pref.tgui_lock
|
||||||
S["tgui_input_mode"] << pref.tgui_input_mode
|
S["tgui_input_mode"] << pref.tgui_input_mode
|
||||||
|
S["tgui_large_buttons"] << pref.tgui_large_buttons
|
||||||
|
S["tgui_swapped_buttons"] << pref.tgui_swapped_buttons
|
||||||
|
|
||||||
/datum/category_item/player_setup_item/player_global/ui/sanitize_preferences()
|
/datum/category_item/player_setup_item/player_global/ui/sanitize_preferences()
|
||||||
pref.UI_style = sanitize_inlist(pref.UI_style, all_ui_styles, initial(pref.UI_style))
|
pref.UI_style = sanitize_inlist(pref.UI_style, all_ui_styles, initial(pref.UI_style))
|
||||||
@@ -40,6 +44,8 @@
|
|||||||
pref.tgui_fancy = sanitize_integer(pref.tgui_fancy, 0, 1, initial(pref.tgui_fancy))
|
pref.tgui_fancy = sanitize_integer(pref.tgui_fancy, 0, 1, initial(pref.tgui_fancy))
|
||||||
pref.tgui_lock = sanitize_integer(pref.tgui_lock, 0, 1, initial(pref.tgui_lock))
|
pref.tgui_lock = sanitize_integer(pref.tgui_lock, 0, 1, initial(pref.tgui_lock))
|
||||||
pref.tgui_input_mode = sanitize_integer(pref.tgui_input_mode, 0, 1, initial(pref.tgui_input_mode))
|
pref.tgui_input_mode = sanitize_integer(pref.tgui_input_mode, 0, 1, initial(pref.tgui_input_mode))
|
||||||
|
pref.tgui_large_buttons = sanitize_integer(pref.tgui_large_buttons, 0, 1, initial(pref.tgui_large_buttons))
|
||||||
|
pref.tgui_swapped_buttons = sanitize_integer(pref.tgui_swapped_buttons, 0, 1, initial(pref.tgui_swapped_buttons))
|
||||||
|
|
||||||
/datum/category_item/player_setup_item/player_global/ui/content(var/mob/user)
|
/datum/category_item/player_setup_item/player_global/ui/content(var/mob/user)
|
||||||
. = "<b>UI Style:</b> <a href='?src=\ref[src];select_style=1'><b>[pref.UI_style]</b></a><br>"
|
. = "<b>UI Style:</b> <a href='?src=\ref[src];select_style=1'><b>[pref.UI_style]</b></a><br>"
|
||||||
@@ -52,7 +58,9 @@
|
|||||||
. += "<b>Ambience Chance:</b> <a href='?src=\ref[src];select_ambience_chance=1'><b>[pref.ambience_chance]</b></a><br>"
|
. += "<b>Ambience Chance:</b> <a href='?src=\ref[src];select_ambience_chance=1'><b>[pref.ambience_chance]</b></a><br>"
|
||||||
. += "<b>tgui Window Mode:</b> <a href='?src=\ref[src];tgui_fancy=1'><b>[(pref.tgui_fancy) ? "Fancy (default)" : "Compatible (slower)"]</b></a><br>"
|
. += "<b>tgui Window Mode:</b> <a href='?src=\ref[src];tgui_fancy=1'><b>[(pref.tgui_fancy) ? "Fancy (default)" : "Compatible (slower)"]</b></a><br>"
|
||||||
. += "<b>tgui Window Placement:</b> <a href='?src=\ref[src];tgui_lock=1'><b>[(pref.tgui_lock) ? "Primary Monitor" : "Free (default)"]</b></a><br>"
|
. += "<b>tgui Window Placement:</b> <a href='?src=\ref[src];tgui_lock=1'><b>[(pref.tgui_lock) ? "Primary Monitor" : "Free (default)"]</b></a><br>"
|
||||||
. += "<b>Input Mode (Say, Me, Whisper, Subtle):</b> <a href='?src=\ref[src];tgui_input_mode=1'><b>[(pref.tgui_input_mode) ? "TGUI" : "BYOND (default)"]</b></a><br>"
|
. += "<b>TGUI Input Framework:</b> <a href='?src=\ref[src];tgui_input_mode=1'><b>[(pref.tgui_input_mode) ? "Enabled" : "Disabled (default)"]</b></a><br>"
|
||||||
|
. += "<b>TGUI Large Buttons:</b> <a href='?src=\ref[src];tgui_large_buttons=1'><b>[(pref.tgui_large_buttons) ? "Enabled (default)" : "Disabled"]</b></a><br>"
|
||||||
|
. += "<b>TGUI Swapped Buttons:</b> <a href='?src=\ref[src];tgui_swapped_buttons=1'><b>[(pref.tgui_swapped_buttons) ? "Enabled" : "Disabled (default)"]</b></a><br>"
|
||||||
if(can_select_ooc_color(user))
|
if(can_select_ooc_color(user))
|
||||||
. += "<b>OOC Color:</b>"
|
. += "<b>OOC Color:</b>"
|
||||||
if(pref.ooccolor == initial(pref.ooccolor))
|
if(pref.ooccolor == initial(pref.ooccolor))
|
||||||
@@ -126,6 +134,14 @@
|
|||||||
pref.tgui_input_mode = !pref.tgui_input_mode
|
pref.tgui_input_mode = !pref.tgui_input_mode
|
||||||
return TOPIC_REFRESH
|
return TOPIC_REFRESH
|
||||||
|
|
||||||
|
else if(href_list["tgui_large_buttons"])
|
||||||
|
pref.tgui_large_buttons = !pref.tgui_large_buttons
|
||||||
|
return TOPIC_REFRESH
|
||||||
|
|
||||||
|
else if(href_list["tgui_swapped_buttons"])
|
||||||
|
pref.tgui_swapped_buttons = !pref.tgui_swapped_buttons
|
||||||
|
return TOPIC_REFRESH
|
||||||
|
|
||||||
else if(href_list["reset"])
|
else if(href_list["reset"])
|
||||||
switch(href_list["reset"])
|
switch(href_list["reset"])
|
||||||
if("ui")
|
if("ui")
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
return TOPIC_REFRESH
|
return TOPIC_REFRESH
|
||||||
|
|
||||||
else if(href_list["weight_gain"])
|
else if(href_list["weight_gain"])
|
||||||
var/weight_gain_rate = tgui_input_num(user, "Choose your character's rate of weight gain between 100% \
|
var/weight_gain_rate = tgui_input_number(user, "Choose your character's rate of weight gain between 100% \
|
||||||
(full realism body fat gain) and 0% (no body fat gain).\n\
|
(full realism body fat gain) and 0% (no body fat gain).\n\
|
||||||
(If you want to disable weight gain, set this to 0.01 to round it to 0%.)\
|
(If you want to disable weight gain, set this to 0.01 to round it to 0%.)\
|
||||||
([WEIGHT_CHANGE_MIN]-[WEIGHT_CHANGE_MAX])", "Character Preference", pref.weight_gain)
|
([WEIGHT_CHANGE_MIN]-[WEIGHT_CHANGE_MAX])", "Character Preference", pref.weight_gain)
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
return TOPIC_REFRESH
|
return TOPIC_REFRESH
|
||||||
|
|
||||||
else if(href_list["weight_loss"])
|
else if(href_list["weight_loss"])
|
||||||
var/weight_loss_rate = tgui_input_num(user, "Choose your character's rate of weight loss between 100% \
|
var/weight_loss_rate = tgui_input_number(user, "Choose your character's rate of weight loss between 100% \
|
||||||
(full realism body fat loss) and 0% (no body fat loss).\n\
|
(full realism body fat loss) and 0% (no body fat loss).\n\
|
||||||
(If you want to disable weight loss, set this to 0.01 round it to 0%.)\
|
(If you want to disable weight loss, set this to 0.01 round it to 0%.)\
|
||||||
([WEIGHT_CHANGE_MIN]-[WEIGHT_CHANGE_MAX])", "Character Preference", pref.weight_loss)
|
([WEIGHT_CHANGE_MIN]-[WEIGHT_CHANGE_MAX])", "Character Preference", pref.weight_loss)
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ var/list/preferences_datums = list()
|
|||||||
|
|
||||||
var/tgui_fancy = TRUE
|
var/tgui_fancy = TRUE
|
||||||
var/tgui_lock = FALSE
|
var/tgui_lock = FALSE
|
||||||
var/tgui_input_mode = FALSE // Say, Me, Whisper, Subtle Input Mode; Disabled by default; FALSE = BYOND, TRUE = TGUI
|
var/tgui_input_mode = FALSE // All the Input Boxes (Text,Number,List,Alert)
|
||||||
|
var/tgui_large_buttons = TRUE
|
||||||
|
var/tgui_swapped_buttons = FALSE
|
||||||
|
|
||||||
//character preferences
|
//character preferences
|
||||||
var/num_languages = 0 //CHOMPEdit
|
var/num_languages = 0 //CHOMPEdit
|
||||||
|
|||||||
@@ -40,11 +40,7 @@
|
|||||||
set hidden = 1
|
set hidden = 1
|
||||||
|
|
||||||
set_typing_indicator(TRUE)
|
set_typing_indicator(TRUE)
|
||||||
var/message
|
var/message = tgui_input_text(usr, "Type your message:", "Say")
|
||||||
if(usr.client.prefs.tgui_input_mode)
|
|
||||||
message = tgui_input_text(usr, "Type your message:", "Say")
|
|
||||||
else
|
|
||||||
message = input(usr, "Type your message:", "Say") as text
|
|
||||||
set_typing_indicator(FALSE)
|
set_typing_indicator(FALSE)
|
||||||
|
|
||||||
if(message)
|
if(message)
|
||||||
@@ -55,11 +51,7 @@
|
|||||||
set hidden = 1
|
set hidden = 1
|
||||||
|
|
||||||
set_typing_indicator(TRUE)
|
set_typing_indicator(TRUE)
|
||||||
var/message
|
var/message = tgui_input_text(usr, "Type your message:", "Emote", multiline = TRUE)
|
||||||
if(usr.client.prefs.tgui_input_mode)
|
|
||||||
message = tgui_input_message(usr, "Type your message:", "Emote")
|
|
||||||
else
|
|
||||||
message = input(usr, "Type your message:", "Emote") as message
|
|
||||||
set_typing_indicator(FALSE)
|
set_typing_indicator(FALSE)
|
||||||
|
|
||||||
if(message)
|
if(message)
|
||||||
@@ -70,11 +62,7 @@
|
|||||||
set name = ".Whisper"
|
set name = ".Whisper"
|
||||||
set hidden = 1
|
set hidden = 1
|
||||||
|
|
||||||
var/message
|
var/message = tgui_input_text(usr, "Type your message:", "Whisper")
|
||||||
if(usr.client.prefs.tgui_input_mode)
|
|
||||||
message = tgui_input_text(usr, "Type your message:", "Whisper")
|
|
||||||
else
|
|
||||||
message = input(usr, "Type your message:", "Whisper") as text
|
|
||||||
|
|
||||||
if(message)
|
if(message)
|
||||||
whisper(message)
|
whisper(message)
|
||||||
@@ -83,11 +71,7 @@
|
|||||||
set name = ".Subtle"
|
set name = ".Subtle"
|
||||||
set hidden = 1
|
set hidden = 1
|
||||||
|
|
||||||
var/message
|
var/message = tgui_input_text(usr, "Type your message:", "Subtle", multiline = TRUE)
|
||||||
if(usr.client.prefs.tgui_input_mode)
|
|
||||||
message = tgui_input_message(usr, "Type your message:", "Subtle")
|
|
||||||
else
|
|
||||||
message = input(usr, "Type your message:", "Subtle") as message
|
|
||||||
|
|
||||||
if(message)
|
if(message)
|
||||||
me_verb_subtle(message)
|
me_verb_subtle(message)
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
to_chat(usr, "<span class='info'>There isn't enough space left on \the [src] to write anything.</span>")
|
to_chat(usr, "<span class='info'>There isn't enough space left on \the [src] to write anything.</span>")
|
||||||
return
|
return
|
||||||
|
|
||||||
var/raw_t = tgui_input_message(usr, "Enter what you want to write:", "Write")
|
var/raw_t = tgui_input_text(usr, "Enter what you want to write:", "Write", multiline = TRUE)
|
||||||
if(!raw_t)
|
if(!raw_t)
|
||||||
return
|
return
|
||||||
var/t = sanitize(raw_t, free_space, extra = 0)
|
var/t = sanitize(raw_t, free_space, extra = 0)
|
||||||
|
|||||||
@@ -118,7 +118,7 @@
|
|||||||
|
|
||||||
if("add_player_info")
|
if("add_player_info")
|
||||||
var/key = params["ckey"]
|
var/key = params["ckey"]
|
||||||
var/add = tgui_input_message(usr, "Write your comment below.", "Add Player Info")
|
var/add = tgui_input_text(usr, "Write your comment below.", "Add Player Info", multiline = TRUE)
|
||||||
if(!add) return
|
if(!add) return
|
||||||
|
|
||||||
notes_add(key,add,usr)
|
notes_add(key,add,usr)
|
||||||
|
|||||||
@@ -32,8 +32,12 @@
|
|||||||
var/closing = FALSE
|
var/closing = FALSE
|
||||||
/// The status/visibility of the UI.
|
/// The status/visibility of the UI.
|
||||||
var/status = STATUS_INTERACTIVE
|
var/status = STATUS_INTERACTIVE
|
||||||
|
/// Timed refreshing state
|
||||||
|
var/refreshing = FALSE
|
||||||
/// Topic state used to determine status/interactability.
|
/// Topic state used to determine status/interactability.
|
||||||
var/datum/tgui_state/state = null
|
var/datum/tgui_state/state = null
|
||||||
|
/// Rate limit client refreshes to prevent DoS.
|
||||||
|
COOLDOWN_DECLARE(refresh_cooldown)
|
||||||
/// The map z-level to display.
|
/// The map z-level to display.
|
||||||
var/map_z_level = 1
|
var/map_z_level = 1
|
||||||
/// The Parent UI
|
/// The Parent UI
|
||||||
@@ -176,11 +180,17 @@
|
|||||||
/datum/tgui/proc/send_full_update(custom_data, force)
|
/datum/tgui/proc/send_full_update(custom_data, force)
|
||||||
if(!user.client || !initialized || closing)
|
if(!user.client || !initialized || closing)
|
||||||
return
|
return
|
||||||
|
if(!COOLDOWN_FINISHED(src, refresh_cooldown))
|
||||||
|
refreshing = TRUE
|
||||||
|
addtimer(CALLBACK(src, .proc/send_full_update), TGUI_REFRESH_FULL_UPDATE_COOLDOWN, TIMER_UNIQUE)
|
||||||
|
return
|
||||||
|
refreshing = FALSE
|
||||||
var/should_update_data = force || status >= STATUS_UPDATE
|
var/should_update_data = force || status >= STATUS_UPDATE
|
||||||
window.send_message("update", get_payload(
|
window.send_message("update", get_payload(
|
||||||
custom_data,
|
custom_data,
|
||||||
with_data = should_update_data,
|
with_data = should_update_data,
|
||||||
with_static_data = TRUE))
|
with_static_data = TRUE))
|
||||||
|
COOLDOWN_START(src, refresh_cooldown, TGUI_REFRESH_FULL_UPDATE_COOLDOWN)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* public
|
* public
|
||||||
@@ -211,6 +221,7 @@
|
|||||||
"title" = title,
|
"title" = title,
|
||||||
"status" = status,
|
"status" = status,
|
||||||
"interface" = interface,
|
"interface" = interface,
|
||||||
|
"refreshing" = refreshing,
|
||||||
"map" = (using_map && using_map.path) ? using_map.path : "Unknown",
|
"map" = (using_map && using_map.path) ? using_map.path : "Unknown",
|
||||||
"mapZLevel" = map_z_level,
|
"mapZLevel" = map_z_level,
|
||||||
"window" = list(
|
"window" = list(
|
||||||
@@ -312,6 +323,9 @@
|
|||||||
return FALSE
|
return FALSE
|
||||||
switch(type)
|
switch(type)
|
||||||
if("ready")
|
if("ready")
|
||||||
|
// Send a full update when the user manually refreshes the UI
|
||||||
|
if(initialized)
|
||||||
|
send_full_update()
|
||||||
initialized = TRUE
|
initialized = TRUE
|
||||||
if("pingReply")
|
if("pingReply")
|
||||||
initialized = TRUE
|
initialized = TRUE
|
||||||
|
|||||||
@@ -1,299 +0,0 @@
|
|||||||
/**
|
|
||||||
* Creates a TGUI input text window and returns the user's response.
|
|
||||||
*
|
|
||||||
* This proc should be used to create alerts that the caller will wait for a response from.
|
|
||||||
* Arguments:
|
|
||||||
* * user - The user to show the input box to.
|
|
||||||
* * message - The content of the input box, shown in the body of the TGUI window.
|
|
||||||
* * title - The title of the input box, shown on the top of the TGUI window.
|
|
||||||
* * default - The default value pre-populated in the input box.
|
|
||||||
* * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout.
|
|
||||||
*/
|
|
||||||
/proc/tgui_input_text(mob/user, message, title, default, timeout = 0)
|
|
||||||
if (istext(user))
|
|
||||||
stack_trace("tgui_input_text() received text for user instead of mob")
|
|
||||||
return
|
|
||||||
if (!user)
|
|
||||||
user = usr
|
|
||||||
if (!istype(user))
|
|
||||||
if (istype(user, /client))
|
|
||||||
var/client/client = user
|
|
||||||
user = client.mob
|
|
||||||
else
|
|
||||||
return
|
|
||||||
var/datum/tgui_input_dialog/input = new(user, message, title, default, timeout)
|
|
||||||
input.input_type = "text"
|
|
||||||
input.tgui_interact(user)
|
|
||||||
input.wait()
|
|
||||||
if (input)
|
|
||||||
. = input.choice
|
|
||||||
qdel(input)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a TGUI input message window and returns the user's response.
|
|
||||||
*
|
|
||||||
* This proc should be used to create alerts that the caller will wait for a response from.
|
|
||||||
* Arguments:
|
|
||||||
* * user - The user to show the input box to.
|
|
||||||
* * message - The content of the input box, shown in the body of the TGUI window.
|
|
||||||
* * title - The title of the input box, shown on the top of the TGUI window.
|
|
||||||
* * default - The default value pre-populated in the input box.
|
|
||||||
* * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout.
|
|
||||||
*/
|
|
||||||
/proc/tgui_input_message(mob/user, message, title, default, timeout = 0)
|
|
||||||
if (istext(user))
|
|
||||||
stack_trace("tgui_input_message() received text for user instead of mob")
|
|
||||||
return
|
|
||||||
if (!user)
|
|
||||||
user = usr
|
|
||||||
if (!istype(user))
|
|
||||||
if (istype(user, /client))
|
|
||||||
var/client/client = user
|
|
||||||
user = client.mob
|
|
||||||
else
|
|
||||||
return
|
|
||||||
var/datum/tgui_input_dialog/input = new(user, message, title, default, timeout)
|
|
||||||
input.input_type = "message"
|
|
||||||
input.tgui_interact(user)
|
|
||||||
input.wait()
|
|
||||||
if (input)
|
|
||||||
. = input.choice
|
|
||||||
qdel(input)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a TGUI input num window and returns the user's response.
|
|
||||||
*
|
|
||||||
* This proc should be used to create alerts that the caller will wait for a response from.
|
|
||||||
* Arguments:
|
|
||||||
* * user - The user to show the input box to.
|
|
||||||
* * message - The content of the input box, shown in the body of the TGUI window.
|
|
||||||
* * title - The title of the input box, shown on the top of the TGUI window.
|
|
||||||
* * default - The default value pre-populated in the input box.
|
|
||||||
* * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout.
|
|
||||||
*/
|
|
||||||
/proc/tgui_input_num(mob/user, message, title, default, timeout = 0)
|
|
||||||
if (istext(user))
|
|
||||||
stack_trace("tgui_input_num() received text for user instead of mob")
|
|
||||||
return
|
|
||||||
if (!user)
|
|
||||||
user = usr
|
|
||||||
if (!istype(user))
|
|
||||||
if (istype(user, /client))
|
|
||||||
var/client/client = user
|
|
||||||
user = client.mob
|
|
||||||
else
|
|
||||||
return
|
|
||||||
var/datum/tgui_input_dialog/input = new(user, message, title, default, timeout)
|
|
||||||
input.input_type = "num"
|
|
||||||
input.tgui_interact(user)
|
|
||||||
input.wait()
|
|
||||||
if (input)
|
|
||||||
. = input.choice
|
|
||||||
qdel(input)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an asynchronous TGUI input text window with an associated callback.
|
|
||||||
*
|
|
||||||
* This proc should be used to create inputs that invoke a callback with the user's chosen option.
|
|
||||||
* Arguments:
|
|
||||||
* * user - The user to show the input box to.
|
|
||||||
* * message - The content of the input box, shown in the body of the TGUI window.
|
|
||||||
* * title - The title of the input box, shown on the top of the TGUI window.
|
|
||||||
* * default - The default value pre-populated in the input box.
|
|
||||||
* * callback - The callback to be invoked when a choice is made.
|
|
||||||
* * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
|
|
||||||
*/
|
|
||||||
/proc/tgui_input_text_async(mob/user, message, title, default, datum/callback/callback, timeout = 60 SECONDS)
|
|
||||||
if (istext(user))
|
|
||||||
stack_trace("tgui_input_text_async() received text for user instead of mob")
|
|
||||||
return
|
|
||||||
if (!user)
|
|
||||||
user = usr
|
|
||||||
if (!istype(user))
|
|
||||||
if (istype(user, /client))
|
|
||||||
var/client/client = user
|
|
||||||
user = client.mob
|
|
||||||
else
|
|
||||||
return
|
|
||||||
var/datum/tgui_input_dialog/async/input = new(user, message, title, default, callback, timeout)
|
|
||||||
input.input_type = "text"
|
|
||||||
input.tgui_interact(user)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an asynchronous TGUI input message window with an associated callback.
|
|
||||||
*
|
|
||||||
* This proc should be used to create inputs that invoke a callback with the user's chosen option.
|
|
||||||
* Arguments:
|
|
||||||
* * user - The user to show the input box to.
|
|
||||||
* * message - The content of the input box, shown in the body of the TGUI window.
|
|
||||||
* * title - The title of the input box, shown on the top of the TGUI window.
|
|
||||||
* * default - The default value pre-populated in the input box.
|
|
||||||
* * callback - The callback to be invoked when a choice is made.
|
|
||||||
* * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
|
|
||||||
*/
|
|
||||||
/proc/tgui_input_message_async(mob/user, message, title, default, datum/callback/callback, timeout = 60 SECONDS)
|
|
||||||
if (istext(user))
|
|
||||||
stack_trace("tgui_input_message_async() received text for user instead of mob")
|
|
||||||
return
|
|
||||||
if (!user)
|
|
||||||
user = usr
|
|
||||||
if (!istype(user))
|
|
||||||
if (istype(user, /client))
|
|
||||||
var/client/client = user
|
|
||||||
user = client.mob
|
|
||||||
else
|
|
||||||
return
|
|
||||||
var/datum/tgui_input_dialog/async/input = new(user, message, title, default, callback, timeout)
|
|
||||||
input.input_type = "message"
|
|
||||||
input.tgui_interact(user)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an asynchronous TGUI input num window with an associated callback.
|
|
||||||
*
|
|
||||||
* This proc should be used to create inputs that invoke a callback with the user's chosen option.
|
|
||||||
* Arguments:
|
|
||||||
* * user - The user to show the input box to.
|
|
||||||
* * message - The content of the input box, shown in the body of the TGUI window.
|
|
||||||
* * title - The title of the input box, shown on the top of the TGUI window.
|
|
||||||
* * default - The default value pre-populated in the input box.
|
|
||||||
* * callback - The callback to be invoked when a choice is made.
|
|
||||||
* * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
|
|
||||||
*/
|
|
||||||
/proc/tgui_input_num_async(mob/user, message, title, default, datum/callback/callback, timeout = 60 SECONDS)
|
|
||||||
if (istext(user))
|
|
||||||
stack_trace("tgui_input_num_async() received text for user instead of mob")
|
|
||||||
return
|
|
||||||
if (!user)
|
|
||||||
user = usr
|
|
||||||
if (!istype(user))
|
|
||||||
if (istype(user, /client))
|
|
||||||
var/client/client = user
|
|
||||||
user = client.mob
|
|
||||||
else
|
|
||||||
return
|
|
||||||
var/datum/tgui_input_dialog/async/input = new(user, message, title, default, callback, timeout)
|
|
||||||
input.input_type = "num"
|
|
||||||
input.tgui_interact(user)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* # tgui_input_dialog
|
|
||||||
*
|
|
||||||
* Datum used for instantiating and using a TGUI-controlled input that prompts the user with
|
|
||||||
* a message and a box for accepting text/message/num input.
|
|
||||||
*/
|
|
||||||
/datum/tgui_input_dialog
|
|
||||||
/// The title of the TGUI window
|
|
||||||
var/title
|
|
||||||
/// The textual body of the TGUI window
|
|
||||||
var/message
|
|
||||||
/// The default value to initially populate the input box.
|
|
||||||
var/initial
|
|
||||||
/// The value that the user input into the input box, null if cancelled.
|
|
||||||
var/choice
|
|
||||||
/// The time at which the tgui_text_input was created, for displaying timeout progress.
|
|
||||||
var/start_time
|
|
||||||
/// The lifespan of the tgui_text_input, after which the window will close and delete itself.
|
|
||||||
var/timeout
|
|
||||||
/// Boolean field describing if the tgui_text_input was closed by the user.
|
|
||||||
var/closed
|
|
||||||
/// Indicates the data type we want to collect ("text", "message", "num")
|
|
||||||
var/input_type = "text"
|
|
||||||
|
|
||||||
/datum/tgui_input_dialog/New(mob/user, message, title, default, timeout)
|
|
||||||
src.title = title
|
|
||||||
src.message = message
|
|
||||||
// TODO - Do we need to sanitize the initial value for illegal characters?
|
|
||||||
src.initial = default
|
|
||||||
if (timeout)
|
|
||||||
src.timeout = timeout
|
|
||||||
start_time = world.time
|
|
||||||
QDEL_IN(src, timeout)
|
|
||||||
|
|
||||||
/datum/tgui_input_dialog/Destroy(force, ...)
|
|
||||||
SStgui.close_uis(src)
|
|
||||||
. = ..()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Waits for a user's response to the tgui_text_input's prompt before returning. Returns early if
|
|
||||||
* the window was closed by the user.
|
|
||||||
*/
|
|
||||||
/datum/tgui_input_dialog/proc/wait()
|
|
||||||
while (!choice && !closed)
|
|
||||||
stoplag(1)
|
|
||||||
|
|
||||||
/datum/tgui_input_dialog/tgui_interact(mob/user, datum/tgui/ui)
|
|
||||||
ui = SStgui.try_update_ui(user, src, ui)
|
|
||||||
if(!ui)
|
|
||||||
ui = new(user, src, "InputModal")
|
|
||||||
ui.open()
|
|
||||||
|
|
||||||
/datum/tgui_input_dialog/tgui_close(mob/user)
|
|
||||||
. = ..()
|
|
||||||
closed = TRUE
|
|
||||||
|
|
||||||
/datum/tgui_input_dialog/tgui_state(mob/user)
|
|
||||||
return GLOB.tgui_always_state
|
|
||||||
|
|
||||||
/datum/tgui_input_dialog/tgui_static_data(mob/user)
|
|
||||||
. = list(
|
|
||||||
"title" = title,
|
|
||||||
"message" = message,
|
|
||||||
"initial" = initial,
|
|
||||||
"input_type" = input_type
|
|
||||||
)
|
|
||||||
|
|
||||||
/datum/tgui_input_dialog/tgui_data(mob/user)
|
|
||||||
. = list()
|
|
||||||
if(timeout)
|
|
||||||
.["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1)
|
|
||||||
|
|
||||||
/datum/tgui_input_dialog/tgui_act(action, list/params)
|
|
||||||
. = ..()
|
|
||||||
if (.)
|
|
||||||
return
|
|
||||||
switch(action)
|
|
||||||
if("choose")
|
|
||||||
set_choice(params["choice"])
|
|
||||||
if(isnull(src.choice))
|
|
||||||
return
|
|
||||||
SStgui.close_uis(src)
|
|
||||||
return TRUE
|
|
||||||
if("cancel")
|
|
||||||
SStgui.close_uis(src)
|
|
||||||
closed = TRUE
|
|
||||||
return TRUE
|
|
||||||
|
|
||||||
/datum/tgui_input_dialog/proc/set_choice(choice)
|
|
||||||
if(input_type == "num")
|
|
||||||
src.choice = text2num(choice)
|
|
||||||
return
|
|
||||||
src.choice = choice
|
|
||||||
|
|
||||||
/**
|
|
||||||
* # async tgui_text_input
|
|
||||||
*
|
|
||||||
* An asynchronous version of tgui_text_input to be used with callbacks instead of waiting on user responses.
|
|
||||||
*/
|
|
||||||
/datum/tgui_input_dialog/async
|
|
||||||
/// The callback to be invoked by the tgui_text_input upon having a choice made.
|
|
||||||
var/datum/callback/callback
|
|
||||||
|
|
||||||
/datum/tgui_input_dialog/async/New(mob/user, message, title, default, callback, timeout)
|
|
||||||
..(user, title, message, default, timeout)
|
|
||||||
src.callback = callback
|
|
||||||
|
|
||||||
/datum/tgui_input_dialog/async/Destroy(force, ...)
|
|
||||||
QDEL_NULL(callback)
|
|
||||||
. = ..()
|
|
||||||
|
|
||||||
/datum/tgui_input_dialog/async/tgui_close(mob/user)
|
|
||||||
. = ..()
|
|
||||||
qdel(src)
|
|
||||||
|
|
||||||
/datum/tgui_input_dialog/async/set_choice(choice)
|
|
||||||
. = ..()
|
|
||||||
if(!isnull(src.choice))
|
|
||||||
callback?.InvokeAsync(src.choice)
|
|
||||||
|
|
||||||
/datum/tgui_input_dialog/async/wait()
|
|
||||||
return
|
|
||||||
@@ -8,8 +8,9 @@
|
|||||||
* * title - The of the alert modal, shown on the top 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.
|
* * 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.
|
* * timeout - The timeout of the alert, after which the modal will close and qdel itself. Set to zero for no timeout.
|
||||||
|
* * autofocus - The bool that controls if this alert should grab window focus.
|
||||||
*/
|
*/
|
||||||
/proc/tgui_alert(mob/user, message = null, title = null, list/buttons = list("Ok"), timeout = 0)
|
/proc/tgui_alert(mob/user, message = "", title, list/buttons = list("Ok"), timeout = 0, autofocus = TRUE)
|
||||||
if (istext(buttons))
|
if (istext(buttons))
|
||||||
stack_trace("tgui_alert() received text for buttons instead of list")
|
stack_trace("tgui_alert() received text for buttons instead of list")
|
||||||
return
|
return
|
||||||
@@ -24,7 +25,19 @@
|
|||||||
user = client.mob
|
user = client.mob
|
||||||
else
|
else
|
||||||
return
|
return
|
||||||
var/datum/tgui_alert/alert = new(user, message, title, buttons, timeout)
|
// A gentle nudge - you should not be using TGUI alert for anything other than a simple message.
|
||||||
|
if(length(buttons) > 3)
|
||||||
|
log_tgui(user, "Error: TGUI Alert initiated with too many buttons. Use a list.", "TguiAlert")
|
||||||
|
return tgui_input_list(user, message, title, buttons, timeout, autofocus)
|
||||||
|
|
||||||
|
// Client does NOT have tgui_input on: Returns regular input
|
||||||
|
if(!usr.client.prefs.tgui_input_mode)
|
||||||
|
if(length(buttons) == 2)
|
||||||
|
return alert(user, message, title, buttons[1], buttons[2])
|
||||||
|
if(length(buttons) == 3)
|
||||||
|
return alert(user, message, title, buttons[1], buttons[2], buttons[3])
|
||||||
|
|
||||||
|
var/datum/tgui_alert/alert = new(user, message, title, buttons, timeout, autofocus)
|
||||||
alert.tgui_interact(user)
|
alert.tgui_interact(user)
|
||||||
alert.wait()
|
alert.wait()
|
||||||
if (alert)
|
if (alert)
|
||||||
@@ -32,37 +45,7 @@
|
|||||||
qdel(alert)
|
qdel(alert)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an asynchronous TGUI alert window with an associated callback.
|
* # tgui_alert
|
||||||
*
|
|
||||||
* This proc should be used to create alerts that invoke a callback with the user's chosen option.
|
|
||||||
* Arguments:
|
|
||||||
* * user - The user to show the alert to.
|
|
||||||
* * message - The content of the alert, shown in the body of the TGUI window.
|
|
||||||
* * title - The of the alert modal, shown on the top of the TGUI window.
|
|
||||||
* * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
|
|
||||||
* * callback - The callback to be invoked when a choice is made.
|
|
||||||
* * timeout - The timeout of the alert, after which the modal will close and qdel itself. Disabled by default, can be set to seconds otherwise.
|
|
||||||
*/
|
|
||||||
/proc/tgui_alert_async(mob/user, message = null, title = null, list/buttons = list("Ok"), datum/callback/callback, timeout = 0)
|
|
||||||
if (istext(buttons))
|
|
||||||
stack_trace("tgui_alert() received text for buttons instead of list")
|
|
||||||
return
|
|
||||||
if (istext(user))
|
|
||||||
stack_trace("tgui_alert() received text for user instead of list")
|
|
||||||
return
|
|
||||||
if (!user)
|
|
||||||
user = usr
|
|
||||||
if (!istype(user))
|
|
||||||
if (istype(user, /client))
|
|
||||||
var/client/client = user
|
|
||||||
user = client.mob
|
|
||||||
else
|
|
||||||
return
|
|
||||||
var/datum/tgui_alert/async/alert = new(user, message, title, buttons, callback, timeout)
|
|
||||||
alert.tgui_interact(user)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* # tgui_modal
|
|
||||||
*
|
*
|
||||||
* Datum used for instantiating and using a TGUI-controlled modal that prompts the user with
|
* Datum used for instantiating and using a TGUI-controlled modal that prompts the user with
|
||||||
* a message and has buttons for responses.
|
* a message and has buttons for responses.
|
||||||
@@ -80,13 +63,16 @@
|
|||||||
var/start_time
|
var/start_time
|
||||||
/// The lifespan of the tgui_modal, after which the window will close and delete itself.
|
/// The lifespan of the tgui_modal, after which the window will close and delete itself.
|
||||||
var/timeout
|
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.
|
/// Boolean field describing if the tgui_modal was closed by the user.
|
||||||
var/closed
|
var/closed
|
||||||
|
|
||||||
/datum/tgui_alert/New(mob/user, message, title, list/buttons, timeout)
|
/datum/tgui_alert/New(mob/user, message, title, list/buttons, timeout, autofocus)
|
||||||
src.title = title
|
src.autofocus = autofocus
|
||||||
src.message = message
|
|
||||||
src.buttons = buttons.Copy()
|
src.buttons = buttons.Copy()
|
||||||
|
src.message = message
|
||||||
|
src.title = title
|
||||||
if (timeout)
|
if (timeout)
|
||||||
src.timeout = timeout
|
src.timeout = timeout
|
||||||
start_time = world.time
|
start_time = world.time
|
||||||
@@ -118,15 +104,21 @@
|
|||||||
/datum/tgui_alert/tgui_state(mob/user)
|
/datum/tgui_alert/tgui_state(mob/user)
|
||||||
return GLOB.tgui_always_state
|
return GLOB.tgui_always_state
|
||||||
|
|
||||||
/datum/tgui_alert/tgui_data(mob/user)
|
/datum/tgui_alert/tgui_static_data(mob/user)
|
||||||
. = list(
|
var/list/data = list()
|
||||||
"title" = title,
|
data["autofocus"] = autofocus
|
||||||
"message" = message,
|
data["buttons"] = buttons
|
||||||
"buttons" = buttons
|
data["message"] = message
|
||||||
)
|
data["large_buttons"] = usr.client.prefs.tgui_large_buttons
|
||||||
|
data["swapped_buttons"] = !usr.client.prefs.tgui_swapped_buttons
|
||||||
|
data["title"] = title
|
||||||
|
return data
|
||||||
|
|
||||||
|
/datum/tgui_alert/tgui_data(mob/user)
|
||||||
|
var/list/data = list()
|
||||||
if(timeout)
|
if(timeout)
|
||||||
.["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
|
.["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
|
||||||
|
return data
|
||||||
|
|
||||||
/datum/tgui_alert/tgui_act(action, list/params)
|
/datum/tgui_alert/tgui_act(action, list/params)
|
||||||
. = ..()
|
. = ..()
|
||||||
@@ -139,10 +131,44 @@
|
|||||||
set_choice(params["choice"])
|
set_choice(params["choice"])
|
||||||
SStgui.close_uis(src)
|
SStgui.close_uis(src)
|
||||||
return TRUE
|
return TRUE
|
||||||
|
if("cancel")
|
||||||
|
closed = TRUE
|
||||||
|
SStgui.close_uis(src)
|
||||||
|
return TRUE
|
||||||
|
|
||||||
/datum/tgui_alert/proc/set_choice(choice)
|
/datum/tgui_alert/proc/set_choice(choice)
|
||||||
src.choice = choice
|
src.choice = choice
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an asynchronous TGUI alert window with an associated callback.
|
||||||
|
*
|
||||||
|
* This proc should be used to create alerts that invoke a callback with the user's chosen option.
|
||||||
|
* Arguments:
|
||||||
|
* * user - The user to show the alert to.
|
||||||
|
* * message - The content of the alert, shown in the body of the TGUI window.
|
||||||
|
* * title - The of the alert modal, shown on the top of the TGUI window.
|
||||||
|
* * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
|
||||||
|
* * callback - The callback to be invoked when a choice is made.
|
||||||
|
* * timeout - The timeout of the alert, after which the modal will close and qdel itself. Disabled by default, can be set to seconds otherwise.
|
||||||
|
*/
|
||||||
|
/proc/tgui_alert_async(mob/user, message = "", title, list/buttons = list("Ok"), datum/callback/callback, timeout = 0, autofocus = TRUE)
|
||||||
|
if (istext(buttons))
|
||||||
|
stack_trace("tgui_alert() received text for buttons instead of list")
|
||||||
|
return
|
||||||
|
if (istext(user))
|
||||||
|
stack_trace("tgui_alert() received text for user instead of list")
|
||||||
|
return
|
||||||
|
if (!user)
|
||||||
|
user = usr
|
||||||
|
if (!istype(user))
|
||||||
|
if (istype(user, /client))
|
||||||
|
var/client/client = user
|
||||||
|
user = client.mob
|
||||||
|
else
|
||||||
|
return
|
||||||
|
var/datum/tgui_alert/async/alert = new(user, message, title, buttons, callback, timeout, autofocus)
|
||||||
|
alert.tgui_interact(user)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* # async tgui_modal
|
* # async tgui_modal
|
||||||
*
|
*
|
||||||
@@ -152,8 +178,8 @@
|
|||||||
/// The callback to be invoked by the tgui_modal upon having a choice made.
|
/// The callback to be invoked by the tgui_modal upon having a choice made.
|
||||||
var/datum/callback/callback
|
var/datum/callback/callback
|
||||||
|
|
||||||
/datum/tgui_alert/async/New(mob/user, message, title, list/buttons, callback, timeout)
|
/datum/tgui_alert/async/New(mob/user, message, title, list/buttons, callback, timeout, autofocus)
|
||||||
..(user, message, title, buttons, timeout)
|
..(user, message, title, buttons, timeout, autofocus)
|
||||||
src.callback = callback
|
src.callback = callback
|
||||||
|
|
||||||
/datum/tgui_alert/async/Destroy(force, ...)
|
/datum/tgui_alert/async/Destroy(force, ...)
|
||||||
@@ -6,17 +6,17 @@
|
|||||||
* * user - The user to show the input box to.
|
* * user - The user to show the input box to.
|
||||||
* * message - The content of the input box, shown in the body of the TGUI window.
|
* * 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.
|
* * title - The title of the input box, shown on the top of the TGUI window.
|
||||||
* * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
|
* * items - The options that can be chosen by the user, each string is assigned a button on the UI.
|
||||||
* * default - The option with this value will be selected on first paint of the TGUI window.
|
* * default - The option with this value will be selected on first paint of the TGUI window.
|
||||||
* * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout.
|
* * timeout - The timeout of the input box, after which the input box will close and qdel itself. Set to zero for no timeout.
|
||||||
*/
|
*/
|
||||||
/proc/tgui_input_list(mob/user, message, title, list/buttons, default, timeout = 0)
|
/proc/tgui_input_list(mob/user, message, title = "Select", list/items, default, timeout = 0)
|
||||||
if (istext(user))
|
if (istext(user))
|
||||||
stack_trace("tgui_alert() received text for user instead of mob")
|
stack_trace("tgui_alert() received text for user instead of mob")
|
||||||
return
|
return
|
||||||
if (!user)
|
if (!user)
|
||||||
user = usr
|
user = usr
|
||||||
if(!length(buttons))
|
if(!length(items))
|
||||||
return
|
return
|
||||||
if (!istype(user))
|
if (!istype(user))
|
||||||
if (istype(user, /client))
|
if (istype(user, /client))
|
||||||
@@ -24,43 +24,16 @@
|
|||||||
user = client.mob
|
user = client.mob
|
||||||
else
|
else
|
||||||
return
|
return
|
||||||
var/datum/tgui_list_input/input = new(user, message, title, buttons, default, timeout)
|
/// Client does NOT have tgui_input on: Returns regular input
|
||||||
|
if(!usr.client.prefs.tgui_input_mode)
|
||||||
|
return input(user, message, title, default) as null|anything in items
|
||||||
|
var/datum/tgui_list_input/input = new(user, message, title, items, default, timeout)
|
||||||
input.tgui_interact(user)
|
input.tgui_interact(user)
|
||||||
input.wait()
|
input.wait()
|
||||||
if (input)
|
if (input)
|
||||||
. = input.choice
|
. = input.choice
|
||||||
qdel(input)
|
qdel(input)
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates an asynchronous TGUI input list window with an associated callback.
|
|
||||||
*
|
|
||||||
* This proc should be used to create inputs that invoke a callback with the user's chosen option.
|
|
||||||
* Arguments:
|
|
||||||
* * user - The user to show the input box to.
|
|
||||||
* * message - The content of the input box, shown in the body of the TGUI window.
|
|
||||||
* * title - The title of the input box, shown on the top of the TGUI window.
|
|
||||||
* * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
|
|
||||||
* * default - The option with this value will be selected on first paint of the TGUI window.
|
|
||||||
* * callback - The callback to be invoked when a choice is made.
|
|
||||||
* * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
|
|
||||||
*/
|
|
||||||
/proc/tgui_input_list_async(mob/user, message, title, list/buttons, default, datum/callback/callback, timeout = 60 SECONDS)
|
|
||||||
if (istext(user))
|
|
||||||
stack_trace("tgui_alert() received text for user instead of mob")
|
|
||||||
return
|
|
||||||
if (!user)
|
|
||||||
user = usr
|
|
||||||
if(!length(buttons))
|
|
||||||
return
|
|
||||||
if (!istype(user))
|
|
||||||
if (istype(user, /client))
|
|
||||||
var/client/client = user
|
|
||||||
user = client.mob
|
|
||||||
else
|
|
||||||
return
|
|
||||||
var/datum/tgui_list_input/async/input = new(user, message, title, buttons, default, callback, timeout)
|
|
||||||
input.tgui_interact(user)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* # tgui_list_input
|
* # tgui_list_input
|
||||||
*
|
*
|
||||||
@@ -72,14 +45,14 @@
|
|||||||
var/title
|
var/title
|
||||||
/// The textual body of the TGUI window
|
/// The textual body of the TGUI window
|
||||||
var/message
|
var/message
|
||||||
/// The list of buttons (responses) provided on the TGUI window
|
/// The list of items (responses) provided on the TGUI window
|
||||||
var/list/buttons
|
var/list/items
|
||||||
/// Buttons (strings specifically) mapped to the actual value (e.g. a mob or a verb)
|
/// Items (strings specifically) mapped to the actual value (e.g. a mob or a verb)
|
||||||
var/list/buttons_map
|
var/list/items_map
|
||||||
/// Value of the button that should be pre-selected on first paint.
|
|
||||||
var/initial
|
|
||||||
/// The button that the user has pressed, null if no selection has been made
|
/// The button that the user has pressed, null if no selection has been made
|
||||||
var/choice
|
var/choice
|
||||||
|
/// The default item to be selected
|
||||||
|
var/default
|
||||||
/// The time at which the tgui_list_input was created, for displaying timeout progress.
|
/// The time at which the tgui_list_input was created, for displaying timeout progress.
|
||||||
var/start_time
|
var/start_time
|
||||||
/// The lifespan of the tgui_list_input, after which the window will close and delete itself.
|
/// The lifespan of the tgui_list_input, after which the window will close and delete itself.
|
||||||
@@ -87,29 +60,29 @@
|
|||||||
/// Boolean field describing if the tgui_list_input was closed by the user.
|
/// Boolean field describing if the tgui_list_input was closed by the user.
|
||||||
var/closed
|
var/closed
|
||||||
|
|
||||||
/datum/tgui_list_input/New(mob/user, message, title, list/buttons, default, timeout)
|
/datum/tgui_list_input/New(mob/user, message, title, list/items, default, timeout)
|
||||||
src.title = title
|
src.title = title
|
||||||
src.message = message
|
src.message = message
|
||||||
src.buttons = list()
|
src.items = list()
|
||||||
src.buttons_map = list()
|
src.items_map = list()
|
||||||
src.initial = default
|
src.default = default
|
||||||
var/list/repeat_buttons = list()
|
var/list/repeat_items = list()
|
||||||
|
|
||||||
// Gets rid of illegal characters
|
// Gets rid of illegal characters
|
||||||
var/static/regex/whitelistedWords = regex(@{"([^\u0020-\u8000]+)"})
|
var/static/regex/whitelistedWords = regex(@{"([^\u0020-\u8000]+)"})
|
||||||
|
|
||||||
for(var/i in buttons)
|
for(var/i in items)
|
||||||
if(isnull(i))
|
if(isnull(i))
|
||||||
stack_trace("Null in a tgui_input_list() buttons")
|
stack_trace("Null in a tgui_input_list() items")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
var/string_key = whitelistedWords.Replace("[i]", "")
|
var/string_key = whitelistedWords.Replace("[i]", "")
|
||||||
|
|
||||||
//avoids duplicated keys E.g: when areas have the same name
|
//avoids duplicated keys E.g: when areas have the same name
|
||||||
string_key = avoid_assoc_duplicate_keys(string_key, repeat_buttons)
|
string_key = avoid_assoc_duplicate_keys(string_key, repeat_items)
|
||||||
|
|
||||||
src.buttons += string_key
|
src.items += string_key
|
||||||
src.buttons_map[string_key] = i
|
src.items_map[string_key] = i
|
||||||
|
|
||||||
|
|
||||||
if (timeout)
|
if (timeout)
|
||||||
@@ -119,7 +92,7 @@
|
|||||||
|
|
||||||
/datum/tgui_list_input/Destroy(force, ...)
|
/datum/tgui_list_input/Destroy(force, ...)
|
||||||
SStgui.close_uis(src)
|
SStgui.close_uis(src)
|
||||||
QDEL_NULL(buttons)
|
QDEL_NULL(items)
|
||||||
. = ..()
|
. = ..()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -133,7 +106,7 @@
|
|||||||
/datum/tgui_list_input/tgui_interact(mob/user, datum/tgui/ui)
|
/datum/tgui_list_input/tgui_interact(mob/user, datum/tgui/ui)
|
||||||
ui = SStgui.try_update_ui(user, src, ui)
|
ui = SStgui.try_update_ui(user, src, ui)
|
||||||
if(!ui)
|
if(!ui)
|
||||||
ui = new(user, src, "ListInput")
|
ui = new(user, src, "ListInputModal")
|
||||||
ui.open()
|
ui.open()
|
||||||
|
|
||||||
/datum/tgui_list_input/tgui_close(mob/user)
|
/datum/tgui_list_input/tgui_close(mob/user)
|
||||||
@@ -144,27 +117,31 @@
|
|||||||
return GLOB.tgui_always_state
|
return GLOB.tgui_always_state
|
||||||
|
|
||||||
/datum/tgui_list_input/tgui_static_data(mob/user)
|
/datum/tgui_list_input/tgui_static_data(mob/user)
|
||||||
. = list(
|
var/list/data = list()
|
||||||
"title" = title,
|
data["init_value"] = default || items[1]
|
||||||
"message" = message,
|
data["items"] = items
|
||||||
"buttons" = buttons,
|
data["large_buttons"] = usr.client.prefs.tgui_large_buttons
|
||||||
"initial" = initial
|
data["message"] = message
|
||||||
)
|
data["swapped_buttons"] = !usr.client.prefs.tgui_swapped_buttons
|
||||||
|
data["title"] = title
|
||||||
|
return data
|
||||||
|
|
||||||
/datum/tgui_list_input/tgui_data(mob/user)
|
/datum/tgui_list_input/tgui_data(mob/user)
|
||||||
. = list()
|
var/list/data = list()
|
||||||
if(timeout)
|
if(timeout)
|
||||||
.["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1)
|
.["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1)
|
||||||
|
return data
|
||||||
|
|
||||||
/datum/tgui_list_input/tgui_act(action, list/params)
|
/datum/tgui_list_input/tgui_act(action, list/params)
|
||||||
. = ..()
|
. = ..()
|
||||||
if (.)
|
if (.)
|
||||||
return
|
return
|
||||||
switch(action)
|
switch(action)
|
||||||
if("choose")
|
if("submit")
|
||||||
if (!(params["choice"] in buttons))
|
if (!(params["entry"] in items))
|
||||||
return
|
return
|
||||||
set_choice(buttons_map[params["choice"]])
|
set_choice(items_map[params["entry"]])
|
||||||
|
closed = TRUE
|
||||||
SStgui.close_uis(src)
|
SStgui.close_uis(src)
|
||||||
return TRUE
|
return TRUE
|
||||||
if("cancel")
|
if("cancel")
|
||||||
@@ -175,6 +152,36 @@
|
|||||||
/datum/tgui_list_input/proc/set_choice(choice)
|
/datum/tgui_list_input/proc/set_choice(choice)
|
||||||
src.choice = choice
|
src.choice = choice
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an asynchronous TGUI input list window with an associated callback.
|
||||||
|
*
|
||||||
|
* This proc should be used to create inputs that invoke a callback with the user's chosen option.
|
||||||
|
* Arguments:
|
||||||
|
* * user - The user to show the input box to.
|
||||||
|
* * message - The content of the input box, shown in the body of the TGUI window.
|
||||||
|
* * title - The title of the input box, shown on the top of the TGUI window.
|
||||||
|
* * items - The options that can be chosen by the user, each string is assigned a button on the UI.
|
||||||
|
* * default - The option with this value will be selected on first paint of the TGUI window.
|
||||||
|
* * callback - The callback to be invoked when a choice is made.
|
||||||
|
* * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
|
||||||
|
*/
|
||||||
|
/proc/tgui_input_list_async(mob/user, message, title, list/items, default, datum/callback/callback, timeout = 60 SECONDS)
|
||||||
|
if (istext(user))
|
||||||
|
stack_trace("tgui_alert() received text for user instead of mob")
|
||||||
|
return
|
||||||
|
if (!user)
|
||||||
|
user = usr
|
||||||
|
if(!length(items))
|
||||||
|
return
|
||||||
|
if (!istype(user))
|
||||||
|
if (istype(user, /client))
|
||||||
|
var/client/client = user
|
||||||
|
user = client.mob
|
||||||
|
else
|
||||||
|
return
|
||||||
|
var/datum/tgui_list_input/async/input = new(user, message, title, items, default, callback, timeout)
|
||||||
|
input.tgui_interact(user)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* # async tgui_list_input
|
* # async tgui_list_input
|
||||||
*
|
*
|
||||||
@@ -184,8 +191,8 @@
|
|||||||
/// The callback to be invoked by the tgui_list_input upon having a choice made.
|
/// The callback to be invoked by the tgui_list_input upon having a choice made.
|
||||||
var/datum/callback/callback
|
var/datum/callback/callback
|
||||||
|
|
||||||
/datum/tgui_list_input/async/New(mob/user, message, title, list/buttons, default, callback, timeout)
|
/datum/tgui_list_input/async/New(mob/user, message, title, list/items, default, callback, timeout)
|
||||||
..(user, title, message, buttons, default, timeout)
|
..(user, title, message, items, default, timeout)
|
||||||
src.callback = callback
|
src.callback = callback
|
||||||
|
|
||||||
/datum/tgui_list_input/async/Destroy(force, ...)
|
/datum/tgui_list_input/async/Destroy(force, ...)
|
||||||
209
code/modules/tgui_input/number.dm
Normal file
209
code/modules/tgui_input/number.dm
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
/**
|
||||||
|
* Creates a TGUI window with a number input. Returns the user's response as num | null.
|
||||||
|
*
|
||||||
|
* This proc should be used to create windows for number entry that the caller will wait for a response from.
|
||||||
|
* If tgui fancy chat is turned off: Will return a normal input. If a max or min value is specified, will
|
||||||
|
* validate the input inside the UI and ui_act.
|
||||||
|
*
|
||||||
|
* Arguments:
|
||||||
|
* * user - The user to show the number input to.
|
||||||
|
* * message - The content of the number input, shown in the body of the TGUI window.
|
||||||
|
* * title - The title of the number input modal, shown on the top of the TGUI window.
|
||||||
|
* * default - The default (or current) value, shown as a placeholder. Users can press refresh with this.
|
||||||
|
* * max_value - Specifies a maximum value. If none is set, any number can be entered. Pressing "max" defaults to 1000.
|
||||||
|
* * min_value - Specifies a minimum value. Often 0.
|
||||||
|
* * timeout - The timeout of the number input, after which the modal will close and qdel itself. Set to zero for no timeout.
|
||||||
|
* * round_value - whether the inputted number is rounded down into an integer.
|
||||||
|
*/
|
||||||
|
/proc/tgui_input_number(mob/user, message, title = "Number Input", default = 0, max_value = 10000, min_value = 0, timeout = 0, round_value = TRUE)
|
||||||
|
if (!user)
|
||||||
|
user = usr
|
||||||
|
if (!istype(user))
|
||||||
|
if (istype(user, /client))
|
||||||
|
var/client/client = user
|
||||||
|
user = client.mob
|
||||||
|
else
|
||||||
|
return
|
||||||
|
// Client does NOT have tgui_input on: Returns regular input
|
||||||
|
if(!usr.client.prefs.tgui_input_mode)
|
||||||
|
var/input_number = input(user, message, title, default) as null|num
|
||||||
|
return clamp(round_value ? round(input_number) : input_number, min_value, max_value)
|
||||||
|
var/datum/tgui_input_number/number_input = new(user, message, title, default, max_value, min_value, timeout, round_value)
|
||||||
|
number_input.tgui_interact(user)
|
||||||
|
number_input.wait()
|
||||||
|
if (number_input)
|
||||||
|
. = number_input.entry
|
||||||
|
qdel(number_input)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # tgui_input_number
|
||||||
|
*
|
||||||
|
* Datum used for instantiating and using a TGUI-controlled number input that prompts the user with
|
||||||
|
* a message and has an input for number entry.
|
||||||
|
*/
|
||||||
|
/datum/tgui_input_number
|
||||||
|
/// Boolean field describing if the tgui_input_number was closed by the user.
|
||||||
|
var/closed
|
||||||
|
/// The default (or current) value, shown as a default. Users can press reset with this.
|
||||||
|
var/default
|
||||||
|
/// The entry that the user has return_typed in.
|
||||||
|
var/entry
|
||||||
|
/// The maximum value that can be entered.
|
||||||
|
var/max_value
|
||||||
|
/// The prompt's body, if any, of the TGUI window.
|
||||||
|
var/message
|
||||||
|
/// The minimum value that can be entered.
|
||||||
|
var/min_value
|
||||||
|
/// Whether the submitted number is rounded down into an integer.
|
||||||
|
var/round_value
|
||||||
|
/// The time at which the number input was created, for displaying timeout progress.
|
||||||
|
var/start_time
|
||||||
|
/// The lifespan of the number input, after which the window will close and delete itself.
|
||||||
|
var/timeout
|
||||||
|
/// The title of the TGUI window
|
||||||
|
var/title
|
||||||
|
|
||||||
|
/datum/tgui_input_number/New(mob/user, message, title, default, max_value, min_value, timeout, round_value)
|
||||||
|
src.default = default
|
||||||
|
src.max_value = max_value
|
||||||
|
src.message = message
|
||||||
|
src.min_value = min_value
|
||||||
|
src.title = title
|
||||||
|
src.round_value = round_value
|
||||||
|
if (timeout)
|
||||||
|
src.timeout = timeout
|
||||||
|
start_time = world.time
|
||||||
|
QDEL_IN(src, timeout)
|
||||||
|
/// Checks for empty numbers - bank accounts, etc.
|
||||||
|
if(max_value == 0)
|
||||||
|
src.min_value = 0
|
||||||
|
if(default)
|
||||||
|
src.default = 0
|
||||||
|
/// Sanity check
|
||||||
|
if(default < min_value)
|
||||||
|
src.default = min_value
|
||||||
|
if(default > max_value)
|
||||||
|
CRASH("Default value is greater than max value.")
|
||||||
|
|
||||||
|
/datum/tgui_input_number/Destroy(force, ...)
|
||||||
|
SStgui.close_uis(src)
|
||||||
|
return ..()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for a user's response to the tgui_input_number's prompt before returning. Returns early if
|
||||||
|
* the window was closed by the user.
|
||||||
|
*/
|
||||||
|
/datum/tgui_input_number/proc/wait()
|
||||||
|
while (!entry && !closed && !QDELETED(src))
|
||||||
|
stoplag(1)
|
||||||
|
|
||||||
|
/datum/tgui_input_number/tgui_interact(mob/user, datum/tgui/ui)
|
||||||
|
ui = SStgui.try_update_ui(user, src, ui)
|
||||||
|
if(!ui)
|
||||||
|
ui = new(user, src, "NumberInputModal")
|
||||||
|
ui.open()
|
||||||
|
|
||||||
|
/datum/tgui_input_number/tgui_close(mob/user)
|
||||||
|
. = ..()
|
||||||
|
closed = TRUE
|
||||||
|
|
||||||
|
/datum/tgui_input_number/tgui_state(mob/user)
|
||||||
|
return GLOB.tgui_always_state
|
||||||
|
|
||||||
|
/datum/tgui_input_number/tgui_static_data(mob/user)
|
||||||
|
var/list/data = list()
|
||||||
|
data["init_value"] = default // Default is a reserved keyword
|
||||||
|
data["large_buttons"] = usr.client.prefs.tgui_large_buttons
|
||||||
|
data["max_value"] = max_value
|
||||||
|
data["message"] = message
|
||||||
|
data["min_value"] = min_value
|
||||||
|
data["swapped_buttons"] = !usr.client.prefs.tgui_swapped_buttons
|
||||||
|
data["title"] = title
|
||||||
|
return data
|
||||||
|
|
||||||
|
/datum/tgui_input_number/tgui_data(mob/user)
|
||||||
|
var/list/data = list()
|
||||||
|
if(timeout)
|
||||||
|
data["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
|
||||||
|
return data
|
||||||
|
|
||||||
|
/datum/tgui_input_number/tgui_act(action, list/params)
|
||||||
|
. = ..()
|
||||||
|
if (.)
|
||||||
|
return
|
||||||
|
switch(action)
|
||||||
|
if("submit")
|
||||||
|
if(!isnum(params["entry"]))
|
||||||
|
CRASH("A non number was input into tgui input number by [usr]")
|
||||||
|
var/choice = round_value ? round(params["entry"]) : params["entry"]
|
||||||
|
if(choice > max_value)
|
||||||
|
CRASH("A number greater than the max value was input into tgui input number by [usr]")
|
||||||
|
if(choice < min_value)
|
||||||
|
CRASH("A number less than the min value was input into tgui input number by [usr]")
|
||||||
|
set_entry(choice)
|
||||||
|
closed = TRUE
|
||||||
|
SStgui.close_uis(src)
|
||||||
|
return TRUE
|
||||||
|
if("cancel")
|
||||||
|
closed = TRUE
|
||||||
|
SStgui.close_uis(src)
|
||||||
|
return TRUE
|
||||||
|
|
||||||
|
/datum/tgui_input_number/proc/set_entry(entry)
|
||||||
|
src.entry = entry
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an asynchronous TGUI input num window with an associated callback.
|
||||||
|
*
|
||||||
|
* This proc should be used to create inputs that invoke a callback with the user's chosen option.
|
||||||
|
* Arguments:
|
||||||
|
* * user - The user to show the input box to.
|
||||||
|
* * message - The content of the input box, shown in the body of the TGUI window.
|
||||||
|
* * title - The title of the input box, shown on the top of the TGUI window.
|
||||||
|
* * default - The default value pre-populated in the input box.
|
||||||
|
* * callback - The callback to be invoked when a choice is made.
|
||||||
|
* * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
|
||||||
|
*/
|
||||||
|
/proc/tgui_input_number_async(mob/user, message, title, default, datum/callback/callback, timeout = 60 SECONDS)
|
||||||
|
if (istext(user))
|
||||||
|
stack_trace("tgui_input_num_async() received text for user instead of mob")
|
||||||
|
return
|
||||||
|
if (!user)
|
||||||
|
user = usr
|
||||||
|
if (!istype(user))
|
||||||
|
if (istype(user, /client))
|
||||||
|
var/client/client = user
|
||||||
|
user = client.mob
|
||||||
|
else
|
||||||
|
return
|
||||||
|
var/datum/tgui_input_number/async/input = new(user, message, title, default, callback, timeout)
|
||||||
|
input.tgui_interact(user)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # async tgui_text_input
|
||||||
|
*
|
||||||
|
* An asynchronous version of tgui_text_input to be used with callbacks instead of waiting on user responses.
|
||||||
|
*/
|
||||||
|
/datum/tgui_input_number/async
|
||||||
|
/// The callback to be invoked by the tgui_text_input upon having a choice made.
|
||||||
|
var/datum/callback/callback
|
||||||
|
|
||||||
|
/datum/tgui_input_number/async/New(mob/user, message, title, default, callback, timeout)
|
||||||
|
..(user, title, message, default, timeout)
|
||||||
|
src.callback = callback
|
||||||
|
|
||||||
|
/datum/tgui_input_number/async/Destroy(force, ...)
|
||||||
|
QDEL_NULL(callback)
|
||||||
|
. = ..()
|
||||||
|
|
||||||
|
/datum/tgui_input_number/async/tgui_close(mob/user)
|
||||||
|
. = ..()
|
||||||
|
qdel(src)
|
||||||
|
|
||||||
|
/datum/tgui_input_number/async/set_entry(entry)
|
||||||
|
. = ..()
|
||||||
|
if(!isnull(src.entry))
|
||||||
|
callback?.InvokeAsync(src.entry)
|
||||||
|
|
||||||
|
/datum/tgui_input_number/async/wait()
|
||||||
|
return
|
||||||
209
code/modules/tgui_input/text.dm
Normal file
209
code/modules/tgui_input/text.dm
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
/**
|
||||||
|
* Creates a TGUI window with a text input. Returns the user's response.
|
||||||
|
*
|
||||||
|
* This proc should be used to create windows for text entry that the caller will wait for a response from.
|
||||||
|
* If tgui fancy chat is turned off: Will return a normal input. If max_length is specified, will return
|
||||||
|
* stripped_multiline_input.
|
||||||
|
*
|
||||||
|
* Arguments:
|
||||||
|
* * user - The user to show the text input to.
|
||||||
|
* * message - The content of the text input, shown in the body of the TGUI window.
|
||||||
|
* * title - The title of the text input modal, shown on the top of the TGUI window.
|
||||||
|
* * default - The default (or current) value, shown as a placeholder.
|
||||||
|
* * max_length - Specifies a max length for input. MAX_MESSAGE_LEN is default (1024)
|
||||||
|
* * multiline - Bool that determines if the input box is much larger. Good for large messages, laws, etc.
|
||||||
|
* * encode - Toggling this determines if input is filtered via html_encode. Setting this to FALSE gives raw input.
|
||||||
|
* * timeout - The timeout of the textbox, after which the modal will close and qdel itself. Set to zero for no timeout.
|
||||||
|
*/
|
||||||
|
/proc/tgui_input_text(mob/user, message = "", title = "Text Input", default, max_length = MAX_MESSAGE_LEN, multiline = FALSE, encode = TRUE, timeout = 0)
|
||||||
|
if (istext(user))
|
||||||
|
stack_trace("tgui_input_text() received text for user instead of mob")
|
||||||
|
return
|
||||||
|
if (!user)
|
||||||
|
user = usr
|
||||||
|
if (!istype(user))
|
||||||
|
if (istype(user, /client))
|
||||||
|
var/client/client = user
|
||||||
|
user = client.mob
|
||||||
|
else
|
||||||
|
return
|
||||||
|
// Client does NOT have tgui_input on: Returns regular input
|
||||||
|
if(!usr.client.prefs.tgui_input_mode)
|
||||||
|
if(encode)
|
||||||
|
if(multiline)
|
||||||
|
return stripped_multiline_input(user, message, title, default, max_length)
|
||||||
|
else
|
||||||
|
return stripped_input(user, message, title, default, max_length)
|
||||||
|
else
|
||||||
|
if(multiline)
|
||||||
|
return input(user, message, title, default) as message|null
|
||||||
|
else
|
||||||
|
return input(user, message, title, default) as text|null
|
||||||
|
var/datum/tgui_input_text/text_input = new(user, message, title, default, max_length, multiline, encode, timeout)
|
||||||
|
text_input.tgui_interact(user)
|
||||||
|
text_input.wait()
|
||||||
|
if (text_input)
|
||||||
|
. = text_input.entry
|
||||||
|
qdel(text_input)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tgui_input_text
|
||||||
|
*
|
||||||
|
* Datum used for instantiating and using a TGUI-controlled text input that prompts the user with
|
||||||
|
* a message and has an input for text entry.
|
||||||
|
*/
|
||||||
|
/datum/tgui_input_text
|
||||||
|
/// Boolean field describing if the tgui_input_text was closed by the user.
|
||||||
|
var/closed
|
||||||
|
/// The default (or current) value, shown as a default.
|
||||||
|
var/default
|
||||||
|
/// Whether the input should be stripped using html_encode
|
||||||
|
var/encode
|
||||||
|
/// The entry that the user has return_typed in.
|
||||||
|
var/entry
|
||||||
|
/// The maximum length for text entry
|
||||||
|
var/max_length
|
||||||
|
/// The prompt's body, if any, of the TGUI window.
|
||||||
|
var/message
|
||||||
|
/// Multiline input for larger input boxes.
|
||||||
|
var/multiline
|
||||||
|
/// The time at which the text input was created, for displaying timeout progress.
|
||||||
|
var/start_time
|
||||||
|
/// The lifespan of the text input, after which the window will close and delete itself.
|
||||||
|
var/timeout
|
||||||
|
/// The title of the TGUI window
|
||||||
|
var/title
|
||||||
|
|
||||||
|
/datum/tgui_input_text/New(mob/user, message, title, default, max_length, multiline, encode, timeout)
|
||||||
|
src.default = default
|
||||||
|
src.encode = encode
|
||||||
|
src.max_length = max_length
|
||||||
|
src.message = message
|
||||||
|
src.multiline = multiline
|
||||||
|
src.title = title
|
||||||
|
if (timeout)
|
||||||
|
src.timeout = timeout
|
||||||
|
start_time = world.time
|
||||||
|
QDEL_IN(src, timeout)
|
||||||
|
|
||||||
|
/datum/tgui_input_text/Destroy(force, ...)
|
||||||
|
SStgui.close_uis(src)
|
||||||
|
. = ..()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for a user's response to the tgui_text_input's prompt before returning. Returns early if
|
||||||
|
* the window was closed by the user.
|
||||||
|
*/
|
||||||
|
/datum/tgui_input_text/proc/wait()
|
||||||
|
while (!entry && !closed)
|
||||||
|
stoplag(1)
|
||||||
|
|
||||||
|
/datum/tgui_input_text/tgui_interact(mob/user, datum/tgui/ui)
|
||||||
|
ui = SStgui.try_update_ui(user, src, ui)
|
||||||
|
if(!ui)
|
||||||
|
ui = new(user, src, "TextInputModal")
|
||||||
|
ui.open()
|
||||||
|
|
||||||
|
/datum/tgui_input_text/tgui_close(mob/user)
|
||||||
|
. = ..()
|
||||||
|
closed = TRUE
|
||||||
|
|
||||||
|
/datum/tgui_input_text/tgui_state(mob/user)
|
||||||
|
return GLOB.tgui_always_state
|
||||||
|
|
||||||
|
/datum/tgui_input_text/tgui_static_data(mob/user)
|
||||||
|
var/list/data = list()
|
||||||
|
data["large_buttons"] = usr.client.prefs.tgui_large_buttons
|
||||||
|
data["max_length"] = max_length
|
||||||
|
data["message"] = message
|
||||||
|
data["multiline"] = multiline
|
||||||
|
data["placeholder"] = default // Default is a reserved keyword
|
||||||
|
data["swapped_buttons"] = !usr.client.prefs.tgui_swapped_buttons
|
||||||
|
data["title"] = title
|
||||||
|
return data
|
||||||
|
|
||||||
|
/datum/tgui_input_text/tgui_data(mob/user)
|
||||||
|
var/list/data = list()
|
||||||
|
if(timeout)
|
||||||
|
.["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1)
|
||||||
|
return data
|
||||||
|
|
||||||
|
/datum/tgui_input_text/tgui_act(action, list/params)
|
||||||
|
. = ..()
|
||||||
|
if (.)
|
||||||
|
return
|
||||||
|
switch(action)
|
||||||
|
if("submit")
|
||||||
|
if(length(params["entry"]) > max_length)
|
||||||
|
return
|
||||||
|
if(encode && (length(html_encode(params["entry"])) > max_length))
|
||||||
|
to_chat(usr, span_notice("Your message was clipped due to special character usage."))
|
||||||
|
set_entry(params["entry"])
|
||||||
|
closed = TRUE
|
||||||
|
SStgui.close_uis(src)
|
||||||
|
return TRUE
|
||||||
|
if("cancel")
|
||||||
|
SStgui.close_uis(src)
|
||||||
|
closed = TRUE
|
||||||
|
return TRUE
|
||||||
|
|
||||||
|
/datum/tgui_input_text/proc/set_entry(entry)
|
||||||
|
if(!isnull(entry))
|
||||||
|
var/converted_entry = encode ? html_encode(entry) : entry
|
||||||
|
src.entry = trim(converted_entry, max_length)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an asynchronous TGUI input text window with an associated callback.
|
||||||
|
*
|
||||||
|
* This proc should be used to create inputs that invoke a callback with the user's chosen option.
|
||||||
|
* Arguments:
|
||||||
|
* * user - The user to show the input box to.
|
||||||
|
* * message - The content of the input box, shown in the body of the TGUI window.
|
||||||
|
* * title - The title of the input box, shown on the top of the TGUI window.
|
||||||
|
* * default - The default value pre-populated in the input box.
|
||||||
|
* * callback - The callback to be invoked when a choice is made.
|
||||||
|
* * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
|
||||||
|
*/
|
||||||
|
/proc/tgui_input_text_async(mob/user, message, title, default, datum/callback/callback, max_length, multiline, encode, timeout = 60 SECONDS)
|
||||||
|
if (istext(user))
|
||||||
|
stack_trace("tgui_input_text_async() received text for user instead of mob")
|
||||||
|
return
|
||||||
|
if (!user)
|
||||||
|
user = usr
|
||||||
|
if (!istype(user))
|
||||||
|
if (istype(user, /client))
|
||||||
|
var/client/client = user
|
||||||
|
user = client.mob
|
||||||
|
else
|
||||||
|
return
|
||||||
|
var/datum/tgui_input_text/async/input = new(user, message, title, default, callback, max_length, multiline, encode, timeout)
|
||||||
|
input.tgui_interact(user)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* # async tgui_text_input
|
||||||
|
*
|
||||||
|
* An asynchronous version of tgui_text_input to be used with callbacks instead of waiting on user responses.
|
||||||
|
*/
|
||||||
|
/datum/tgui_input_text/async
|
||||||
|
/// The callback to be invoked by the tgui_text_input upon having a choice made.
|
||||||
|
var/datum/callback/callback
|
||||||
|
|
||||||
|
/datum/tgui_input_text/async/New(mob/user, message, title, default, callback, max_length, multiline, encode, timeout)
|
||||||
|
..(user, title, message, default, max_length, multiline, encode, timeout)
|
||||||
|
src.callback = callback
|
||||||
|
|
||||||
|
/datum/tgui_input_text/async/Destroy(force, ...)
|
||||||
|
QDEL_NULL(callback)
|
||||||
|
. = ..()
|
||||||
|
|
||||||
|
/datum/tgui_input_text/async/tgui_close(mob/user)
|
||||||
|
. = ..()
|
||||||
|
qdel(src)
|
||||||
|
|
||||||
|
/datum/tgui_input_text/async/set_entry(entry)
|
||||||
|
. = ..()
|
||||||
|
if(!isnull(src.entry))
|
||||||
|
callback?.InvokeAsync(src.entry)
|
||||||
|
|
||||||
|
/datum/tgui_input_text/async/wait()
|
||||||
|
return
|
||||||
@@ -227,6 +227,7 @@ type BackendState<TData> = {
|
|||||||
title: string,
|
title: string,
|
||||||
status: number,
|
status: number,
|
||||||
interface: string,
|
interface: string,
|
||||||
|
refreshing: boolean,
|
||||||
window: {
|
window: {
|
||||||
key: string,
|
key: string,
|
||||||
size: [number, number],
|
size: [number, number],
|
||||||
|
|||||||
154
tgui/packages/tgui/components/RestrictedInput.js
Normal file
154
tgui/packages/tgui/components/RestrictedInput.js
Normal file
@@ -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 (
|
||||||
|
<Box
|
||||||
|
className={classes([
|
||||||
|
'Input',
|
||||||
|
fluid && 'Input--fluid',
|
||||||
|
monospace && 'Input--monospace',
|
||||||
|
className,
|
||||||
|
])}
|
||||||
|
{...rest}>
|
||||||
|
<div className="Input__baseline">.</div>
|
||||||
|
<input
|
||||||
|
className="Input__input"
|
||||||
|
onChange={this.handleChange}
|
||||||
|
onInput={this.handleInput}
|
||||||
|
onFocus={this.handleFocus}
|
||||||
|
onBlur={this.handleBlur}
|
||||||
|
onKeyDown={this.handleKeyDown}
|
||||||
|
ref={this.inputRef}
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ export { NoticeBox } from './NoticeBox';
|
|||||||
export { NumberInput } from './NumberInput';
|
export { NumberInput } from './NumberInput';
|
||||||
export { ProgressBar } from './ProgressBar';
|
export { ProgressBar } from './ProgressBar';
|
||||||
export { Popper } from './Popper';
|
export { Popper } from './Popper';
|
||||||
|
export { RestrictedInput } from './RestrictedInput';
|
||||||
export { RoundGauge } from './RoundGauge';
|
export { RoundGauge } from './RoundGauge';
|
||||||
export { Section } from './Section';
|
export { Section } from './Section';
|
||||||
export { Slider } from './Slider';
|
export { Slider } from './Slider';
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file
|
|
||||||
* @copyright 2020 bobbahbrown (https://github.com/bobbahbrown)
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Loader } from "./common/Loader";
|
|
||||||
import { useBackend } from '../backend';
|
|
||||||
import { Component, createRef } from 'inferno';
|
|
||||||
import { Box, Flex, Section } from '../components';
|
|
||||||
import { Window } from '../layouts';
|
|
||||||
import {
|
|
||||||
KEY_ENTER,
|
|
||||||
KEY_LEFT,
|
|
||||||
KEY_RIGHT,
|
|
||||||
KEY_SPACE,
|
|
||||||
KEY_TAB,
|
|
||||||
} from 'common/keycodes';
|
|
||||||
|
|
||||||
export class AlertModal extends Component {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.buttonRefs = [createRef()];
|
|
||||||
this.state = { current: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { data } = useBackend(this.context);
|
|
||||||
const { buttons } = data;
|
|
||||||
const { current } = this.state;
|
|
||||||
const button = this.buttonRefs[current].current;
|
|
||||||
|
|
||||||
// Fill ref array with refs for other buttons
|
|
||||||
for (let i = 1; i < buttons.length; i++) {
|
|
||||||
this.buttonRefs.push(createRef());
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => button.focus(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrent(current, isArrowKey) {
|
|
||||||
const { data } = useBackend(this.context);
|
|
||||||
const { buttons } = data;
|
|
||||||
|
|
||||||
// Mimic alert() behavior for tabs and arrow keys
|
|
||||||
if (current >= buttons.length) {
|
|
||||||
current = isArrowKey ? current - 1 : 0;
|
|
||||||
} else if (current < 0) {
|
|
||||||
current = isArrowKey ? 0 : buttons.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const button = this.buttonRefs[current].current;
|
|
||||||
|
|
||||||
// Prevents an error from occurring on close
|
|
||||||
if (button) {
|
|
||||||
setTimeout(() => button.focus(), 1);
|
|
||||||
}
|
|
||||||
this.setState({ current });
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { act, data } = useBackend(this.context);
|
|
||||||
const { title, message, buttons, timeout } = data;
|
|
||||||
const { current } = this.state;
|
|
||||||
const focusCurrentButton = () => this.setCurrent(current, false);
|
|
||||||
|
|
||||||
const windowHeight = Math.max(150, message.length);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Window
|
|
||||||
title={title}
|
|
||||||
theme="abstract"
|
|
||||||
width={350}
|
|
||||||
height={windowHeight}
|
|
||||||
canClose={timeout > 0}>
|
|
||||||
{timeout && <Loader value={timeout} />}
|
|
||||||
<Window.Content
|
|
||||||
onFocus={focusCurrentButton}
|
|
||||||
onClick={focusCurrentButton}>
|
|
||||||
<Section fill>
|
|
||||||
<Flex direction="column" height="100%">
|
|
||||||
<Flex.Item grow={1}>
|
|
||||||
<Flex
|
|
||||||
direction="column"
|
|
||||||
className="AlertModal__Message"
|
|
||||||
height="100%">
|
|
||||||
<Flex.Item>
|
|
||||||
<Box m={1}>
|
|
||||||
{message}
|
|
||||||
</Box>
|
|
||||||
</Flex.Item>
|
|
||||||
</Flex>
|
|
||||||
</Flex.Item>
|
|
||||||
<Flex.Item my={2}>
|
|
||||||
<Flex className="AlertModal__Buttons" wrap>
|
|
||||||
{buttons.map((button, buttonIndex) => (
|
|
||||||
<Flex.Item key={buttonIndex} mx={1} my={0.5}>
|
|
||||||
<div
|
|
||||||
ref={this.buttonRefs[buttonIndex]}
|
|
||||||
className="Button Button--color--default"
|
|
||||||
px={3}
|
|
||||||
onClick={() => act("choose", { choice: button })}
|
|
||||||
onKeyDown={e => {
|
|
||||||
const keyCode = window.event ? e.which : e.keyCode;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simulate a click when pressing space or enter,
|
|
||||||
* allow keyboard navigation, override tab behavior
|
|
||||||
*/
|
|
||||||
if (keyCode === KEY_SPACE || keyCode === KEY_ENTER) {
|
|
||||||
act("choose", { choice: button });
|
|
||||||
} else if (
|
|
||||||
keyCode === KEY_LEFT
|
|
||||||
|| (e.shiftKey && keyCode === KEY_TAB)
|
|
||||||
) {
|
|
||||||
this.setCurrent(current - 1, keyCode === KEY_LEFT);
|
|
||||||
} else if (
|
|
||||||
keyCode === KEY_RIGHT || keyCode === KEY_TAB
|
|
||||||
) {
|
|
||||||
this.setCurrent(current + 1, keyCode === KEY_RIGHT);
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
{button}
|
|
||||||
</div>
|
|
||||||
</Flex.Item>
|
|
||||||
))}
|
|
||||||
</Flex>
|
|
||||||
</Flex.Item>
|
|
||||||
</Flex>
|
|
||||||
</Section>
|
|
||||||
</Window.Content>
|
|
||||||
</Window>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
151
tgui/packages/tgui/interfaces/AlertModal.tsx
Normal file
151
tgui/packages/tgui/interfaces/AlertModal.tsx
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import { Loader } from './common/Loader';
|
||||||
|
import { useBackend, useLocalState } from '../backend';
|
||||||
|
import { KEY_ENTER, KEY_ESCAPE, KEY_LEFT, KEY_RIGHT, KEY_SPACE, KEY_TAB } from '../../common/keycodes';
|
||||||
|
import { Autofocus, Box, Button, Flex, Section, Stack } from '../components';
|
||||||
|
import { Window } from '../layouts';
|
||||||
|
|
||||||
|
type AlertModalData = {
|
||||||
|
autofocus: boolean;
|
||||||
|
buttons: string[];
|
||||||
|
large_buttons: boolean;
|
||||||
|
message: string;
|
||||||
|
swapped_buttons: boolean;
|
||||||
|
timeout: number;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const KEY_DECREMENT = -1;
|
||||||
|
const KEY_INCREMENT = 1;
|
||||||
|
|
||||||
|
export const AlertModal = (_, context) => {
|
||||||
|
const { act, data } = useBackend<AlertModalData>(context);
|
||||||
|
const {
|
||||||
|
autofocus,
|
||||||
|
buttons = [],
|
||||||
|
large_buttons,
|
||||||
|
message = '',
|
||||||
|
timeout,
|
||||||
|
title,
|
||||||
|
} = data;
|
||||||
|
const [selected, setSelected] = useLocalState<number>(context, 'selected', 0);
|
||||||
|
// Dynamically sets window dimensions
|
||||||
|
const windowHeight
|
||||||
|
= 115
|
||||||
|
+ (message.length > 30 ? Math.ceil(message.length / 4) : 0)
|
||||||
|
+ (message.length && large_buttons ? 5 : 0);
|
||||||
|
const windowWidth = 325 + (buttons.length > 2 ? 55 : 0);
|
||||||
|
const onKey = (direction: number) => {
|
||||||
|
if (selected === 0 && direction === KEY_DECREMENT) {
|
||||||
|
setSelected(buttons.length - 1);
|
||||||
|
} else if (selected === buttons.length - 1 && direction === KEY_INCREMENT) {
|
||||||
|
setSelected(0);
|
||||||
|
} else {
|
||||||
|
setSelected(selected + direction);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Window height={windowHeight} title={title} width={windowWidth}>
|
||||||
|
{!!timeout && <Loader value={timeout} />}
|
||||||
|
<Window.Content
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
const keyCode = window.event ? e.which : e.keyCode;
|
||||||
|
/**
|
||||||
|
* Simulate a click when pressing space or enter,
|
||||||
|
* allow keyboard navigation, override tab behavior
|
||||||
|
*/
|
||||||
|
if (keyCode === KEY_SPACE || keyCode === KEY_ENTER) {
|
||||||
|
act('choose', { choice: buttons[selected] });
|
||||||
|
} else if (keyCode === KEY_ESCAPE) {
|
||||||
|
act('cancel');
|
||||||
|
} else if (keyCode === KEY_LEFT) {
|
||||||
|
e.preventDefault();
|
||||||
|
onKey(KEY_DECREMENT);
|
||||||
|
} else if (keyCode === KEY_TAB || keyCode === KEY_RIGHT) {
|
||||||
|
e.preventDefault();
|
||||||
|
onKey(KEY_INCREMENT);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<Section fill>
|
||||||
|
<Stack fill vertical>
|
||||||
|
<Stack.Item grow m={1}>
|
||||||
|
<Box color="label" overflow="hidden">
|
||||||
|
{message}
|
||||||
|
</Box>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item>
|
||||||
|
{!!autofocus && <Autofocus />}
|
||||||
|
<ButtonDisplay selected={selected} />
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
</Section>
|
||||||
|
</Window.Content>
|
||||||
|
</Window>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<AlertModalData>(context);
|
||||||
|
const { buttons = [], large_buttons, swapped_buttons } = data;
|
||||||
|
const { selected } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
direction={!swapped_buttons ? 'row-reverse' : 'row'}
|
||||||
|
fill
|
||||||
|
justify="space-around"
|
||||||
|
wrap>
|
||||||
|
{buttons?.map((button, index) =>
|
||||||
|
!!large_buttons && buttons.length < 3 ? (
|
||||||
|
<Flex.Item grow key={index}>
|
||||||
|
<AlertButton
|
||||||
|
button={button}
|
||||||
|
id={index.toString()}
|
||||||
|
selected={selected === index}
|
||||||
|
/>
|
||||||
|
</Flex.Item>
|
||||||
|
) : (
|
||||||
|
<Flex.Item key={index}>
|
||||||
|
<AlertButton
|
||||||
|
button={button}
|
||||||
|
id={index.toString()}
|
||||||
|
selected={selected === index}
|
||||||
|
/>
|
||||||
|
</Flex.Item>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a button with variable sizing.
|
||||||
|
*/
|
||||||
|
const AlertButton = (props, context) => {
|
||||||
|
const { act, data } = useBackend<AlertModalData>(context);
|
||||||
|
const { large_buttons } = data;
|
||||||
|
const { button, selected } = props;
|
||||||
|
const buttonWidth = button.length > 7 ? button.length : 7;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
fluid={!!large_buttons}
|
||||||
|
height={!!large_buttons && 2}
|
||||||
|
onClick={() => act('choose', { choice: button })}
|
||||||
|
m={0.5}
|
||||||
|
pl={2}
|
||||||
|
pr={2}
|
||||||
|
pt={large_buttons ? 0.33 : 0}
|
||||||
|
selected={selected}
|
||||||
|
textAlign="center"
|
||||||
|
width={!large_buttons && buttonWidth}>
|
||||||
|
{!large_buttons ? button : button.toUpperCase()}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file
|
|
||||||
* @copyright 2021 Leshana
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { clamp01 } from 'common/math';
|
|
||||||
import { useBackend, useLocalState } from '../backend';
|
|
||||||
import { Box, Button, Section, Input, Stack, TextArea } from '../components';
|
|
||||||
import { KEY_ESCAPE } from 'common/keycodes';
|
|
||||||
import { Window } from '../layouts';
|
|
||||||
import { createLogger } from '../logging';
|
|
||||||
|
|
||||||
const logger = createLogger('inputmodal');
|
|
||||||
|
|
||||||
export const InputModal = (props, context) => {
|
|
||||||
const { act, data } = useBackend(context);
|
|
||||||
const { title, message, initial, input_type, timeout } = data;
|
|
||||||
|
|
||||||
// Current Input Value
|
|
||||||
const [curValue, setCurValue] = useLocalState(context, 'curValue', initial);
|
|
||||||
|
|
||||||
const handleKeyDown = e => {
|
|
||||||
if (e.keyCode === KEY_ESCAPE) {
|
|
||||||
e.preventDefault();
|
|
||||||
act("cancel");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let initialHeight, initialWidth;
|
|
||||||
let modalBody;
|
|
||||||
switch (input_type) {
|
|
||||||
case 'text':
|
|
||||||
case 'num':
|
|
||||||
initialWidth = 325;
|
|
||||||
initialHeight = Math.max(150, message.length);
|
|
||||||
modalBody = (
|
|
||||||
<Input
|
|
||||||
value={initial}
|
|
||||||
width="100%"
|
|
||||||
autoFocus
|
|
||||||
autoSelect
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onChange={(_e, val) => {
|
|
||||||
setCurValue(val);
|
|
||||||
}}
|
|
||||||
onEnter={(_e, val) => {
|
|
||||||
act('choose', { choice: val });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'message':
|
|
||||||
initialWidth = 450;
|
|
||||||
initialHeight = 350;
|
|
||||||
modalBody = (
|
|
||||||
<TextArea
|
|
||||||
value={initial}
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
autoFocus
|
|
||||||
dontUseTabForIndent
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onChange={(_e, val) => {
|
|
||||||
setCurValue(val);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Window title={title} theme="abstract" width={initialWidth} height={initialHeight}>
|
|
||||||
{timeout !== undefined && <Loader value={timeout} />}
|
|
||||||
<Window.Content>
|
|
||||||
<Stack fill vertical>
|
|
||||||
<Stack.Item grow>
|
|
||||||
<Section fill scrollable className="InputModal__Section" title={message} tabIndex={0}>
|
|
||||||
{modalBody}
|
|
||||||
</Section>
|
|
||||||
</Stack.Item>
|
|
||||||
<Stack.Item>
|
|
||||||
<Stack textAlign="center">
|
|
||||||
<Stack.Item grow basis={0}>
|
|
||||||
<Button
|
|
||||||
fluid
|
|
||||||
color="good"
|
|
||||||
lineHeight={2}
|
|
||||||
content="Confirm"
|
|
||||||
onClick={() => act('choose', { choice: curValue })}
|
|
||||||
/>
|
|
||||||
</Stack.Item>
|
|
||||||
<Stack.Item grow basis={0}>
|
|
||||||
<Button fluid color="bad" lineHeight={2} content="Cancel" onClick={() => act('cancel')} />
|
|
||||||
</Stack.Item>
|
|
||||||
</Stack>
|
|
||||||
</Stack.Item>
|
|
||||||
</Stack>
|
|
||||||
</Window.Content>
|
|
||||||
</Window>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Loader = (props) => {
|
|
||||||
const { value } = props;
|
|
||||||
return (
|
|
||||||
<div className="InputModal__Loader">
|
|
||||||
<Box
|
|
||||||
className="InputModal__LoaderProgress"
|
|
||||||
style={{
|
|
||||||
width: clamp01(value) * 100 + '%',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file
|
|
||||||
* @copyright 2020 watermelon914 (https://github.com/watermelon914)
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Loader } from "./common/Loader";
|
|
||||||
import { useBackend, useLocalState } from '../backend';
|
|
||||||
import { Button, Section, Input, Stack } from '../components';
|
|
||||||
import { KEY_DOWN, KEY_UP, KEY_ENTER, KEY_SPACE, KEY_ESCAPE, KEY_HOME, KEY_END } from 'common/keycodes';
|
|
||||||
import { Window } from '../layouts';
|
|
||||||
|
|
||||||
let lastScrollTime = 0;
|
|
||||||
|
|
||||||
export const ListInput = (props, context) => {
|
|
||||||
const { act, data } = useBackend(context);
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
buttons,
|
|
||||||
timeout,
|
|
||||||
initial,
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
// Search
|
|
||||||
const [showSearchBar, setShowSearchBar] = useLocalState(
|
|
||||||
context, 'search_bar', false);
|
|
||||||
const [displayedArray, setDisplayedArray] = useLocalState(
|
|
||||||
context, 'displayed_array', buttons);
|
|
||||||
|
|
||||||
// KeyPress
|
|
||||||
const [searchArray, setSearchArray] = useLocalState(
|
|
||||||
context, 'search_array', []);
|
|
||||||
const [searchIndex, setSearchIndex] = useLocalState(
|
|
||||||
context, 'search_index', 0);
|
|
||||||
const [lastCharCode, setLastCharCode] = useLocalState(
|
|
||||||
context, 'last_char_code', null);
|
|
||||||
|
|
||||||
// Selected Button
|
|
||||||
const [selectedButton, setSelectedButton] = useLocalState(
|
|
||||||
context, 'selected_button', initial || buttons[0]);
|
|
||||||
|
|
||||||
const handleKeyDown = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (lastScrollTime > performance.now()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
lastScrollTime = performance.now() + 125;
|
|
||||||
|
|
||||||
if (e.keyCode === KEY_HOME || e.keyCode === KEY_END) {
|
|
||||||
let index = e.keyCode === KEY_HOME ? 0 : buttons.length - 1;
|
|
||||||
setSelectedButton(buttons[index]);
|
|
||||||
setLastCharCode(null);
|
|
||||||
document.getElementById(buttons[index]).focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.keyCode === KEY_UP || e.keyCode === KEY_DOWN) {
|
|
||||||
let direction = 1;
|
|
||||||
if (e.keyCode === KEY_UP) direction = -1;
|
|
||||||
|
|
||||||
let index = 0;
|
|
||||||
for (index; index < buttons.length; index++) {
|
|
||||||
if (buttons[index] === selectedButton) break;
|
|
||||||
}
|
|
||||||
index += direction;
|
|
||||||
if (index < 0) index = buttons.length - 1;
|
|
||||||
else if (index >= buttons.length) index = 0;
|
|
||||||
setSelectedButton(buttons[index]);
|
|
||||||
setLastCharCode(null);
|
|
||||||
document.getElementById(buttons[index]).focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.keyCode === KEY_SPACE || e.keyCode === KEY_ENTER) {
|
|
||||||
act("choose", { choice: selectedButton });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.keyCode === KEY_ESCAPE) {
|
|
||||||
act("cancel");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const charCode = String.fromCharCode(e.keyCode).toLowerCase();
|
|
||||||
if (!charCode) return;
|
|
||||||
|
|
||||||
let foundValue;
|
|
||||||
if (charCode === lastCharCode && searchArray.length > 0) {
|
|
||||||
const nextIndex = searchIndex + 1;
|
|
||||||
|
|
||||||
if (nextIndex < searchArray.length) {
|
|
||||||
foundValue = searchArray[nextIndex];
|
|
||||||
setSearchIndex(nextIndex);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
foundValue = searchArray[0];
|
|
||||||
setSearchIndex(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const resultArray = displayedArray.filter(value =>
|
|
||||||
value.substring(0, 1).toLowerCase() === charCode
|
|
||||||
);
|
|
||||||
|
|
||||||
if (resultArray.length > 0) {
|
|
||||||
setSearchArray(resultArray);
|
|
||||||
setSearchIndex(0);
|
|
||||||
foundValue = resultArray[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foundValue) {
|
|
||||||
setLastCharCode(charCode);
|
|
||||||
setSelectedButton(foundValue);
|
|
||||||
document.getElementById(foundValue).focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Window
|
|
||||||
title={title}
|
|
||||||
theme="abstract"
|
|
||||||
width={325}
|
|
||||||
height={325}>
|
|
||||||
{timeout !== undefined && <Loader value={timeout} />}
|
|
||||||
<Window.Content>
|
|
||||||
<Stack fill vertical>
|
|
||||||
<Stack.Item grow>
|
|
||||||
<Section
|
|
||||||
fill
|
|
||||||
scrollable
|
|
||||||
autoFocus
|
|
||||||
className="ListInput__Section"
|
|
||||||
title={message}
|
|
||||||
tabIndex={0}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
buttons={(
|
|
||||||
<Button
|
|
||||||
compact
|
|
||||||
icon="search"
|
|
||||||
color="transparent"
|
|
||||||
selected={showSearchBar}
|
|
||||||
tooltip="Search Bar"
|
|
||||||
tooltipPosition="left"
|
|
||||||
onClick={() => {
|
|
||||||
setShowSearchBar(!showSearchBar);
|
|
||||||
setDisplayedArray(buttons);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}>
|
|
||||||
{displayedArray.map(button => (
|
|
||||||
<Button
|
|
||||||
key={button}
|
|
||||||
fluid
|
|
||||||
color="transparent"
|
|
||||||
id={button}
|
|
||||||
selected={selectedButton === button}
|
|
||||||
onClick={() => {
|
|
||||||
if (selectedButton === button) {
|
|
||||||
act("choose", { choice: button });
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setSelectedButton(button);
|
|
||||||
}
|
|
||||||
setLastCharCode(null);
|
|
||||||
}}>
|
|
||||||
{button}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</Section>
|
|
||||||
</Stack.Item>
|
|
||||||
{showSearchBar && (
|
|
||||||
<Stack.Item>
|
|
||||||
<Input
|
|
||||||
fluid
|
|
||||||
onInput={(e, value) => setDisplayedArray(
|
|
||||||
buttons.filter(val => (
|
|
||||||
val.toLowerCase().search(value.toLowerCase()) !== -1
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Stack.Item>
|
|
||||||
)}
|
|
||||||
<Stack.Item>
|
|
||||||
<Stack textAlign="center">
|
|
||||||
<Stack.Item grow basis={0}>
|
|
||||||
<Button
|
|
||||||
fluid
|
|
||||||
color="good"
|
|
||||||
lineHeight={2}
|
|
||||||
content="Confirm"
|
|
||||||
disabled={selectedButton === null}
|
|
||||||
onClick={() => act("choose", { choice: selectedButton })}
|
|
||||||
/>
|
|
||||||
</Stack.Item>
|
|
||||||
<Stack.Item grow basis={0}>
|
|
||||||
<Button
|
|
||||||
fluid
|
|
||||||
color="bad"
|
|
||||||
lineHeight={2}
|
|
||||||
content="Cancel"
|
|
||||||
onClick={() => act("cancel")}
|
|
||||||
/>
|
|
||||||
</Stack.Item>
|
|
||||||
</Stack>
|
|
||||||
</Stack.Item>
|
|
||||||
</Stack>
|
|
||||||
</Window.Content>
|
|
||||||
</Window>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
247
tgui/packages/tgui/interfaces/ListInputModal.tsx
Normal file
247
tgui/packages/tgui/interfaces/ListInputModal.tsx
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import { Loader } from './common/Loader';
|
||||||
|
import { InputButtons } from './common/InputButtons';
|
||||||
|
import { Button, Input, Section, Stack } from '../components';
|
||||||
|
import { useBackend, useLocalState } from '../backend';
|
||||||
|
import { KEY_A, KEY_DOWN, KEY_ESCAPE, KEY_ENTER, KEY_UP, KEY_Z } from '../../common/keycodes';
|
||||||
|
import { Window } from '../layouts';
|
||||||
|
|
||||||
|
type ListInputData = {
|
||||||
|
init_value: string;
|
||||||
|
items: string[];
|
||||||
|
large_buttons: boolean;
|
||||||
|
message: string;
|
||||||
|
timeout: number;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ListInputModal = (_, context) => {
|
||||||
|
const { act, data } = useBackend<ListInputData>(context);
|
||||||
|
const {
|
||||||
|
items = [],
|
||||||
|
message = "",
|
||||||
|
init_value,
|
||||||
|
large_buttons,
|
||||||
|
timeout,
|
||||||
|
title,
|
||||||
|
} = data;
|
||||||
|
const [selected, setSelected] = useLocalState<number>(
|
||||||
|
context,
|
||||||
|
'selected',
|
||||||
|
items.indexOf(init_value)
|
||||||
|
);
|
||||||
|
const [searchBarVisible, setSearchBarVisible] = useLocalState<boolean>(
|
||||||
|
context,
|
||||||
|
'searchBarVisible',
|
||||||
|
items.length > 9
|
||||||
|
);
|
||||||
|
const [searchQuery, setSearchQuery] = useLocalState<string>(
|
||||||
|
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 (
|
||||||
|
<Window title={title} width={325} height={windowHeight}>
|
||||||
|
{timeout && <Loader value={timeout} />}
|
||||||
|
<Window.Content
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<Section
|
||||||
|
buttons={
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
icon={searchBarVisible ? 'search' : 'font'}
|
||||||
|
selected
|
||||||
|
tooltip={
|
||||||
|
searchBarVisible
|
||||||
|
? 'Search Mode. Type to search or use arrow keys to select manually.'
|
||||||
|
: 'Hotkey Mode. Type a letter to jump to the first match. Enter to select.'
|
||||||
|
}
|
||||||
|
tooltipPosition="left"
|
||||||
|
onClick={() => onSearchBarToggle()}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
className="ListInput__Section"
|
||||||
|
fill
|
||||||
|
title={message}>
|
||||||
|
<Stack fill vertical>
|
||||||
|
<Stack.Item grow>
|
||||||
|
<ListDisplay
|
||||||
|
filteredItems={filteredItems}
|
||||||
|
onClick={onClick}
|
||||||
|
onFocusSearch={onFocusSearch}
|
||||||
|
searchBarVisible={searchBarVisible}
|
||||||
|
selected={selected}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
{searchBarVisible && (
|
||||||
|
<SearchBar
|
||||||
|
filteredItems={filteredItems}
|
||||||
|
onSearch={onSearch}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
selected={selected}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Stack.Item>
|
||||||
|
<InputButtons input={filteredItems[selected]} />
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
</Section>
|
||||||
|
</Window.Content>
|
||||||
|
</Window>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the list of selectable items.
|
||||||
|
* If a search query is provided, filters the items.
|
||||||
|
*/
|
||||||
|
const ListDisplay = (props, context) => {
|
||||||
|
const { act } = useBackend<ListInputData>(context);
|
||||||
|
const { filteredItems, onClick, onFocusSearch, searchBarVisible, selected }
|
||||||
|
= props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section fill scrollable tabIndex={0}>
|
||||||
|
{filteredItems.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
color="transparent"
|
||||||
|
fluid
|
||||||
|
id={index}
|
||||||
|
key={index}
|
||||||
|
onClick={() => onClick(index)}
|
||||||
|
onDblClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
act('submit', { entry: filteredItems[selected] });
|
||||||
|
}}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
const keyCode = window.event ? event.which : event.keyCode;
|
||||||
|
if (searchBarVisible && keyCode >= KEY_A && keyCode <= KEY_Z) {
|
||||||
|
event.preventDefault();
|
||||||
|
onFocusSearch();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
selected={index === selected}
|
||||||
|
style={{
|
||||||
|
'animation': 'none',
|
||||||
|
'transition': 'none',
|
||||||
|
}}>
|
||||||
|
{item.replace(/^\w/, (c) => c.toUpperCase())}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a search bar input.
|
||||||
|
* Closing the bar defaults input to an empty string.
|
||||||
|
*/
|
||||||
|
const SearchBar = (props, context) => {
|
||||||
|
const { act } = useBackend<ListInputData>(context);
|
||||||
|
const { filteredItems, onSearch, searchQuery, selected } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
autoFocus
|
||||||
|
autoSelect
|
||||||
|
fluid
|
||||||
|
onEnter={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
act('submit', { entry: filteredItems[selected] });
|
||||||
|
}}
|
||||||
|
onInput={(_, value) => onSearch(value)}
|
||||||
|
placeholder="Search..."
|
||||||
|
value={searchQuery}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
118
tgui/packages/tgui/interfaces/NumberInputModal.tsx
Normal file
118
tgui/packages/tgui/interfaces/NumberInputModal.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { Loader } from './common/Loader';
|
||||||
|
import { InputButtons } 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 = {
|
||||||
|
init_value: number;
|
||||||
|
large_buttons: boolean;
|
||||||
|
max_value: number | null;
|
||||||
|
message: string;
|
||||||
|
min_value: number | null;
|
||||||
|
timeout: number;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NumberInputModal = (_, context) => {
|
||||||
|
const { act, data } = useBackend<NumberInputData>(context);
|
||||||
|
const { init_value, large_buttons, message = "", timeout, title }
|
||||||
|
= data;
|
||||||
|
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
|
||||||
|
= 140
|
||||||
|
+ (message.length > 30 ? Math.ceil(message.length / 3) : 0)
|
||||||
|
+ (message.length && large_buttons ? 5 : 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Window title={title} width={270} height={windowHeight}>
|
||||||
|
{timeout && <Loader value={timeout} />}
|
||||||
|
<Window.Content
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
const keyCode = window.event ? event.which : event.keyCode;
|
||||||
|
if (keyCode === KEY_ENTER) {
|
||||||
|
act('submit', { entry: input });
|
||||||
|
}
|
||||||
|
if (keyCode === KEY_ESCAPE) {
|
||||||
|
act('cancel');
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<Section fill>
|
||||||
|
<Stack fill vertical>
|
||||||
|
<Stack.Item grow>
|
||||||
|
<Box color="label">{message}</Box>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item>
|
||||||
|
<InputArea input={input} onClick={onClick} onChange={onChange} />
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item>
|
||||||
|
<InputButtons input={input} />
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
</Section>
|
||||||
|
</Window.Content>
|
||||||
|
</Window>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Gets the user input and invalidates if there's a constraint. */
|
||||||
|
const InputArea = (props, context) => {
|
||||||
|
const { act, data } = useBackend<NumberInputData>(context);
|
||||||
|
const { min_value, max_value, init_value } = data;
|
||||||
|
const { input, onClick, onChange } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack fill>
|
||||||
|
<Stack.Item>
|
||||||
|
<Button
|
||||||
|
disabled={input === min_value}
|
||||||
|
icon="angle-double-left"
|
||||||
|
onClick={() => onClick(min_value)}
|
||||||
|
tooltip={min_value ? `Min (${min_value})` : 'Min'}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item grow>
|
||||||
|
<RestrictedInput
|
||||||
|
autoFocus
|
||||||
|
autoSelect
|
||||||
|
fluid
|
||||||
|
minValue={min_value}
|
||||||
|
maxValue={max_value}
|
||||||
|
onChange={(_, value) => onChange(value)}
|
||||||
|
onEnter={(_, value) => act('submit', { entry: value })}
|
||||||
|
value={input}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item>
|
||||||
|
<Button
|
||||||
|
disabled={input === max_value}
|
||||||
|
icon="angle-double-right"
|
||||||
|
onClick={() => onClick(max_value)}
|
||||||
|
tooltip={max_value ? `Max (${max_value})` : 'Max'}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item>
|
||||||
|
<Button
|
||||||
|
disabled={input === init_value}
|
||||||
|
icon="redo"
|
||||||
|
onClick={() => onClick(init_value)}
|
||||||
|
tooltip={init_value ? `Reset (${init_value})` : 'Reset'}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
103
tgui/packages/tgui/interfaces/TextInputModal.tsx
Normal file
103
tgui/packages/tgui/interfaces/TextInputModal.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { Loader } from './common/Loader';
|
||||||
|
import { InputButtons } from './common/InputButtons';
|
||||||
|
import { useBackend, useLocalState } from '../backend';
|
||||||
|
import { KEY_ENTER, KEY_ESCAPE } from '../../common/keycodes';
|
||||||
|
import { Box, Section, Stack, TextArea } from '../components';
|
||||||
|
import { Window } from '../layouts';
|
||||||
|
|
||||||
|
type TextInputData = {
|
||||||
|
large_buttons: boolean;
|
||||||
|
max_length: number;
|
||||||
|
message: string;
|
||||||
|
multiline: boolean;
|
||||||
|
placeholder: string;
|
||||||
|
timeout: number;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TextInputModal = (_, context) => {
|
||||||
|
const { act, data } = useBackend<TextInputData>(context);
|
||||||
|
const {
|
||||||
|
large_buttons,
|
||||||
|
max_length,
|
||||||
|
message = "",
|
||||||
|
multiline,
|
||||||
|
placeholder,
|
||||||
|
timeout,
|
||||||
|
title,
|
||||||
|
} = data;
|
||||||
|
const [input, setInput] = useLocalState<string>(
|
||||||
|
context,
|
||||||
|
'input',
|
||||||
|
placeholder || ''
|
||||||
|
);
|
||||||
|
const onType = (value: string) => {
|
||||||
|
if (value === input) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInput(value);
|
||||||
|
};
|
||||||
|
// Dynamically changes the window height based on the message.
|
||||||
|
const windowHeight
|
||||||
|
= 135
|
||||||
|
+ (message.length > 30 ? Math.ceil(message.length / 4) : 0)
|
||||||
|
+ (multiline || input.length >= 30 ? 75 : 0)
|
||||||
|
+ (message.length && large_buttons ? 5 : 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Window title={title} width={325} height={windowHeight}>
|
||||||
|
{timeout && <Loader value={timeout} />}
|
||||||
|
<Window.Content
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
const keyCode = window.event ? event.which : event.keyCode;
|
||||||
|
if (keyCode === KEY_ENTER) {
|
||||||
|
act('submit', { entry: input });
|
||||||
|
}
|
||||||
|
if (keyCode === KEY_ESCAPE) {
|
||||||
|
act('cancel');
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<Section fill>
|
||||||
|
<Stack fill vertical>
|
||||||
|
<Stack.Item>
|
||||||
|
<Box color="label">{message}</Box>
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item grow>
|
||||||
|
<InputArea input={input} onType={onType} />
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item>
|
||||||
|
<InputButtons
|
||||||
|
input={input}
|
||||||
|
message={`${input.length}/${max_length}`}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
</Section>
|
||||||
|
</Window.Content>
|
||||||
|
</Window>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Gets the user input and invalidates if there's a constraint. */
|
||||||
|
const InputArea = (props, context) => {
|
||||||
|
const { act, data } = useBackend<TextInputData>(context);
|
||||||
|
const { max_length, multiline } = data;
|
||||||
|
const { input, onType } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextArea
|
||||||
|
autoFocus
|
||||||
|
autoSelect
|
||||||
|
height={multiline || input.length >= 30 ? '100%' : '1.8rem'}
|
||||||
|
maxLength={max_length}
|
||||||
|
onEscape={() => act('cancel')}
|
||||||
|
onEnter={(event) => {
|
||||||
|
act('submit', { entry: input });
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
onInput={(_, value) => onType(value)}
|
||||||
|
placeholder="Type something..."
|
||||||
|
value={input}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
75
tgui/packages/tgui/interfaces/common/InputButtons.tsx
Normal file
75
tgui/packages/tgui/interfaces/common/InputButtons.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { useBackend } from '../../backend';
|
||||||
|
import { Box, Button, Flex } from '../../components';
|
||||||
|
|
||||||
|
type InputButtonsData = {
|
||||||
|
large_buttons: boolean;
|
||||||
|
swapped_buttons: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type InputButtonsProps = {
|
||||||
|
input: string | number;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InputButtons = (props: InputButtonsProps, context) => {
|
||||||
|
const { act, data } = useBackend<InputButtonsData>(context);
|
||||||
|
const { large_buttons, swapped_buttons } = data;
|
||||||
|
const { input, message } = props;
|
||||||
|
const submitButton = (
|
||||||
|
<Button
|
||||||
|
color="good"
|
||||||
|
fluid={!!large_buttons}
|
||||||
|
height={!!large_buttons && 2}
|
||||||
|
onClick={() => act('submit', { entry: input })}
|
||||||
|
m={0.5}
|
||||||
|
pl={2}
|
||||||
|
pr={2}
|
||||||
|
pt={large_buttons ? 0.33 : 0}
|
||||||
|
textAlign="center"
|
||||||
|
tooltip={large_buttons && message}
|
||||||
|
width={!large_buttons && 6}>
|
||||||
|
{large_buttons ? 'SUBMIT' : 'Submit'}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
const cancelButton = (
|
||||||
|
<Button
|
||||||
|
color="bad"
|
||||||
|
fluid={!!large_buttons}
|
||||||
|
height={!!large_buttons && 2}
|
||||||
|
onClick={() => act('cancel')}
|
||||||
|
m={0.5}
|
||||||
|
pl={2}
|
||||||
|
pr={2}
|
||||||
|
pt={large_buttons ? 0.33 : 0}
|
||||||
|
textAlign="center"
|
||||||
|
width={!large_buttons && 6}>
|
||||||
|
{large_buttons ? 'CANCEL' : 'Cancel'}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
direction={!swapped_buttons ? 'row' : 'row-reverse'}
|
||||||
|
fill
|
||||||
|
justify="space-around">
|
||||||
|
{large_buttons ? (
|
||||||
|
<Flex.Item grow>{cancelButton}</Flex.Item>
|
||||||
|
) : (
|
||||||
|
<Flex.Item>{cancelButton}</Flex.Item>
|
||||||
|
)}
|
||||||
|
{!large_buttons && message && (
|
||||||
|
<Flex.Item>
|
||||||
|
<Box color="label" textAlign="center">
|
||||||
|
{message}
|
||||||
|
</Box>
|
||||||
|
</Flex.Item>
|
||||||
|
)}
|
||||||
|
{large_buttons ? (
|
||||||
|
<Flex.Item grow>{submitButton}</Flex.Item>
|
||||||
|
) : (
|
||||||
|
<Flex.Item>{submitButton}</Flex.Item>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { selectBackend } from './backend';
|
import { selectBackend } from './backend';
|
||||||
|
import { Icon, Stack } from './components';
|
||||||
import { selectDebug } from './debug/selectors';
|
import { selectDebug } from './debug/selectors';
|
||||||
import { Window } from './layouts';
|
import { Window } from './layouts';
|
||||||
|
|
||||||
@@ -33,12 +34,32 @@ const SuspendedWindow = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RefreshingWindow = () => {
|
||||||
|
return (
|
||||||
|
<Window height={130} title="Loading" width={150}>
|
||||||
|
<Window.Content>
|
||||||
|
<Stack align="center" fill justify="center" vertical>
|
||||||
|
<Stack.Item>
|
||||||
|
<Icon color="blue" name="toolbox" spin size={4} />
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item>
|
||||||
|
Please wait...
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
</Window.Content>
|
||||||
|
</Window>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const getRoutedComponent = store => {
|
export const getRoutedComponent = store => {
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
const { suspended, config } = selectBackend(state);
|
const { suspended, config } = selectBackend(state);
|
||||||
if (suspended) {
|
if (suspended) {
|
||||||
return SuspendedWindow;
|
return SuspendedWindow;
|
||||||
}
|
}
|
||||||
|
if (config.refreshing) {
|
||||||
|
return RefreshingWindow;
|
||||||
|
}
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
const debug = selectDebug(state);
|
const debug = selectDebug(state);
|
||||||
// Show a kitchen sink
|
// Show a kitchen sink
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
#include "code\__defines\chemistry_vr.dm"
|
#include "code\__defines\chemistry_vr.dm"
|
||||||
#include "code\__defines\color.dm"
|
#include "code\__defines\color.dm"
|
||||||
#include "code\__defines\construction.dm"
|
#include "code\__defines\construction.dm"
|
||||||
|
#include "code\__defines\cooldowns.dm"
|
||||||
#include "code\__defines\crafting.dm"
|
#include "code\__defines\crafting.dm"
|
||||||
#include "code\__defines\damage_organs.dm"
|
#include "code\__defines\damage_organs.dm"
|
||||||
#include "code\__defines\dna.dm"
|
#include "code\__defines\dna.dm"
|
||||||
@@ -4191,9 +4192,6 @@
|
|||||||
#include "code\modules\tgui\modal.dm"
|
#include "code\modules\tgui\modal.dm"
|
||||||
#include "code\modules\tgui\states.dm"
|
#include "code\modules\tgui\states.dm"
|
||||||
#include "code\modules\tgui\tgui.dm"
|
#include "code\modules\tgui\tgui.dm"
|
||||||
#include "code\modules\tgui\tgui_alert.dm"
|
|
||||||
#include "code\modules\tgui\tgui_input_list.dm"
|
|
||||||
#include "code\modules\tgui\tgui_input_text.dm"
|
|
||||||
#include "code\modules\tgui\tgui_window.dm"
|
#include "code\modules\tgui\tgui_window.dm"
|
||||||
#include "code\modules\tgui\modules\_base.dm"
|
#include "code\modules\tgui\modules\_base.dm"
|
||||||
#include "code\modules\tgui\modules\admin_shuttle_controller.dm"
|
#include "code\modules\tgui\modules\admin_shuttle_controller.dm"
|
||||||
@@ -4238,6 +4236,10 @@
|
|||||||
#include "code\modules\tgui\states\self.dm"
|
#include "code\modules\tgui\states\self.dm"
|
||||||
#include "code\modules\tgui\states\vorepanel_vr.dm"
|
#include "code\modules\tgui\states\vorepanel_vr.dm"
|
||||||
#include "code\modules\tgui\states\zlevel.dm"
|
#include "code\modules\tgui\states\zlevel.dm"
|
||||||
|
#include "code\modules\tgui_input\alert.dm"
|
||||||
|
#include "code\modules\tgui_input\list.dm"
|
||||||
|
#include "code\modules\tgui_input\number.dm"
|
||||||
|
#include "code\modules\tgui_input\text.dm"
|
||||||
#include "code\modules\tooltip\tooltip.dm"
|
#include "code\modules\tooltip\tooltip.dm"
|
||||||
#include "code\modules\turbolift\_turbolift.dm"
|
#include "code\modules\turbolift\_turbolift.dm"
|
||||||
#include "code\modules\turbolift\turbolift.dm"
|
#include "code\modules\turbolift\turbolift.dm"
|
||||||
|
|||||||
Reference in New Issue
Block a user