Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tgui input list #56065

Merged
merged 16 commits into from
Jan 10, 2021
2 changes: 1 addition & 1 deletion code/game/machinery/hologram.dm
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ Possible to do for anyone motivated enough:
if(A)
LAZYADD(callnames[A], I)
callnames -= get_area(src)
var/result = input(usr, "Choose an area to call", "Holocall") as null|anything in sortNames(callnames)
var/result = tgui_input_list(usr, "Choose an area to call", "Holocall", sortNames(callnames))
if(QDELETED(usr) || !result || outgoing_call)
return
if(usr.loc == loc)
Expand Down
2 changes: 1 addition & 1 deletion code/modules/admin/admin_verbs.dm
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ GLOBAL_PROTECT(admin_verbs_hideable)
set desc = "Cause an explosion of varying strength at your location."

var/list/choices = list("Small Bomb (1, 2, 3, 3)", "Medium Bomb (2, 3, 4, 4)", "Big Bomb (3, 5, 7, 5)", "Maxcap", "Custom Bomb")
var/choice = input("What size explosion would you like to produce? NOTE: You can do all this rapidly and in an IC manner (using cruise missiles!) with the Config/Launch Supplypod verb. WARNING: These ignore the maxcap") as null|anything in choices
var/choice = tgui_input_list(usr, "What size explosion would you like to produce? NOTE: You can do all this rapidly and in an IC manner (using cruise missiles!) with the Config/Launch Supplypod verb. WARNING: These ignore the maxcap", "Drop Bomb", choices)
var/turf/epicenter = mob.loc

switch(choice)
Expand Down
174 changes: 174 additions & 0 deletions code/modules/tgui/tgui_input_list.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/**
* Creates a TGUI input list window and returns the user's response.
*
* This proc should be used to create alerts that the caller will wait for a response from.
* Arguments:
* * user - The user to show the input box to.
* * message - The content of the input box, shown in the body of the TGUI window.
* * title - The title of the input box, shown on the top of the TGUI window.
* * buttons - The options that can be chosen by the user, each string is assigned a button on the UI.
* * 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, timeout = 0)
if (!user)
user = usr
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return
var/datum/tgui_list_input/input = new(user, message, title, buttons, timeout)
input.ui_interact(user)
input.wait()
if (input)
. = input.choice
qdel(input)

/**
* Creates an asynchronous TGUI input list window with an associated callback.
*
* This proc should be used to create inputs that invoke a callback with the user's chosen option.
* Arguments:
* * user - The user to show the input box to.
* * message - The content of the input box, shown in the body of the TGUI window.
* * title - The title of the input box, shown on the top of the TGUI window.
* * 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 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, datum/callback/callback, timeout = 60 SECONDS)
if (!user)
user = usr
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return
var/datum/tgui_list_input/async/input = new(user, message, title, buttons, callback, timeout)
input.ui_interact(user)

/**
* # tgui_list_input
*
* Datum used for instantiating and using a TGUI-controlled list input that prompts the user with
* a message and shows a list of selectable options
*/
/datum/tgui_list_input
/// The title of the TGUI window
var/title
/// The textual body of the TGUI window
var/message
/// The list of buttons (responses) provided on the TGUI window
var/list/buttons
/// The button that the user has pressed, null if no selection has been made
var/choice
/// The time at which the tgui_list_input was created, for displaying timeout progress.
var/start_time
/// The lifespan of the tgui_list_input, after which the window will close and delete itself.
var/timeout
/// Boolean field describing if the tgui_list_input was closed by the user.
var/closed

/datum/tgui_list_input/New(mob/user, message, title, list/buttons, timeout)
src.title = title
src.message = message
src.buttons = list()

for(var/i in buttons)
src.buttons += i

// need to do this because byond macros are removed on json_encode
// the value of the buttons need to match the parameters in ui_act
src.buttons = json_decode(json_encode(src.buttons))

if (timeout)
src.timeout = timeout
start_time = world.time
QDEL_IN(src, timeout)

/datum/tgui_list_input/Destroy(force, ...)
SStgui.close_uis(src)
QDEL_NULL(buttons)
. = ..()

/**
* Waits for a user's response to the tgui_list_input's prompt before returning. Returns early if
* the window was closed by the user.
*/
/datum/tgui_list_input/proc/wait()
while (!choice && !closed)
stoplag(1)

/datum/tgui_list_input/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "ListInput")
ui.open()

/datum/tgui_list_input/ui_close(mob/user)
. = ..()
closed = TRUE

/datum/tgui_list_input/ui_state(mob/user)
return GLOB.always_state

/datum/tgui_list_input/ui_static_data(mob/user)
. = list(
"title" = title,
"message" = message,
"buttons" = buttons
)

