From 54a6c5a6feea12ecf8ee288fd79c4c00844ea643 Mon Sep 17 00:00:00 2001 From: kevinz000 <2003111+kevinz000@users.noreply.github.com> Date: Thu, 27 Feb 2020 23:58:49 -0700 Subject: [PATCH] Typing indicators --- code/controllers/subsystem/input.dm | 39 ++++++++++++++++ code/modules/keybindings/bindings_living.dm | 1 - code/modules/keybindings/bindings_mob.dm | 11 +++++ .../mob/living/carbon/human/species.dm | 8 ++++ .../living/carbon/human/typing_indicator.dm | 2 + code/modules/mob/living/say.dm | 2 +- code/modules/mob/mob_defines.dm | 10 ++++ code/modules/mob/typing_indicator.dm | 44 ++++++++++++++++++ icons/mob/talk.dmi | Bin 5497 -> 5491 bytes tgstation.dme | 4 +- 10 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 code/modules/mob/living/carbon/human/typing_indicator.dm create mode 100644 code/modules/mob/typing_indicator.dm diff --git a/code/controllers/subsystem/input.dm b/code/controllers/subsystem/input.dm index 0970b23a16..8a8e0ac7c9 100644 --- a/code/controllers/subsystem/input.dm +++ b/code/controllers/subsystem/input.dm @@ -8,6 +8,12 @@ SUBSYSTEM_DEF(input) var/list/macro_sets var/list/movement_keys + /*! Special thing (until we get custom keybinds that also support clientside macros :weary:) that lets us do stuff like look up "which key is say/me bound to". Special format too, the value of a key should be list(key, modifier1, modifier2, ...). "default" unless override. + * USE get_key_for_macro_id or get_typing_indicator_binds TO ACCESS! + */ + var/list/macro_set_reverse_lookups + /// cache these for speed. + var/list/typing_indicator_binds /datum/controller/subsystem/input/Initialize() setup_default_macro_sets() @@ -58,6 +64,32 @@ SUBSYSTEM_DEF(input) ), ) + macro_set_reverse_lookups = list( + "default" = list( + "say" = list("T"), + "whisper" = list("T", "Ctrl"), + "me" = list("M"), + "subtle" = list("M", "Ctrl") + ), + "old_default" = list( + "say" = list("T", "Ctrl"), + ), + "old_hotkeys" = list( + "say" = list("T"), + "me" = list("M"), + ) + ) + + typing_indicator_binds = list() + var/static/list/typing_indicator_verbs = list("me", "say") + for(var/check_verb in typing_indicator_verbs) + for(var/macro_set in macro_set_reverse_lookups) + var/list/keylist = macro_set_reverse_lookups[macro_set][check_verb] + if(!keylist) + continue + var/key = keylist[1] + typing_indicator_binds[key] = TRUE + // Because i'm lazy and don't want to type all these out twice var/list/old_default = default_macro_sets["old_default"] @@ -117,3 +149,10 @@ SUBSYSTEM_DEF(input) for(var/i in 1 to length(clients)) var/client/C = clients[i] C.keyLoop() + +/datum/controller/subsystem/input/proc/get_key_for_macro_id(macro_id, macroset) + return macro_set_reverse_lookups[macroset][macro_id] || macro_set_reverse_lookups["default"][macro_id] + +/// Returns an associative list of keys without modifiers like ctrl. You have to do another lookup, this is for the first check to be faster. +/datum/controller/subsystem/input/proc/get_typing_indicator_binds(macroset) + return typing_indicator_binds[macroset] diff --git a/code/modules/keybindings/bindings_living.dm b/code/modules/keybindings/bindings_living.dm index ec6c5dd539..b338c5e899 100644 --- a/code/modules/keybindings/bindings_living.dm +++ b/code/modules/keybindings/bindings_living.dm @@ -23,5 +23,4 @@ lay_down() return - return ..() \ No newline at end of file diff --git a/code/modules/keybindings/bindings_mob.dm b/code/modules/keybindings/bindings_mob.dm index 964ee65047..6d413b4166 100644 --- a/code/modules/keybindings/bindings_mob.dm +++ b/code/modules/keybindings/bindings_mob.dm @@ -3,6 +3,17 @@ // Or we can have NPC's send actual keypresses and detect that by seeing no client /mob/key_down(_key, client/user) + if(SSinput.typing_indicator_binds[_key]) + var/macroset = winget(user, "mainwindow", "macro") + var/list/L = SSinput.macro_set_reverse_lookups[macroset] + var/valid = TRUE + if(length(L) > 1) + for(var/modifier in 2 to length(L)) + if(!client.keys_held[modifier]) + valid = FALSE + break + if(valid) + display_typing_indicator() switch(_key) if("Delete", "H") if(!pulling) diff --git a/code/modules/mob/living/carbon/human/species.dm b/code/modules/mob/living/carbon/human/species.dm index b1bc47ea4a..52e14a43e4 100644 --- a/code/modules/mob/living/carbon/human/species.dm +++ b/code/modules/mob/living/carbon/human/species.dm @@ -106,6 +106,9 @@ GLOBAL_LIST_EMPTY(roundstart_race_names) var/whitelist = list() //List the ckeys that can use this species, if it's whitelisted.: list("John Doe", "poopface666", "SeeALiggerPullTheTrigger") Spaces & capitalization can be included or ignored entirely for each key as it checks for both. var/should_draw_citadel = FALSE + /// Our default override for typing indicator state + var/typing_indicator_state + /////////// // PROCS // /////////// @@ -2218,3 +2221,8 @@ GLOBAL_LIST_EMPTY(roundstart_race_names) /datum/species/proc/start_wagging_tail(mob/living/carbon/human/H) /datum/species/proc/stop_wagging_tail(mob/living/carbon/human/H) + + +/////// TYPING INDICATORS /////// +/datum/species/proc/get_typing_indicator_state() + return typing_indicator_state diff --git a/code/modules/mob/living/carbon/human/typing_indicator.dm b/code/modules/mob/living/carbon/human/typing_indicator.dm new file mode 100644 index 0000000000..6d420c1f7b --- /dev/null +++ b/code/modules/mob/living/carbon/human/typing_indicator.dm @@ -0,0 +1,2 @@ +/mob/living/carbon/human/get_typing_indicator_icon_state() + return dna?.species?.get_typing_indicator_state() || ..() diff --git a/code/modules/mob/living/say.dm b/code/modules/mob/living/say.dm index 35b49cce8e..f90b285d2d 100644 --- a/code/modules/mob/living/say.dm +++ b/code/modules/mob/living/say.dm @@ -354,7 +354,7 @@ GLOBAL_LIST_INIT(department_radio_keys, list( if(cultslurring) message = cultslur(message) - + if(clockcultslurring) message = CLOCK_CULT_SLUR(message) diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm index c45f6eec13..7a3959ed8f 100644 --- a/code/modules/mob/mob_defines.dm +++ b/code/modules/mob/mob_defines.dm @@ -128,3 +128,13 @@ var/flavor_text = "" var/flavor_text_2 = "" //version of the above that only lasts for the current round. + + ///////TYPING INDICATORS/////// + /// Set to true if we want to show typing indicators. + var/typing_indicator_enabled = FALSE + /// Default icon_state of our typing indicator. Currently only supports paths (because anything else is, as of time of typing this, unnecesary. + var/typing_indicator_state = /obj/effect/overlay/typing_indicator + /// The timer that will remove our indicator for early aborts (like when an user finishes their message) + var/typing_indicator_timerid + /// Default typing indicator timeout + var/typing_indicator_timeout = 30 SECONDS diff --git a/code/modules/mob/typing_indicator.dm b/code/modules/mob/typing_indicator.dm new file mode 100644 index 0000000000..92710e2434 --- /dev/null +++ b/code/modules/mob/typing_indicator.dm @@ -0,0 +1,44 @@ +/// state = overlay/image/object/type/whatever add_overlay will accept +GLOBAL_LIST_EMPTY(typing_indicator_overlays) + +/// Fetches the typing indicator we'll use from GLOB.typing_indicator_overlays +/mob/proc/get_indicator_overlay(state) + . = GLOB.typing_indicator_overlays[state] + if(.) + return + // doesn't exist, make it and cache it + if(ispath(state)) + . = GLOB.typing_indicator_overlays[state] = state + // We only support paths for now because anything else isn't necessary yet. + +/// Gets the state we will use for typing indicators. Defaults to src.typing_indicator_state +/mob/proc/get_typing_indicator_icon_state() + return typing_indicator_state + +/*! + * Displays typing indicator. + * @param timeout_override - Sets how long until this will disappear on its own without the user finishing their message or logging out. Defaults to src.typing_indicator_timeout + * @param state_override - Sets the state that we will fetch. Defaults to src.get_typing_indicator_icon_state() + * @param force - shows even if src.typing_indcator_enabled is FALSE. + */ +/mob/proc/display_typing_indicator(timeout_override = typing_indicator_timeout, state_override = get_typing_indicator_icon_state(), force = FALSE) + if(!typing_indicator_enabled || force) + return + add_overlay(get_indicator_overlay(state_override)) + addtimer(CALLBACK(src, .proc/clear_typing_indicator, state_override), timeout_override, TIMER_STOPPABLE) + +/*! + * Removes typing indicator. + * @param state_override Sets the state that we will remove. Defaults to src.get_typing_indicator_icon_state() + */ +/mob/proc/clear_typing_indicator(state_override = get_typing_indicator_icon_state()) + deltimer(typing_indicator_timerid) + cut_overlay(get_indicator_overlay(state_override)) + +/// Default typing indicator +/obj/effect/overlay/typing_indicator + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + icon = 'icons/mob/talk.dmi' + icon_state = "default_typing" + appearance_flags = RESET_COLOR | TILE_BOUND | PIXEL_SCALE + layer = LARGE_MOB_LAYER diff --git a/icons/mob/talk.dmi b/icons/mob/talk.dmi index 05fe6f56236377f0227fcb4b11800a2bd837ba17..2e300b5a290ba932ecb8408f5b1f2b41e652b455 100644 GIT binary patch delta 4148 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 QLcq^iXD_Gf(}>&u1vJCV9{>OV delta 4153 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 gzq`c7JvIk