Adds tgui-style input modals for text, message, and num

This commit is contained in:
Chompstation Bot
2021-07-04 04:24:41 +00:00
parent 23e39aa550
commit f32c5af649
11 changed files with 465 additions and 9 deletions

View File

@@ -100,7 +100,7 @@
return TOPIC_REFRESH
else if(href_list["nickname"])
var/raw_nickname = input(user, "Choose your character's nickname:", "Character Nickname") as text|null
var/raw_nickname = tgui_input_text(user, "Choose your character's nickname:", "Character Nickname", pref.nickname)
if (!isnull(raw_nickname) && CanUseTopic(user))
var/new_nickname = sanitize_name(raw_nickname, pref.species, is_FBP())
if(new_nickname)
@@ -140,7 +140,7 @@
return TOPIC_REFRESH
else if(href_list["metadata"])
var/new_metadata = sanitize(input(user, "Enter any information you'd like others to see, such as Roleplay-preferences:", "Game Preference" , html_decode(pref.metadata)) as message|null, extra = 0) //VOREStation Edit
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
if(new_metadata && CanUseTopic(user))
pref.metadata = new_metadata
return TOPIC_REFRESH

View File

@@ -87,19 +87,19 @@
return TOPIC_REFRESH
else if(href_list["weight_gain"])
var/weight_gain_rate = input(user, "Choose your character's rate of weight gain between 100% \
var/weight_gain_rate = tgui_input_num(user, "Choose your character's rate of weight gain between 100% \
(full realism body fat gain) and 0% (no body fat gain).\n\
(If you want to disable weight gain, set this to 0.01 to round it to 0%.)\
([WEIGHT_CHANGE_MIN]-[WEIGHT_CHANGE_MAX])", "Character Preference") as num|null
([WEIGHT_CHANGE_MIN]-[WEIGHT_CHANGE_MAX])", "Character Preference", pref.weight_gain)
if(weight_gain_rate)
pref.weight_gain = round(text2num(weight_gain_rate),1)
return TOPIC_REFRESH
else if(href_list["weight_loss"])
var/weight_loss_rate = input(user, "Choose your character's rate of weight loss between 100% \
var/weight_loss_rate = tgui_input_num(user, "Choose your character's rate of weight loss between 100% \
(full realism body fat loss) and 0% (no body fat loss).\n\
(If you want to disable weight loss, set this to 0.01 round it to 0%.)\
([WEIGHT_CHANGE_MIN]-[WEIGHT_CHANGE_MAX])", "Character Preference") as num|null
([WEIGHT_CHANGE_MIN]-[WEIGHT_CHANGE_MAX])", "Character Preference", pref.weight_loss)
if(weight_loss_rate)
pref.weight_loss = round(text2num(weight_loss_rate),1)
return TOPIC_REFRESH

View File

@@ -0,0 +1,299 @@
/**
* 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

View File

@@ -104,6 +104,9 @@ export class TextArea extends Component {
if (input) {
input.value = toInputValue(nextValue);
}
if (this.props.autoFocus) {
setTimeout(() => input.focus(), 1);
}
}
componentDidUpdate(prevProps, prevState) {

View File

@@ -0,0 +1,116 @@
/**
* @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
onKeyDown={handleKeyDown}
onChange={(_e, val) => {
setCurValue(val);
}}
/>
);
break;
}
return (
<Window title={title} 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>
);
};

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2020 bobbahbrown (https://github.com/bobbahbrown)
* SPDX-License-Identifier: MIT
*/
@use '../colors.scss';
@use '../base.scss';
.InputModal__Section .Section__title{
flex-shrink: 0;
}
.InputModal__Section .Section__titleText {
font-size: base.em(12px);
white-space: pre-line;
}
.InputModal__Loader {
width: 100%;
position: relative;
height: 4px;
}
.InputModal__LoaderProgress {
position: absolute;
transition: background-color 500ms ease-out, width 500ms ease-out;
background-color: colors.bg(colors.$primary);
height: 100%;
}

View File

@@ -45,6 +45,7 @@
// Interfaces
@include meta.load-css('./interfaces/ListInput.scss');
@include meta.load-css('./interfaces/InputModal.scss');
@include meta.load-css('./interfaces/IntegratedCircuit.scss');
@include meta.load-css('./interfaces/AlertModal.scss');
@include meta.load-css('./interfaces/CameraConsole.scss');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4072,6 +4072,7 @@
#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\modules\_base.dm"
#include "code\modules\tgui\modules\admin_shuttle_controller.dm"