/datum/tgui_list_input/ui_data(mob/user)
. = list()
if(timeout)
.["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1)

/datum/tgui_list_input/ui_act(action, list/params)
. = ..()
if (.)
return
switch(action)
if("choose")
if (!(params["choice"] in buttons))
return
choice = params["choice"]
SStgui.close_uis(src)
return TRUE
if("cancel")
SStgui.close_uis(src)
closed = TRUE
return TRUE

/**
* # async tgui_list_input
*
* An asynchronous version of tgui_list_input to be used with callbacks instead of waiting on user responses.
*/
/datum/tgui_list_input/async
/// The callback to be invoked by the tgui_list_input upon having a choice made.
var/datum/callback/callback

/datum/tgui_list_input/async/New(mob/user, message, title, list/buttons, callback, timeout)
..(user, title, message, buttons, timeout)
src.callback = callback

/datum/tgui_list_input/async/Destroy(force, ...)
QDEL_NULL(callback)
. = ..()

/datum/tgui_list_input/async/ui_close(mob/user)
. = ..()
qdel(src)

/datum/tgui_list_input/async/ui_act(action, list/params)
. = ..()
if (!. || choice == null)
return
callback.InvokeAsync(choice)
qdel(src)

/datum/tgui_list_input/async/wait()
return
1 change: 1 addition & 0 deletions tgstation.dme
Original file line number Diff line number Diff line change
Expand Up @@ -3196,6 +3196,7 @@
#include "code\modules\tgui\states.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_window.dm"
#include "code\modules\tgui\states\admin.dm"
#include "code\modules\tgui\states\always.dm"
Expand Down
173 changes: 173 additions & 0 deletions tgui/packages/tgui/interfaces/ListInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/**
* @file
* @copyright 2020 watermelon914 (https://github.com/watermelon914)
* @license MIT
*/

import { clamp01 } from 'common/math';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Flex, Section, Input } from '../components';
import { Window } from '../layouts';

let lastScrollTime = 0;

export const ListInput = (props, context) => {
const { act, data } = useBackend(context);
const {
title,
message,
buttons,
timeout,
} = 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", null);
return (
<Window
title={title}
width={325}
height={325}
>
{timeout !== undefined && <Loader value={timeout} />}
<Window.Content>
<Section fill>
<Flex
Watermelon914 marked this conversation as resolved.
Show resolved Hide resolved
height={showSearchBar? "80%" : "90%"}
>
<Section
scrollable
width="100%"
fill
title={message}
tabIndex={0}
onKeyDown={e => {
const charCode = String.fromCharCode(e.keyCode);
if (!charCode || lastScrollTime > performance.now()) return;
lastScrollTime = performance.now() + 150;

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) === String.fromCharCode(e.keyCode)
);

if (resultArray.length > 0) {
setSearchArray(resultArray);
setSearchIndex(0);
foundValue = resultArray[0];
}
}

if (foundValue) {
setLastCharCode(charCode);
setSelectedButton(foundValue);
document.getElementById(foundValue).focus();
}
}}
buttons={(
<Button
icon="search"
color="transparent"
selected={showSearchBar}
tooltip="Search Bar"
tooltipPosition="left"
onClick={() => {
setShowSearchBar(!showSearchBar);
setDisplayedArray(buttons);
}}
compact
/>
)}
level={2}
Watermelon914 marked this conversation as resolved.
Show resolved Hide resolved
>
<Flex
wrap="wrap"
>
{displayedArray.map(button => (
<Flex.Item key={button} basis="100%">
<Button
color="transparent"
content={button}
id={button}
width="100%"
selected={selectedButton === button}
onClick={() => {
(selectedButton === button)
? act("choose", { choice: button }): setSelectedButton(button);
setLastCharCode(null);
}}
/>
</Flex.Item>
))}
</Flex>
</Section>
</Flex>
{showSearchBar && (
<Flex width="100%" mt={1}>
<Input
width="100%"
onInput={(e, value) => setDisplayedArray(
buttons.filter(val =>
val.toLowerCase().search(value.toLowerCase()) !== -1
)
)}
/>
</Flex>
)}
<Flex width="100%" mt={1}>
<Button
height="100%"
width="100%"
color="good"
content="Confirm"
disabled={selectedButton === null}
onClick={() => act("choose", { choice: selectedButton })}
/>
<Button
height="100%"
width="100%"
color="bad"
content="Cancel"
onClick={() => act("cancel")}
/>
</Flex>
</Section>
</Window.Content>
</Window>
);
};

export const Loader = props => {
const { value } = props;
return (
<div
className="ListInput__Loader">
<Box
className="ListInput__LoaderProgress"
style={{
width: clamp01(value) * 100 + '%',
}} />
</div>
);
};
19 changes: 19 additions & 0 deletions tgui/packages/tgui/styles/interfaces/ListInput.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Copyright (c) 2020 bobbahbrown (https://github.com/bobbahbrown)
* SPDX-License-Identifier: MIT
*/

@use '../colors.scss';

.ListInput__Loader {
width: 100%;
position: relative;
height: 4px;
}

.ListInput__LoaderProgress {
position: absolute;
transition: background-color 500ms, width 500ms;
background-color: colors.bg(colors.$primary);
height: 100%;
}
Loading