Skip to content

Commit

Permalink
[PORT] Bindable action buttons (#2488)
Browse files Browse the repository at this point in the history
* bindable action buttons (#81371)

you may alt-click action buttons to bind them to a key
these are subject to click cooldown and if an action successfully
triggers click cooldown is triggered so you cant instantly do multiple
alt-click again to unbind

moving your mouse to the top left corner to do combat is not good
gameplay

:cl:
qol: you may altclick action buttons to bind them to a key
/:cl:

---------

Co-authored-by: MrMelbert <[email protected]>

* Port action.dm refactor from tgstation/tgstation#79009

---------

Co-authored-by: jimmyl <[email protected]>
Co-authored-by: MrMelbert <[email protected]>
  • Loading branch information
3 people authored Jul 1, 2024
1 parent 38c34c8 commit dc842f9
Show file tree
Hide file tree
Showing 9 changed files with 352 additions and 33 deletions.
3 changes: 3 additions & 0 deletions code/__DEFINES/dcs/signals/signals_mob/signals_mob_main.dm
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,6 @@

/// from /mob/proc/change_mob_type_unchecked() : ()
#define COMSIG_MOB_CHANGED_TYPE "mob_changed_type"

/// from /mob/proc/key_down(): (key, client/client, full_key)
#define COMSIG_MOB_KEYDOWN "mob_key_down"
24 changes: 23 additions & 1 deletion code/_onclick/hud/action_button.dm
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
/// A weakref of the last thing we hovered over
/// God I hate how dragging works
var/datum/weakref/last_hovored_ref
/// overlay for keybind maptext
var/mutable_appearance/keybind_maptext

/atom/movable/screen/movable/action_button/Destroy()
if(our_hud)
Expand Down Expand Up @@ -48,6 +50,9 @@
return FALSE

var/list/modifiers = params2list(params)
if(LAZYACCESS(modifiers, ALT_CLICK))
begin_creating_bind(usr)
return TRUE
if(LAZYACCESS(modifiers, SHIFT_CLICK))
var/datum/hud/our_hud = usr.hud_used
our_hud.position_action(src, SCRN_OBJ_DEFAULT)
Expand All @@ -61,6 +66,14 @@
linked_action.Trigger(trigger_flags = trigger_flags)
return TRUE

/atom/movable/screen/movable/action_button/proc/begin_creating_bind(mob/user)
if(!isnull(linked_action.full_key))
linked_action.full_key = null
linked_action.update_button_status(src)
return
linked_action.full_key = tgui_input_keycombo(user, "Please bind a key for this action.")
linked_action.update_button_status(src)

// Entered and Exited won't fire while you're dragging something, because you're still "holding" it
// Very much byond logic, but I want nice behavior, so we fake it with drag
/atom/movable/screen/movable/action_button/MouseDrag(atom/over_object, src_location, over_location, src_control, over_control, params)
Expand Down Expand Up @@ -149,6 +162,15 @@
return
user.client.prefs.action_buttons_screen_locs -= "[name]_[id]"

/atom/movable/screen/movable/action_button/proc/update_keybind_maptext(key)
cut_overlay(keybind_maptext)
if(!key)
return
keybind_maptext = new
keybind_maptext.maptext = MAPTEXT("<span style='text-align: right'>[key]</span>")
keybind_maptext.transform = keybind_maptext.transform.Translate(-4, length(key) > 1 ? -6 : 2) //with modifiers, its placed lower so cooldown is visible
add_overlay(keybind_maptext)

/**
* This is a silly proc used in hud code code to determine what icon and icon state we should be using
* for hud elements (such as action buttons) that don't have their own icon and icon state set.
Expand Down Expand Up @@ -241,7 +263,7 @@
action.HideFrom(src)

/atom/movable/screen/button_palette
desc = "<b>Drag</b> buttons to move them<br><b>Shift-click</b> any button to reset it<br><b>Alt-click</b> this to reset all buttons"
desc = "<b>Drag</b> buttons to move them<br><b>Shift-click</b> any button to reset it<br><b>Alt-click any button</b> to begin binding it to a key<br><b>Alt-click this</b> to reset all buttons"
icon = 'icons/hud/64x16_actions.dmi'
icon_state = "screen_gen_palette"
screen_loc = ui_action_palette
Expand Down
76 changes: 48 additions & 28 deletions code/datums/actions/action.dm
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
/// This is the icon state for any FOREGROUND overlay icons on the button (such as borders)
var/overlay_icon_state

/// full key we are bound to
var/full_key

/datum/action/New(Target)
link_to(Target)

Expand Down Expand Up @@ -79,16 +82,17 @@

/// Grants the action to the passed mob, making it the owner
/datum/action/proc/Grant(mob/grant_to)
if(!grant_to)
if(isnull(grant_to))
Remove(owner)
return
if(owner)
if(owner == grant_to)
return
Remove(owner)
SEND_SIGNAL(src, COMSIG_ACTION_GRANTED, grant_to)
SEND_SIGNAL(grant_to, COMSIG_MOB_GRANTED_ACTION, src)
if(grant_to == owner)
return // We already have it
var/mob/previous_owner = owner
owner = grant_to
if(!isnull(previous_owner))
Remove(previous_owner)
SEND_SIGNAL(src, COMSIG_ACTION_GRANTED, owner)
SEND_SIGNAL(owner, COMSIG_MOB_GRANTED_ACTION, src)
RegisterSignal(owner, COMSIG_QDELETING, PROC_REF(clear_ref), override = TRUE)

// Register some signals based on our check_flags
Expand All @@ -107,6 +111,7 @@
RegisterSignals(owner, list(SIGNAL_ADDTRAIT(TRAIT_MAGICALLY_PHASED), SIGNAL_REMOVETRAIT(TRAIT_MAGICALLY_PHASED)), PROC_REF(update_status_on_signal))

if(owner_has_control)
RegisterSignal(grant_to, COMSIG_MOB_KEYDOWN, PROC_REF(keydown), override = TRUE)
GiveAction(grant_to)

/// Remove the passed mob from being owner of our action
Expand All @@ -119,28 +124,31 @@
HideFrom(hud.mymob)
LAZYREMOVE(remove_from?.actions, src) // We aren't always properly inserted into the viewers list, gotta make sure that action's cleared
viewers = list()
UnregisterSignal(remove_from, COMSIG_MOB_KEYDOWN)

if(owner)
SEND_SIGNAL(src, COMSIG_ACTION_REMOVED, owner)
SEND_SIGNAL(owner, COMSIG_MOB_REMOVED_ACTION, src)
UnregisterSignal(owner, COMSIG_QDELETING)

// Clean up our check_flag signals
UnregisterSignal(owner, list(
COMSIG_LIVING_SET_BODY_POSITION,
COMSIG_MOB_STATCHANGE,
SIGNAL_ADDTRAIT(TRAIT_HANDS_BLOCKED),
SIGNAL_ADDTRAIT(TRAIT_IMMOBILIZED),
SIGNAL_ADDTRAIT(TRAIT_INCAPACITATED),
SIGNAL_ADDTRAIT(TRAIT_MAGICALLY_PHASED),
SIGNAL_REMOVETRAIT(TRAIT_HANDS_BLOCKED),
SIGNAL_REMOVETRAIT(TRAIT_IMMOBILIZED),
SIGNAL_REMOVETRAIT(TRAIT_INCAPACITATED),
SIGNAL_REMOVETRAIT(TRAIT_MAGICALLY_PHASED),
))

if(target == owner)
RegisterSignal(target, COMSIG_QDELETING, PROC_REF(clear_ref))
if(isnull(owner))
return
SEND_SIGNAL(src, COMSIG_ACTION_REMOVED, owner)
SEND_SIGNAL(owner, COMSIG_MOB_REMOVED_ACTION, src)
UnregisterSignal(owner, COMSIG_QDELETING)

// Clean up our check_flag signals
UnregisterSignal(owner, list(
COMSIG_LIVING_SET_BODY_POSITION,
COMSIG_MOB_STATCHANGE,
SIGNAL_ADDTRAIT(TRAIT_HANDS_BLOCKED),
SIGNAL_ADDTRAIT(TRAIT_IMMOBILIZED),
SIGNAL_ADDTRAIT(TRAIT_INCAPACITATED),
SIGNAL_ADDTRAIT(TRAIT_MAGICALLY_PHASED),
SIGNAL_REMOVETRAIT(TRAIT_HANDS_BLOCKED),
SIGNAL_REMOVETRAIT(TRAIT_IMMOBILIZED),
SIGNAL_REMOVETRAIT(TRAIT_INCAPACITATED),
SIGNAL_REMOVETRAIT(TRAIT_MAGICALLY_PHASED),
))

if(target == owner)
RegisterSignal(target, COMSIG_QDELETING, PROC_REF(clear_ref))
if (owner == remove_from)
owner = null

/// Actually triggers the effects of the action.
Expand Down Expand Up @@ -304,6 +312,7 @@
* force - whether an update is forced regardless of existing status
*/
/datum/action/proc/update_button_status(atom/movable/screen/movable/action_button/current_button, force = FALSE)
current_button.update_keybind_maptext(full_key)
if(IsAvailable())
current_button?.color = rgb(255,255,255,255)
else
Expand Down Expand Up @@ -405,3 +414,14 @@
/// Checks if our action is actively selected. Used for selecting icons primarily.
/datum/action/proc/is_action_active(atom/movable/screen/movable/action_button/current_button)
return FALSE

/datum/action/proc/keydown(mob/source, key, client/client, full_key)
SIGNAL_HANDLER
if(isnull(full_key) || full_key != src.full_key)
return
if(istype(source))
if(source.next_click > world.time)
return
else
source.next_click = world.time + CLICK_CD_RANGE
INVOKE_ASYNC(src, PROC_REF(Trigger))
5 changes: 2 additions & 3 deletions code/modules/keybindings/bindings_client.dm
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,10 @@
if(kb.can_use(src) && kb.down(src) && keycount >= MAX_COMMANDS_PER_KEY)
break

holder?.key_down(_key, src)
mob.focus?.key_down(_key, src)
holder?.key_down(_key, src, full_key)
mob.focus?.key_down(_key, src, full_key)
mob.update_mouse_pointer()


/client/verb/keyUp(_key as text)
set instant = TRUE
set hidden = TRUE
Expand Down
3 changes: 2 additions & 1 deletion code/modules/keybindings/setup.dm
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Set a client's focus to an object and override these procs on that object to let it handle keypresses

/datum/proc/key_down(key, client/user) // Called when a key is pressed down initially
/datum/proc/key_down(key, client/user, full_key) // Called when a key is pressed down initially
SHOULD_CALL_PARENT(TRUE)
return
/datum/proc/key_up(key, client/user) // Called when a key is released
return
Expand Down
4 changes: 4 additions & 0 deletions code/modules/mob/mob.dm
Original file line number Diff line number Diff line change
Expand Up @@ -1554,3 +1554,7 @@
set name = "View Skills"

mind?.print_levels(src)

/mob/key_down(key, client/client, full_key)
..()
SEND_SIGNAL(src, COMSIG_MOB_KEYDOWN, key, client, full_key)
126 changes: 126 additions & 0 deletions code/modules/tgui_input/keycombo.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* Creates a TGUI window with a key input. Returns the user's response as a full key with modifiers, eg ShiftK.
*
* This proc should be used to create windows for key entry that the caller will wait for a response from.
* If tgui fancy chat is turned off: Will return a normal input.
*
* 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) key, shown as a placeholder.
*/
/proc/tgui_input_keycombo(mob/user = usr, message, title = "Key Input", default = 0, timeout = 0, ui_state = GLOB.always_state)
if (!istype(user))
if (istype(user, /client))
var/client/client = user
user = client.mob
else
return null

if (isnull(user.client))
return null

// Client does NOT have tgui_input on: Returns regular input
if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
var/input_key = input(user, message, title + "(Modifiers are TGUI only, sorry!)", default) as null|text
return input_key[1]
var/datum/tgui_input_keycombo/key_input = new(user, message, title, default, timeout, ui_state)
key_input.ui_interact(user)
key_input.wait()
if (key_input)
. = key_input.entry
qdel(key_input)

/**
* # tgui_input_keycombo
*
* Datum used for instantiating and using a TGUI-controlled key input that prompts the user with
* a message and listens for key presses.
*/
/datum/tgui_input_keycombo
/// 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 prompt's body, if any, of the TGUI window.
var/message
/// 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
/// The TGUI UI state that will be returned in ui_state(). Default: always_state
var/datum/ui_state/state

/datum/tgui_input_keycombo/New(mob/user, message, title, default, timeout, ui_state)
src.default = default
src.message = message
src.title = title
src.state = ui_state
if (timeout)
src.timeout = timeout
start_time = world.time
QDEL_IN(src, timeout)

/datum/tgui_input_keycombo/Destroy(force)
SStgui.close_uis(src)
state = null
return ..()

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

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

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

/datum/tgui_input_keycombo/ui_state(mob/user)
return state

/datum/tgui_input_keycombo/ui_static_data(mob/user)
var/list/data = list()
data["init_value"] = default // Default is a reserved keyword
data["large_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
data["message"] = message
data["swapped_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
data["title"] = title
return data

/datum/tgui_input_keycombo/ui_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_keycombo/ui_act(action, list/params)
. = ..()
if (.)
return
switch(action)
if("submit")
set_entry(params["entry"])
closed = TRUE
SStgui.close_uis(src)
return TRUE
if("cancel")
closed = TRUE
SStgui.close_uis(src)
return TRUE

/datum/tgui_input_keycombo/proc/set_entry(entry)
src.entry = entry
1 change: 1 addition & 0 deletions tgstation.dme
Original file line number Diff line number Diff line change
Expand Up @@ -5463,6 +5463,7 @@
#include "code\modules\tgui_input\alert.dm"
#include "code\modules\tgui_input\checkboxes.dm"
#include "code\modules\tgui_input\color.dm"
#include "code\modules\tgui_input\keycombo.dm"
#include "code\modules\tgui_input\list.dm"
#include "code\modules\tgui_input\number.dm"
#include "code\modules\tgui_input\text.dm"
Expand Down
Loading

0 comments on commit dc842f9

Please sign in to comment.