From 49941ea2e675ee2a9ce54e4fa4e4159a48ca89e1 Mon Sep 17 00:00:00 2001 From: Letter N <24603524+LetterN@users.noreply.github.com> Date: Wed, 22 Jul 2020 10:27:37 +0800 Subject: [PATCH] code, still not enabled --- code/controllers/subsystem/tgui.dm | 346 +++++++++++++- code/modules/tgui/external.dm | 115 +++-- code/modules/tgui/states.dm | 57 ++- code/modules/tgui/states/admin.dm | 3 + code/modules/tgui/states/always.dm | 3 + code/modules/tgui/states/conscious.dm | 3 + code/modules/tgui/states/contained.dm | 3 + code/modules/tgui/states/deep_inventory.dm | 6 +- code/modules/tgui/states/default.dm | 6 +- code/modules/tgui/states/default_contained.dm | 13 - code/modules/tgui/states/hands.dm | 7 +- code/modules/tgui/states/human_adjacent.dm | 3 + code/modules/tgui/states/inventory.dm | 6 +- code/modules/tgui/states/language_menu.dm | 3 + code/modules/tgui/states/not_incapacitated.dm | 11 +- code/modules/tgui/states/notcontained.dm | 6 +- code/modules/tgui/states/observer.dm | 3 + code/modules/tgui/states/physical.dm | 3 + code/modules/tgui/states/self.dm | 3 + code/modules/tgui/states/zlevel.dm | 3 + code/modules/tgui/subsystem.dm | 247 ---------- code/modules/tgui/tgui.dm | 450 ++++++++---------- code/modules/tgui/tgui_window.dm | 238 +++++++++ 23 files changed, 936 insertions(+), 602 deletions(-) delete mode 100644 code/modules/tgui/states/default_contained.dm delete mode 100644 code/modules/tgui/subsystem.dm create mode 100644 code/modules/tgui/tgui_window.dm diff --git a/code/controllers/subsystem/tgui.dm b/code/controllers/subsystem/tgui.dm index 58bc28fa2f..a5526d2c03 100644 --- a/code/controllers/subsystem/tgui.dm +++ b/code/controllers/subsystem/tgui.dm @@ -1,3 +1,12 @@ +/** + * tgui subsystem + * + * Contains all tgui state and subsystem code. + * + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT + */ + SUBSYSTEM_DEF(tgui) name = "tgui" wait = 9 @@ -5,33 +14,338 @@ SUBSYSTEM_DEF(tgui) priority = FIRE_PRIORITY_TGUI runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT - var/list/currentrun = list() - var/list/open_uis = list() // A list of open UIs, grouped by src_object and ui_key. - var/list/processing_uis = list() // A list of processing UIs, ungrouped. - var/basehtml // The HTML base used for all UIs. + /// A list of UIs scheduled to process + var/list/current_run = list() + /// A list of open UIs + var/list/open_uis = list() + /// A list of open UIs, grouped by src_object. + var/list/open_uis_by_src = list() + /// The HTML base used for all UIs. + var/basehtml /datum/controller/subsystem/tgui/PreInit() - basehtml = file2text('tgui/packages/tgui/public/tgui.html') + basehtml = file2text('tgui/packages/tgui/public/tgui.html') /datum/controller/subsystem/tgui/Shutdown() close_all_uis() /datum/controller/subsystem/tgui/stat_entry() - ..("P:[processing_uis.len]") + ..("P:[open_uis.len]") /datum/controller/subsystem/tgui/fire(resumed = 0) - if (!resumed) - src.currentrun = processing_uis.Copy() - //cache for sanic speed (lists are references anyways) - var/list/currentrun = src.currentrun - - while(currentrun.len) - var/datum/tgui/ui = currentrun[currentrun.len] - currentrun.len-- + if(!resumed) + src.current_run = open_uis.Copy() + // Cache for sanic speed (lists are references anyways) + var/list/current_run = src.current_run + while(current_run.len) + var/datum/tgui/ui = current_run[current_run.len] + current_run.len-- + // TODO: Move user/src_object check to process() if(ui && ui.user && ui.src_object) ui.process() else - processing_uis.Remove(ui) - if (MC_TICK_CHECK) + open_uis.Remove(ui) + if(MC_TICK_CHECK) return +/** + * public + * + * Requests a usable tgui window from the pool. + * Returns null if pool was exhausted. + * + * required user mob + * return datum/tgui + */ +/datum/controller/subsystem/tgui/proc/request_pooled_window(mob/user) + if(!user.client) + return null + var/list/windows = user.client.tgui_windows + var/window_id + var/datum/tgui_window/window + var/window_found = FALSE + // Find a usable window + for(var/i in 1 to TGUI_WINDOW_HARD_LIMIT) + window_id = TGUI_WINDOW_ID(i) + window = windows[window_id] + // As we are looping, create missing window datums + if(!window) + window = new(user.client, window_id, pooled = TRUE) + // Skip windows with acquired locks + if(window.locked) + continue + if(window.status == TGUI_WINDOW_READY) + return window + if(window.status == TGUI_WINDOW_CLOSED) + window.status = TGUI_WINDOW_LOADING + window_found = TRUE + break + if(!window_found) + log_tgui(user, "Error: Pool exhausted") + return null + return window + +/** + * public + * + * Force closes all tgui windows. + * + * required user mob + */ +/datum/controller/subsystem/tgui/proc/force_close_all_windows(mob/user) + log_tgui(user, "force_close_all_windows") + if(user.client) + user.client.tgui_windows = list() + for(var/i in 1 to TGUI_WINDOW_HARD_LIMIT) + var/window_id = TGUI_WINDOW_ID(i) + user << browse(null, "window=[window_id]") + +/** + * public + * + * Force closes the tgui window by window_id. + * + * required user mob + * required window_id string + */ +/datum/controller/subsystem/tgui/proc/force_close_window(mob/user, window_id) + log_tgui(user, "force_close_window") + // Close all tgui datums based on window_id. + for(var/datum/tgui/ui in user.tgui_open_uis) + if(ui.window && ui.window.id == window_id) + ui.close(can_be_suspended = FALSE) + // Unset machine just to be sure. + user.unset_machine() + // Close window directly just to be sure. + user << browse(null, "window=[window_id]") + +/** + * public + * + * Try to find an instance of a UI, and push an update to it. + * + * required user mob The mob who opened/is using the UI. + * required src_object datum The object/datum which owns the UI. + * optional ui datum/tgui The UI to be updated, if it exists. + * optional force_open bool If the UI should be re-opened instead of updated. + * + * return datum/tgui The found UI. + */ +/datum/controller/subsystem/tgui/proc/try_update_ui( + mob/user, + datum/src_object, + datum/tgui/ui) + // Look up a UI if it wasn't passed + if(isnull(ui)) + ui = get_open_ui(user, src_object) + // Couldn't find a UI. + if(isnull(ui)) + return null + ui.process_status() + // UI ended up with the closed status + // or is actively trying to close itself. + // FIXME: Doesn't actually fix the paper bug. + if(ui.status <= UI_CLOSE) + ui.close() + return null + ui.send_update() + return ui + +/** + * public + * + * Get a open UI given a user and src_object. + * + * required user mob The mob who opened/is using the UI. + * required src_object datum The object/datum which owns the UI. + * + * return datum/tgui The found UI. + */ +/datum/controller/subsystem/tgui/proc/get_open_ui(mob/user, datum/src_object) + var/key = "[REF(src_object)]" + // No UIs opened for this src_object + if(isnull(open_uis_by_src[key]) || !istype(open_uis_by_src[key], /list)) + return null + for(var/datum/tgui/ui in open_uis_by_src[key]) + // Make sure we have the right user + if(ui.user == user) + return ui + return null + +/** + * public + * + * Update all UIs attached to src_object. + * + * required src_object datum The object/datum which owns the UIs. + * + * return int The number of UIs updated. + */ +/datum/controller/subsystem/tgui/proc/update_uis(datum/src_object) + var/count = 0 + var/key = "[REF(src_object)]" + // No UIs opened for this src_object + if(isnull(open_uis_by_src[key]) || !istype(open_uis_by_src[key], /list)) + return count + for(var/datum/tgui/ui in open_uis_by_src[key]) + // Check if UI is valid. + if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user)) + ui.process(force = 1) + count++ + return count + +/** + * public + * + * Close all UIs attached to src_object. + * + * required src_object datum The object/datum which owns the UIs. + * + * return int The number of UIs closed. + */ +/datum/controller/subsystem/tgui/proc/close_uis(datum/src_object) + var/count = 0 + var/key = "[REF(src_object)]" + // No UIs opened for this src_object + if(isnull(open_uis_by_src[key]) || !istype(open_uis_by_src[key], /list)) + return count + for(var/datum/tgui/ui in open_uis_by_src[key]) + // Check if UI is valid. + if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user)) + ui.close() + count++ + return count + +/** + * public + * + * Close all UIs regardless of their attachment to src_object. + * + * return int The number of UIs closed. + */ +/datum/controller/subsystem/tgui/proc/close_all_uis() + var/count = 0 + for(var/key in open_uis_by_src) + for(var/datum/tgui/ui in open_uis_by_src[key]) + // Check if UI is valid. + if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user)) + ui.close() + count++ + return count + +/** + * public + * + * Update all UIs belonging to a user. + * + * required user mob The mob who opened/is using the UI. + * optional src_object datum If provided, only update UIs belonging this src_object. + * + * return int The number of UIs updated. + */ +/datum/controller/subsystem/tgui/proc/update_user_uis(mob/user, datum/src_object) + var/count = 0 + if(length(user?.tgui_open_uis) == 0) + return count + for(var/datum/tgui/ui in user.tgui_open_uis) + if(isnull(src_object) || ui.src_object == src_object) + ui.process(force = 1) + count++ + return count + +/** + * public + * + * Close all UIs belonging to a user. + * + * required user mob The mob who opened/is using the UI. + * optional src_object datum If provided, only close UIs belonging this src_object. + * + * return int The number of UIs closed. + */ +/datum/controller/subsystem/tgui/proc/close_user_uis(mob/user, datum/src_object) + var/count = 0 + if(length(user?.tgui_open_uis) == 0) + return count + for(var/datum/tgui/ui in user.tgui_open_uis) + if(isnull(src_object) || ui.src_object == src_object) + ui.close() + count++ + return count + +/** + * private + * + * Add a UI to the list of open UIs. + * + * required ui datum/tgui The UI to be added. + */ +/datum/controller/subsystem/tgui/proc/on_open(datum/tgui/ui) + var/key = "[REF(ui.src_object)]" + if(isnull(open_uis_by_src[key]) || !istype(open_uis_by_src[key], /list)) + open_uis_by_src[key] = list() + ui.user.tgui_open_uis |= ui + var/list/uis = open_uis_by_src[key] + uis |= ui + open_uis |= ui + +/** + * private + * + * Remove a UI from the list of open UIs. + * + * required ui datum/tgui The UI to be removed. + * + * return bool If the UI was removed or not. + */ +/datum/controller/subsystem/tgui/proc/on_close(datum/tgui/ui) + var/key = "[REF(ui.src_object)]" + if(isnull(open_uis_by_src[key]) || !istype(open_uis_by_src[key], /list)) + return FALSE + // Remove it from the list of processing UIs. + open_uis.Remove(ui) + // If the user exists, remove it from them too. + if(ui.user) + ui.user.tgui_open_uis.Remove(ui) + var/list/uis = open_uis_by_src[key] + uis.Remove(ui) + if(length(uis) == 0) + open_uis_by_src.Remove(key) + return TRUE + +/** + * private + * + * Handle client logout, by closing all their UIs. + * + * required user mob The mob which logged out. + * + * return int The number of UIs closed. + */ +/datum/controller/subsystem/tgui/proc/on_logout(mob/user) + close_user_uis(user) + +/** + * private + * + * Handle clients switching mobs, by transferring their UIs. + * + * required user source The client's original mob. + * required user target The client's new mob. + * + * return bool If the UIs were transferred. + */ +/datum/controller/subsystem/tgui/proc/on_transfer(mob/source, mob/target) + // The old mob had no open UIs. + if(length(source?.tgui_open_uis) == 0) + return FALSE + if(isnull(target.tgui_open_uis) || !istype(target.tgui_open_uis, /list)) + target.tgui_open_uis = list() + // Transfer all the UIs. + for(var/datum/tgui/ui in source.tgui_open_uis) + // Inform the UIs of their new owner. + ui.user = target + target.tgui_open_uis.Add(ui) + // Clear the old list. + source.tgui_open_uis.Cut() + return TRUE diff --git a/code/modules/tgui/external.dm b/code/modules/tgui/external.dm index 5de54c439c..46b324e151 100644 --- a/code/modules/tgui/external.dm +++ b/code/modules/tgui/external.dm @@ -1,7 +1,8 @@ /** - * tgui external + * External tgui definitions, such as src_object APIs. * - * Contains all external tgui declarations. + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT */ /** @@ -11,13 +12,9 @@ * If this proc is not implemented properly, the UI will not update correctly. * * required user mob The mob who opened/is using the UI. - * optional ui_key string The ui_key of the UI. * optional ui datum/tgui The UI to be updated, if it exists. - * optional force_open bool If the UI should be re-opened instead of updated. - * optional master_ui datum/tgui The parent UI. - * optional state datum/ui_state The state used to determine status. */ -/datum/proc/ui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.default_state) +/datum/proc/ui_interact(mob/user, datum/tgui/ui) return FALSE // Not implemented. /** @@ -37,10 +34,11 @@ * public * * Static Data to be sent to the UI. - * Static data differs from normal data in that it's large data that should be sent infrequently - * This is implemented optionally for heavy uis that would be sending a lot of redundant data - * frequently. - * Gets squished into one object on the frontend side, but the static part is cached. + * + * Static data differs from normal data in that it's large data that should be + * sent infrequently. This is implemented optionally for heavy uis that would + * be sending a lot of redundant data frequently. Gets squished into one + * object on the frontend side, but the static part is cached. * * required user mob The mob interacting with the UI. * @@ -52,18 +50,17 @@ /** * public * - * Forces an update on static data. Should be done manually whenever something happens to change static data. + * Forces an update on static data. Should be done manually whenever something + * happens to change static data. * * required user the mob currently interacting with the ui * optional ui ui to be updated - * optional ui_key ui key of ui to be updated */ -/datum/proc/update_static_data(mob/user, datum/tgui/ui, ui_key = "main") - ui = SStgui.try_update_ui(user, src, ui_key, ui) - // If there was no ui to update, there's no static data to update either. +/datum/proc/update_static_data(mob/user, datum/tgui/ui) if(!ui) - return - ui.push_data(null, ui_static_data(), TRUE) + ui = SStgui.get_open_ui(user, src) + if(ui) + ui.send_full_update() /** * public @@ -85,17 +82,12 @@ * public * * Called on an object when a tgui object is being created, allowing you to - * customise the html - * For example: inserting a custom stylesheet that you need in the head + * push various assets to tgui, for examples spritesheets. * - * For this purpose, some tags are available in the html, to be parsed out - ^ with replacetext - * (customheadhtml) - Additions to the head tag - * - * required html the html base text + * return list List of asset datums or file paths. */ -/datum/proc/ui_base_html(html) - return html +/datum/proc/ui_assets(mob/user) + return list() /** * private @@ -107,6 +99,15 @@ /datum/proc/ui_host(mob/user) return src // Default src. +/** + * private + * + * The UI's state controller to be used for created uis + * This is a proc over a var for memory reasons + */ +/datum/proc/ui_state(mob/user) + return GLOB.default_state + /** * global * @@ -118,9 +119,17 @@ /** * global * - * Used to track UIs for a mob. + * Tracks open UIs for a user. */ -/mob/var/list/open_uis = list() +/mob/var/list/tgui_open_uis = list() + +/** + * global + * + * Tracks open windows for a user. + */ +/client/var/list/tgui_windows = list() + /** * public * @@ -137,17 +146,43 @@ * * required uiref ref The UI that was closed. */ -/client/verb/uiclose(ref as text) +/client/verb/uiclose(window_id as text) // Name the verb, and hide it from the user panel. set name = "uiclose" - set hidden = 1 + set hidden = TRUE + var/mob/user = src && src.mob + if(!user) + return + // Close all tgui datums based on window_id. + SStgui.force_close_window(user, window_id) - // Get the UI based on the ref. - var/datum/tgui/ui = locate(ref) - - // If we found the UI, close it. - if(istype(ui)) - ui.close() - // Unset machine just to be sure. - if(src && src.mob) - src.mob.unset_machine() +/** + * Middleware for /client/Topic. + * + * return bool Whether the topic is passed (TRUE), or cancelled (FALSE). + */ +/proc/tgui_Topic(href_list) + // Skip non-tgui topics + if(!href_list["tgui"]) + return TRUE + var/type = href_list["type"] + // Unconditionally collect tgui logs + if(type == "log") + log_tgui(usr, href_list["message"]) + // Locate window + var/window_id = href_list["window_id"] + var/datum/tgui_window/window + if(window_id) + window = usr.client.tgui_windows[window_id] + if(!window) + log_tgui(usr, "Error: Couldn't find the window datum, force closing.") + SStgui.force_close_window(usr, window_id) + return FALSE + // Decode payload + var/payload + if(href_list["payload"]) + payload = json_decode(href_list["payload"]) + // Pass message to window + if(window) + window.on_message(type, payload, href_list) + return FALSE diff --git a/code/modules/tgui/states.dm b/code/modules/tgui/states.dm index e626b98815..383cce7a78 100644 --- a/code/modules/tgui/states.dm +++ b/code/modules/tgui/states.dm @@ -1,7 +1,9 @@ /** - * tgui states + * Base state and helpers for states. Just does some sanity checks, + * implement a proper state for in-depth checks. * - * Base state and helpers for states. Just does some sanity checks, implement a state for in-depth checks. + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT */ /** @@ -22,13 +24,14 @@ if(isobserver(user)) // If they turn on ghost AI control, admins can always interact. - if(IsAdminGhost(user)) + if(isAdminGhostAI(user)) //IsAdminGhost . = max(., UI_INTERACTIVE) // Regular ghosts can always at least view if in range. - var/clientviewlist = getviewsize(user.client.view) - if(get_dist(src_object, user) < max(clientviewlist[1],clientviewlist[2])) - . = max(., UI_UPDATE) + if(user.client) + var/clientviewlist = getviewsize(user.client.view) + if(get_dist(src_object, user) < max(clientviewlist[1], clientviewlist[2])) + . = max(., UI_UPDATE) // Check if the state allows interaction var/result = state.can_use_topic(src_object, user) @@ -46,7 +49,8 @@ * return UI_state The state of the UI. */ /datum/ui_state/proc/can_use_topic(src_object, mob/user) - return UI_CLOSE // Don't allow interaction by default. + // Don't allow interaction by default. + return UI_CLOSE /** * public @@ -56,21 +60,31 @@ * return UI_state The state of the UI. */ /mob/proc/shared_ui_interaction(src_object) - if(!client) // Close UIs if mindless. + // Close UIs if mindless. + if(!client) return UI_CLOSE - else if(stat) // Disable UIs if unconcious. + // Disable UIs if unconcious. + else if(stat) return UI_DISABLED - else if(incapacitated() || lying) // Update UIs if incapicitated but concious. + // Update UIs if incapicitated but concious. + else if(incapacitated()) return UI_UPDATE return UI_INTERACTIVE +/mob/living/shared_ui_interaction(src_object) + . = ..() + if(!(mobility_flags & MOBILITY_UI) && . == UI_INTERACTIVE) + return UI_UPDATE + /mob/living/silicon/ai/shared_ui_interaction(src_object) - if(lacks_power()) // Disable UIs if the AI is unpowered. + // Disable UIs if the AI is unpowered. + if(lacks_power()) return UI_DISABLED return ..() /mob/living/silicon/robot/shared_ui_interaction(src_object) - if(!cell || cell.charge <= 0 || locked_down) // Disable UIs if the Borg is unpowered or locked. + // Disable UIs if the Borg is unpowered or locked. + if(!cell || cell.charge <= 0 || lockcharge) return UI_DISABLED return ..() @@ -87,7 +101,8 @@ * return UI_state The state of the UI. */ /atom/proc/contents_ui_distance(src_object, mob/living/user) - return user.shared_living_ui_distance(src_object) // Just call this mob's check. + // Just call this mob's check. + return user.shared_living_ui_distance(src_object) /** * public @@ -99,17 +114,21 @@ * return UI_state The state of the UI. */ /mob/living/proc/shared_living_ui_distance(atom/movable/src_object, viewcheck = TRUE) - if(viewcheck && !(src_object in view(src))) // If the object is obscured, close it. + // If the object is obscured, close it. + if(viewcheck && !(src_object in view(src))) return UI_CLOSE - var/dist = get_dist(src_object, src) - if(dist <= 1 || src_object.hasSiliconAccessInArea(src)) // Open and interact if 1-0 tiles away. + // Open and interact if 1-0 tiles away. + if(dist <= 1) return UI_INTERACTIVE - else if(dist <= 2) // View only if 2-3 tiles away. + // View only if 2-3 tiles away. + else if(dist <= 2) return UI_UPDATE - else if(dist <= 5) // Disable if 5 tiles away. + // Disable if 5 tiles away. + else if(dist <= 5) return UI_DISABLED - return UI_CLOSE // Otherwise, we got nothing. + // Otherwise, we got nothing. + return UI_CLOSE /mob/living/carbon/human/shared_living_ui_distance(atom/movable/src_object, viewcheck = TRUE) if(dna.check_mutation(TK) && tkMaxRangeCheck(src, src_object)) diff --git a/code/modules/tgui/states/admin.dm b/code/modules/tgui/states/admin.dm index 61fc373118..227a294078 100644 --- a/code/modules/tgui/states/admin.dm +++ b/code/modules/tgui/states/admin.dm @@ -2,6 +2,9 @@ * tgui state: admin_state * * Checks that the user is an admin, end-of-story. + * + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT */ GLOBAL_DATUM_INIT(admin_state, /datum/ui_state/admin_state, new) diff --git a/code/modules/tgui/states/always.dm b/code/modules/tgui/states/always.dm index a741e2e3d4..210f0896a2 100644 --- a/code/modules/tgui/states/always.dm +++ b/code/modules/tgui/states/always.dm @@ -2,6 +2,9 @@ * tgui state: always_state * * Always grants the user UI_INTERACTIVE. Period. + * + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT */ GLOBAL_DATUM_INIT(always_state, /datum/ui_state/always_state, new) diff --git a/code/modules/tgui/states/conscious.dm b/code/modules/tgui/states/conscious.dm index 4e2793d130..670ca7c07e 100644 --- a/code/modules/tgui/states/conscious.dm +++ b/code/modules/tgui/states/conscious.dm @@ -2,6 +2,9 @@ * tgui state: conscious_state * * Only checks if the user is conscious. + * + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT */ GLOBAL_DATUM_INIT(conscious_state, /datum/ui_state/conscious_state, new) diff --git a/code/modules/tgui/states/contained.dm b/code/modules/tgui/states/contained.dm index f02424d01e..1eb8edba25 100644 --- a/code/modules/tgui/states/contained.dm +++ b/code/modules/tgui/states/contained.dm @@ -2,6 +2,9 @@ * tgui state: contained_state * * Checks that the user is inside the src_object. + * + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT */ GLOBAL_DATUM_INIT(contained_state, /datum/ui_state/contained_state, new) diff --git a/code/modules/tgui/states/deep_inventory.dm b/code/modules/tgui/states/deep_inventory.dm index 43758cbab1..a2b9276a59 100644 --- a/code/modules/tgui/states/deep_inventory.dm +++ b/code/modules/tgui/states/deep_inventory.dm @@ -1,7 +1,11 @@ /** * tgui state: deep_inventory_state * - * Checks that the src_object is in the user's deep (backpack, box, toolbox, etc) inventory. + * Checks that the src_object is in the user's deep + * (backpack, box, toolbox, etc) inventory. + * + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT */ GLOBAL_DATUM_INIT(deep_inventory_state, /datum/ui_state/deep_inventory_state, new) diff --git a/code/modules/tgui/states/default.dm b/code/modules/tgui/states/default.dm index 6bb159640e..367e57beff 100644 --- a/code/modules/tgui/states/default.dm +++ b/code/modules/tgui/states/default.dm @@ -1,7 +1,11 @@ /** * tgui state: default_state * - * Checks a number of things -- mostly physical distance for humans and view for robots. + * Checks a number of things -- mostly physical distance for humans + * and view for robots. + * + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT */ GLOBAL_DATUM_INIT(default_state, /datum/ui_state/default, new) diff --git a/code/modules/tgui/states/default_contained.dm b/code/modules/tgui/states/default_contained.dm deleted file mode 100644 index c532e9f5d1..0000000000 --- a/code/modules/tgui/states/default_contained.dm +++ /dev/null @@ -1,13 +0,0 @@ -/** - * tgui state: default_contained - * - * Basically default and contained combined, allowing for both - */ - -GLOBAL_DATUM_INIT(default_contained_state, /datum/ui_state/default/contained, new) - -/datum/ui_state/default/contained/can_use_topic(atom/src_object, mob/user) - if(src_object.contains(user)) - return UI_INTERACTIVE - return ..() - \ No newline at end of file diff --git a/code/modules/tgui/states/hands.dm b/code/modules/tgui/states/hands.dm index d73d1058ea..1c885ed414 100644 --- a/code/modules/tgui/states/hands.dm +++ b/code/modules/tgui/states/hands.dm @@ -2,6 +2,9 @@ * tgui state: hands_state * * Checks that the src_object is in the user's hands. + * + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT */ GLOBAL_DATUM_INIT(hands_state, /datum/ui_state/hands_state, new) @@ -19,7 +22,7 @@ GLOBAL_DATUM_INIT(hands_state, /datum/ui_state/hands_state, new) return UI_INTERACTIVE return UI_CLOSE -/mob/living/silicon/robot/hands_can_use_topic(obj/src_object) - if(activated(src_object) || istype(src_object.loc, /obj/item/weapon/gripper)) +/mob/living/silicon/robot/hands_can_use_topic(src_object) + if(activated(src_object)) return UI_INTERACTIVE return UI_CLOSE diff --git a/code/modules/tgui/states/human_adjacent.dm b/code/modules/tgui/states/human_adjacent.dm index 7aefe43e44..2ac7c8637b 100644 --- a/code/modules/tgui/states/human_adjacent.dm +++ b/code/modules/tgui/states/human_adjacent.dm @@ -3,6 +3,9 @@ * * In addition to default checks, only allows interaction for a * human adjacent user. + * + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT */ GLOBAL_DATUM_INIT(human_adjacent_state, /datum/ui_state/human_adjacent_state, new) diff --git a/code/modules/tgui/states/inventory.dm b/code/modules/tgui/states/inventory.dm index 43fe2cb451..dc5dd0d57e 100644 --- a/code/modules/tgui/states/inventory.dm +++ b/code/modules/tgui/states/inventory.dm @@ -1,7 +1,11 @@ /** * tgui state: inventory_state * - * Checks that the src_object is in the user's top-level (hand, ear, pocket, belt, etc) inventory. + * Checks that the src_object is in the user's top-level + * (hand, ear, pocket, belt, etc) inventory. + * + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT */ GLOBAL_DATUM_INIT(inventory_state, /datum/ui_state/inventory_state, new) diff --git a/code/modules/tgui/states/language_menu.dm b/code/modules/tgui/states/language_menu.dm index 5c816c8922..6389b05cd5 100644 --- a/code/modules/tgui/states/language_menu.dm +++ b/code/modules/tgui/states/language_menu.dm @@ -1,5 +1,8 @@ /** * tgui state: language_menu_state + * + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT */ GLOBAL_DATUM_INIT(language_menu_state, /datum/ui_state/language_menu, new) diff --git a/code/modules/tgui/states/not_incapacitated.dm b/code/modules/tgui/states/not_incapacitated.dm index 364b59424d..16dcb7881e 100644 --- a/code/modules/tgui/states/not_incapacitated.dm +++ b/code/modules/tgui/states/not_incapacitated.dm @@ -2,6 +2,9 @@ * tgui state: not_incapacitated_state * * Checks that the user isn't incapacitated + * + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT */ GLOBAL_DATUM_INIT(not_incapacitated_state, /datum/ui_state/not_incapacitated_state, new) @@ -24,6 +27,10 @@ GLOBAL_DATUM_INIT(not_incapacitated_turf_state, /datum/ui_state/not_incapacitate /datum/ui_state/not_incapacitated_state/can_use_topic(src_object, mob/user) if(user.stat) return UI_CLOSE - if(user.incapacitated() || user.lying || (turf_check && !isturf(user.loc))) + if(user.incapacitated() || (turf_check && !isturf(user.loc))) return UI_DISABLED - return UI_INTERACTIVE \ No newline at end of file + if(isliving(user)) + var/mob/living/L = user + if(!(L.mobility_flags & MOBILITY_STAND)) + return UI_DISABLED + return UI_INTERACTIVE diff --git a/code/modules/tgui/states/notcontained.dm b/code/modules/tgui/states/notcontained.dm index 642c6ce95f..1d4e6aec19 100644 --- a/code/modules/tgui/states/notcontained.dm +++ b/code/modules/tgui/states/notcontained.dm @@ -1,7 +1,11 @@ /** * tgui state: notcontained_state * - * Checks that the user is not inside src_object, and then makes the default checks. + * Checks that the user is not inside src_object, and then makes the + * default checks. + * + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT */ GLOBAL_DATUM_INIT(notcontained_state, /datum/ui_state/notcontained_state, new) diff --git a/code/modules/tgui/states/observer.dm b/code/modules/tgui/states/observer.dm index 86ad776b13..d105de1c0c 100644 --- a/code/modules/tgui/states/observer.dm +++ b/code/modules/tgui/states/observer.dm @@ -2,6 +2,9 @@ * tgui state: observer_state * * Checks that the user is an observer/ghost. + * + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT */ GLOBAL_DATUM_INIT(observer_state, /datum/ui_state/observer_state, new) diff --git a/code/modules/tgui/states/physical.dm b/code/modules/tgui/states/physical.dm index 88c8a291aa..3073039d14 100644 --- a/code/modules/tgui/states/physical.dm +++ b/code/modules/tgui/states/physical.dm @@ -2,6 +2,9 @@ * tgui state: physical_state * * Short-circuits the default state to only check physical distance. + * + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT */ GLOBAL_DATUM_INIT(physical_state, /datum/ui_state/physical, new) diff --git a/code/modules/tgui/states/self.dm b/code/modules/tgui/states/self.dm index b0c9500fbc..4b6e3b9fd9 100644 --- a/code/modules/tgui/states/self.dm +++ b/code/modules/tgui/states/self.dm @@ -2,6 +2,9 @@ * tgui state: self_state * * Only checks that the user and src_object are the same. + * + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT */ GLOBAL_DATUM_INIT(self_state, /datum/ui_state/self_state, new) diff --git a/code/modules/tgui/states/zlevel.dm b/code/modules/tgui/states/zlevel.dm index 5e3ccfb7de..64ea2fa1c0 100644 --- a/code/modules/tgui/states/zlevel.dm +++ b/code/modules/tgui/states/zlevel.dm @@ -2,6 +2,9 @@ * tgui state: z_state * * Only checks that the Z-level of the user and src_object are the same. + * + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT */ GLOBAL_DATUM_INIT(z_state, /datum/ui_state/z_state, new) diff --git a/code/modules/tgui/subsystem.dm b/code/modules/tgui/subsystem.dm deleted file mode 100644 index cbe94e2d7f..0000000000 --- a/code/modules/tgui/subsystem.dm +++ /dev/null @@ -1,247 +0,0 @@ -/** - * tgui subsystem - * - * Contains all tgui state and subsystem code. - */ - -/** - * public - * - * Get a open UI given a user, src_object, and ui_key and try to update it with data. - * - * required user mob The mob who opened/is using the UI. - * required src_object datum The object/datum which owns the UI. - * required ui_key string The ui_key of the UI. - * optional ui datum/tgui The UI to be updated, if it exists. - * optional force_open bool If the UI should be re-opened instead of updated. - * - * return datum/tgui The found UI. - */ -/datum/controller/subsystem/tgui/proc/try_update_ui(mob/user, datum/src_object, ui_key, datum/tgui/ui, force_open = FALSE) - if(isnull(ui)) // No UI was passed, so look for one. - ui = get_open_ui(user, src_object, ui_key) - - if(!isnull(ui)) - var/data = src_object.ui_data(user) // Get data from the src_object. - if(!force_open) // UI is already open; update it. - ui.push_data(data) - else // Re-open it anyways. - ui.reinitialize(null, data) - return ui // We found the UI, return it. - else - return null // We couldn't find a UI. - -/** - * private - * - * Get a open UI given a user, src_object, and ui_key. - * - * required user mob The mob who opened/is using the UI. - * required src_object datum The object/datum which owns the UI. - * required ui_key string The ui_key of the UI. - * - * return datum/tgui The found UI. - */ -/datum/controller/subsystem/tgui/proc/get_open_ui(mob/user, datum/src_object, ui_key) - var/src_object_key = "[REF(src_object)]" - if(isnull(open_uis[src_object_key]) || !istype(open_uis[src_object_key], /list)) - return null // No UIs open. - else if(isnull(open_uis[src_object_key][ui_key]) || !istype(open_uis[src_object_key][ui_key], /list)) - return null // No UIs open for this object. - - for(var/datum/tgui/ui in open_uis[src_object_key][ui_key]) // Find UIs for this object. - if(ui.user == user) // Make sure we have the right user - return ui - - return null // Couldn't find a UI! - -/** - * private - * - * Update all UIs attached to src_object. - * - * required src_object datum The object/datum which owns the UIs. - * - * return int The number of UIs updated. - */ -/datum/controller/subsystem/tgui/proc/update_uis(datum/src_object) - var/src_object_key = "[REF(src_object)]" - if(isnull(open_uis[src_object_key]) || !istype(open_uis[src_object_key], /list)) - return 0 // Couldn't find any UIs for this object. - - var/update_count = 0 - for(var/ui_key in open_uis[src_object_key]) - for(var/datum/tgui/ui in open_uis[src_object_key][ui_key]) - if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user)) // Check the UI is valid. - ui.process(force = 1) // Update the UI. - update_count++ // Count each UI we update. - return update_count - -/** - * private - * - * Close all UIs attached to src_object. - * - * required src_object datum The object/datum which owns the UIs. - * - * return int The number of UIs closed. - */ -/datum/controller/subsystem/tgui/proc/close_uis(datum/src_object) - var/src_object_key = "[REF(src_object)]" - if(isnull(open_uis[src_object_key]) || !istype(open_uis[src_object_key], /list)) - return 0 // Couldn't find any UIs for this object. - - var/close_count = 0 - for(var/ui_key in open_uis[src_object_key]) - for(var/datum/tgui/ui in open_uis[src_object_key][ui_key]) - if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user)) // Check the UI is valid. - ui.close() // Close the UI. - close_count++ // Count each UI we close. - return close_count - -/** - * private - * - * Close *ALL* UIs - * - * return int The number of UIs closed. - */ -/datum/controller/subsystem/tgui/proc/close_all_uis() - var/close_count = 0 - for(var/src_object_key in open_uis) - for(var/ui_key in open_uis[src_object_key]) - for(var/datum/tgui/ui in open_uis[src_object_key][ui_key]) - if(ui && ui.src_object && ui.user && ui.src_object.ui_host(ui.user)) // Check the UI is valid. - ui.close() // Close the UI. - close_count++ // Count each UI we close. - return close_count - -/** - * private - * - * Update all UIs belonging to a user. - * - * required user mob The mob who opened/is using the UI. - * optional src_object datum If provided, only update UIs belonging this src_object. - * optional ui_key string If provided, only update UIs with this UI key. - * - * return int The number of UIs updated. - */ -/datum/controller/subsystem/tgui/proc/update_user_uis(mob/user, datum/src_object = null, ui_key = null) - if(isnull(user.open_uis) || !istype(user.open_uis, /list) || open_uis.len == 0) - return 0 // Couldn't find any UIs for this user. - - var/update_count = 0 - for(var/datum/tgui/ui in user.open_uis) - if((isnull(src_object) || !isnull(src_object) && ui.src_object == src_object) && (isnull(ui_key) || !isnull(ui_key) && ui.ui_key == ui_key)) - ui.process(force = 1) // Update the UI. - update_count++ // Count each UI we upadte. - return update_count - -/** - * private - * - * Close all UIs belonging to a user. - * - * required user mob The mob who opened/is using the UI. - * optional src_object datum If provided, only close UIs belonging this src_object. - * optional ui_key string If provided, only close UIs with this UI key. - * - * return int The number of UIs closed. - */ -/datum/controller/subsystem/tgui/proc/close_user_uis(mob/user, datum/src_object = null, ui_key = null) - if(isnull(user.open_uis) || !istype(user.open_uis, /list) || open_uis.len == 0) - return 0 // Couldn't find any UIs for this user. - - var/close_count = 0 - for(var/datum/tgui/ui in user.open_uis) - if((isnull(src_object) || !isnull(src_object) && ui.src_object == src_object) && (isnull(ui_key) || !isnull(ui_key) && ui.ui_key == ui_key)) - ui.close() // Close the UI. - close_count++ // Count each UI we close. - return close_count - -/** - * private - * - * Add a UI to the list of open UIs. - * - * required ui datum/tgui The UI to be added. - */ -/datum/controller/subsystem/tgui/proc/on_open(datum/tgui/ui) - var/src_object_key = "[REF(ui.src_object)]" - if(isnull(open_uis[src_object_key]) || !istype(open_uis[src_object_key], /list)) - open_uis[src_object_key] = list(ui.ui_key = list()) // Make a list for the ui_key and src_object. - else if(isnull(open_uis[src_object_key][ui.ui_key]) || !istype(open_uis[src_object_key][ui.ui_key], /list)) - open_uis[src_object_key][ui.ui_key] = list() // Make a list for the ui_key. - - // Append the UI to all the lists. - ui.user.open_uis |= ui - var/list/uis = open_uis[src_object_key][ui.ui_key] - uis |= ui - processing_uis |= ui - -/** - * private - * - * Remove a UI from the list of open UIs. - * - * required ui datum/tgui The UI to be removed. - * - * return bool If the UI was removed or not. - */ -/datum/controller/subsystem/tgui/proc/on_close(datum/tgui/ui) - var/src_object_key = "[REF(ui.src_object)]" - if(isnull(open_uis[src_object_key]) || !istype(open_uis[src_object_key], /list)) - return 0 // It wasn't open. - else if(isnull(open_uis[src_object_key][ui.ui_key]) || !istype(open_uis[src_object_key][ui.ui_key], /list)) - return 0 // It wasn't open. - - processing_uis.Remove(ui) // Remove it from the list of processing UIs. - if(ui.user) // If the user exists, remove it from them too. - ui.user.open_uis.Remove(ui) - var/Ukey = ui.ui_key - var/list/uis = open_uis[src_object_key][Ukey] // Remove it from the list of open UIs. - uis.Remove(ui) - if(!uis.len) - var/list/uiobj = open_uis[src_object_key] - uiobj.Remove(Ukey) - if(!uiobj.len) - open_uis.Remove(src_object_key) - - return 1 // Let the caller know we did it. - -/** - * private - * - * Handle client logout, by closing all their UIs. - * - * required user mob The mob which logged out. - * - * return int The number of UIs closed. - */ -/datum/controller/subsystem/tgui/proc/on_logout(mob/user) - return close_user_uis(user) - -/** - * private - * - * Handle clients switching mobs, by transferring their UIs. - * - * required user source The client's original mob. - * required user target The client's new mob. - * - * return bool If the UIs were transferred. - */ -/datum/controller/subsystem/tgui/proc/on_transfer(mob/source, mob/target) - if(!source || isnull(source.open_uis) || !istype(source.open_uis, /list) || open_uis.len == 0) - return 0 // The old mob had no open UIs. - - if(isnull(target.open_uis) || !istype(target.open_uis, /list)) - target.open_uis = list() // Create a list for the new mob if needed. - - for(var/datum/tgui/ui in source.open_uis) - ui.user = target // Inform the UIs of their new owner. - target.open_uis.Add(ui) // Transfer all the UIs. - - source.open_uis.Cut() // Clear the old list. - return 1 // Let the caller know we did it. diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm index 7971a940d4..5eacfd07fe 100644 --- a/code/modules/tgui/tgui.dm +++ b/code/modules/tgui/tgui.dm @@ -1,7 +1,6 @@ /** - * tgui - * - * /tg/station user interface library + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT */ /** @@ -14,34 +13,26 @@ var/datum/src_object /// The title of te UI. var/title - /// The ui_key of the UI. This allows multiple UIs for one src_object. - var/ui_key /// The window_id for browse() and onclose(). - var/window_id - /// The window width. - var/width = 0 - /// The window height - var/height = 0 + var/datum/tgui_window/window + /// Key that is used for remembering the window geometry. + var/window_key + /// Deprecated: Window size. + var/window_size /// The interface (template) to be used for this UI. var/interface /// Update the UI every MC tick. var/autoupdate = TRUE /// If the UI has been initialized yet. var/initialized = FALSE - /// The data (and datastructure) used to initialize the UI. - var/list/initial_data - /// The static data used to initialize the UI. - var/list/initial_static_data - /// Holder for the json string, that is sent during the initial update - var/_initial_update + /// Time of opening the window. + var/opened_at + /// Stops further updates when close() was called. + var/closing = FALSE /// The status/visibility of the UI. var/status = UI_INTERACTIVE /// Topic state used to determine status/interactability. var/datum/ui_state/state = null - /// The parent UI. - var/datum/tgui/master_ui - /// Children of this UI. - var/list/datum/tgui/children = list() /** * public @@ -50,38 +41,25 @@ * * required user mob The mob who opened/is using the UI. * required src_object datum The object or datum which owns the UI. - * required ui_key string The ui_key of the UI. * required interface string The interface used to render the UI. * optional title string The title of the UI. - * optional width int The window width. - * optional height int The window height. - * optional master_ui datum/tgui The parent UI. - * optional state datum/ui_state The state used to determine status. + * optional ui_x int Deprecated: Window width. + * optional ui_y int Deprecated: Window height. * * return datum/tgui The requested UI. */ -/datum/tgui/New(mob/user, datum/src_object, ui_key, interface, title, width = 0, height = 0, datum/tgui/master_ui = null, datum/ui_state/state = GLOB.default_state) +/datum/tgui/New(mob/user, datum/src_object, interface, title, ui_x, ui_y) + log_tgui(user, "new [interface] fancy [user.client.prefs.tgui_fancy]") src.user = user src.src_object = src_object - src.ui_key = ui_key - // DO NOT replace with \ref here. src_object could potentially be tagged - src.window_id = "[REF(src_object)]-[ui_key]" + src.window_key = "[REF(src_object)]-main" src.interface = interface - if(title) - src.title = sanitize(title) - if(width) - src.width = width - if(height) - src.height = height - - src.master_ui = master_ui - if(master_ui) - master_ui.children += src - src.state = state - - var/datum/asset/assets = get_asset_datum(/datum/asset/group/tgui) - assets.send(user) + src.title = title + src.state = src_object.ui_state() + // Deprecated + if(ui_x && ui_y) + src.window_size = list(ui_x, ui_y) /** * public @@ -90,85 +68,53 @@ */ /datum/tgui/proc/open() if(!user.client) - return // Bail if there is no client. - - update_status(push = FALSE) // Update the window status. + return null + if(window) + return null + process_status() if(status < UI_UPDATE) - return // Bail if we're not supposed to open. - - // Build window options - var/window_options = "can_minimize=0;auto_format=0;" - // If we have a width and height, use them. - if(width && height) - window_options += "size=[width]x[height];" - // Remove titlebar and resize handles for a fancy window - if(user.client.prefs.tgui_fancy) - window_options += "titlebar=0;can_resize=0;" + return null + window = SStgui.request_pooled_window(user) + if(!window) + return null + opened_at = world.time + window.acquire_lock(src) + if(!window.is_ready()) + window.initialize(inline_assets = list( + get_asset_datum(/datum/asset/simple/tgui), + )) else - window_options += "titlebar=1;can_resize=1;" - - // Generate page html - var/html - html = SStgui.basehtml - // Allow the src object to override the html if needed - html = src_object.ui_base_html(html) - // Replace template tokens with important UI data - // NOTE: Intentional \ref usage; tgui datums can't/shouldn't - // be tagged, so this is an effective unwrap - html = replacetextEx(html, "\[tgui:ref]", "\ref[src]") - - // Open the window. - user << browse(html, "window=[window_id];[window_options]") - - // Instruct the client to signal UI when the window is closed. - // NOTE: Intentional \ref usage; tgui datums can't/shouldn't - // be tagged, so this is an effective unwrap - winset(user, window_id, "on-close=\"uiclose \ref[src]\"") - - // Pre-fetch initial state while browser is still loading in - // another thread - if(!initial_data) { - initial_data = src_object.ui_data(user) - } - if(!initial_static_data) { - initial_static_data = src_object.ui_static_data(user) - } - _initial_update = url_encode(get_json(initial_data, initial_static_data)) - + window.send_message("ping") + window.send_asset(get_asset_datum(/datum/asset/simple/fontawesome)) + for(var/datum/asset/asset in src_object.ui_assets(user)) + window.send_asset(asset) + window.send_message("update", get_payload( + with_data = TRUE, + with_static_data = TRUE)) SStgui.on_open(src) /** * public * - * Reinitialize the UI. - * (Possibly with a new interface and/or data). + * Close the UI. * - * optional template string The name of the new interface. - * optional data list The new initial data. + * optional can_be_suspended bool */ -/datum/tgui/proc/reinitialize(interface, list/data, list/static_data) - if(interface) - src.interface = interface - if(data) - initial_data = data - if(static_data) - initial_static_data = static_data - open() - -/** - * public - * - * Close the UI, and all its children. - */ -/datum/tgui/proc/close() - user << browse(null, "window=[window_id]") // Close the window. - src_object.ui_close(user) - SStgui.on_close(src) - for(var/datum/tgui/child in children) // Loop through and close all children. - child.close() - children.Cut() +/datum/tgui/proc/close(can_be_suspended = TRUE) + if(closing) + return + closing = TRUE + // If we don't have window_id, open proc did not have the opportunity + // to finish, therefore it's safe to skip this whole block. + if(window) + // Windows you want to keep are usually blue screens of death + // and we want to keep them around, to allow user to read + // the error message properly. + window.release_lock() + window.close(can_be_suspended) + src_object.ui_close(user) + SStgui.on_close(src) state = null - master_ui = null qdel(src) /** @@ -176,187 +122,173 @@ * * Enable/disable auto-updating of the UI. * - * required state bool Enable/disable auto-updating. + * required value bool Enable/disable auto-updating. */ -/datum/tgui/proc/set_autoupdate(state = TRUE) - autoupdate = state +/datum/tgui/proc/set_autoupdate(autoupdate) + src.autoupdate = autoupdate + +/** + * public + * + * Replace current ui.state with a new one. + * + * required state datum/ui_state/state Next state + */ +/datum/tgui/proc/set_state(datum/ui_state/state) + src.state = state + +/** + * public + * + * Makes an asset available to use in tgui. + * + * required asset datum/asset + */ +/datum/tgui/proc/send_asset(datum/asset/asset) + if(!window) + CRASH("send_asset() can only be called after open().") + window.send_asset(asset) + +/** + * public + * + * Send a full update to the client (includes static data). + * + * optional custom_data list Custom data to send instead of ui_data. + * optional force bool Send an update even if UI is not interactive. + */ +/datum/tgui/proc/send_full_update(custom_data, force) + if(!user.client || !initialized || closing) + return + var/should_update_data = force || status >= UI_UPDATE + window.send_message("update", get_payload( + custom_data, + with_data = should_update_data, + with_static_data = TRUE)) + +/** + * public + * + * Send a partial update to the client (excludes static data). + * + * optional custom_data list Custom data to send instead of ui_data. + * optional force bool Send an update even if UI is not interactive. + */ +/datum/tgui/proc/send_update(custom_data, force) + if(!user.client || !initialized || closing) + return + var/should_update_data = force || status >= UI_UPDATE + window.send_message("update", get_payload( + custom_data, + with_data = should_update_data)) /** * private * * Package the data to send to the UI, as JSON. - * This includes the UI data and config_data. * - * return string The packaged JSON. + * return list */ -/datum/tgui/proc/get_json(list/data, list/static_data) +/datum/tgui/proc/get_payload(custom_data, with_data, with_static_data) var/list/json_data = list() - json_data["config"] = list( "title" = title, "status" = status, "interface" = interface, - "fancy" = user.client.prefs.tgui_fancy, - "locked" = user.client.prefs.tgui_lock, - "observer" = isobserver(user), - "window" = window_id, - // NOTE: Intentional \ref usage; tgui datums can't/shouldn't - // be tagged, so this is an effective unwrap - "ref" = "\ref[src]" + "window" = list( + "key" = window_key, + "size" = window_size, + "fancy" = user.client.prefs.tgui_fancy, + "locked" = user.client.prefs.tgui_lock, + ), + "user" = list( + "name" = "[user]", + "ckey" = "[user.ckey]", + "observer" = isobserver(user), + ), ) - - if(!isnull(data)) + var/data = custom_data || with_data && src_object.ui_data(user) + if(data) json_data["data"] = data - if(!isnull(static_data)) + var/static_data = with_static_data && src_object.ui_static_data(user) + if(static_data) json_data["static_data"] = static_data - - // Send shared states if(src_object.tgui_shared_states) json_data["shared"] = src_object.tgui_shared_states - - // Generate the JSON. - var/json = json_encode(json_data) - // Strip #255/improper. - json = replacetext(json, "\proper", "") - json = replacetext(json, "\improper", "") - return json + return json_data /** * private * - * Handle clicks from the UI. - * Call the src_object's ui_act() if status is UI_INTERACTIVE. - * If the src_object's ui_act() returns 1, update all UIs attacked to it. - */ -/datum/tgui/Topic(href, href_list) - if(user != usr) - return // Something is not right here. - - var/action = href_list["action"] - var/params = href_list; params -= "action" - - switch(action) - if("tgui:initialize") - user << output(_initial_update, "[window_id].browser:update") - initialized = TRUE - if("tgui:setSharedState") - // Update the window state. - update_status(push = FALSE) - // Bail if UI is not interactive or usr calling Topic - // is not the UI user. - if(status != UI_INTERACTIVE) - return - var/key = params["key"] - var/value = params["value"] - if(!src_object.tgui_shared_states) - src_object.tgui_shared_states = list() - src_object.tgui_shared_states[key] = value - SStgui.update_uis(src_object) - if("tgui:setFancy") - var/value = text2num(params["value"]) - user.client.prefs.tgui_fancy = value - if("tgui:log") - // Force window to show frills on fatal errors - if(params["fatal"]) - winset(user, window_id, "titlebar=1;can-resize=1;size=600x600") - log_message(params["log"]) - if("tgui:link") - user << link(params["url"]) - else - // Update the window state. - update_status(push = FALSE) - // Call ui_act() on the src_object. - if(src_object.ui_act(action, params, src, state)) - // Update if the object requested it. - SStgui.update_uis(src_object) - -/** - * private - * - * Update the UI. - * Only updates the data if update is true, otherwise only updates the status. - * - * optional force bool If the UI should be forced to update. + * Run an update cycle for this UI. Called internally by SStgui + * every second or so. */ /datum/tgui/process(force = FALSE) + if(closing) + return var/datum/host = src_object.ui_host(user) - if(!src_object || !host || !user) // If the object or user died (or something else), abort. + // If the object or user died (or something else), abort. + if(!src_object || !host || !user || !window) + close(can_be_suspended = FALSE) + return + // Validate ping + if(!initialized && world.time - opened_at > TGUI_PING_TIMEOUT) + log_tgui(user, \ + "Error: Zombie window detected, killing it with fire.\n" \ + + "window_id: [window.id]\n" \ + + "opened_at: [opened_at]\n" \ + + "world.time: [world.time]") + close(can_be_suspended = FALSE) + return + // Update through a normal call to ui_interact + if(status != UI_DISABLED && (autoupdate || force)) + src_object.ui_interact(user, src) + return + // Update status only + var/needs_update = process_status() + if(status <= UI_CLOSE) close() return - - if(status && (force || autoupdate)) - update() // Update the UI if the status and update settings allow it. - else - update_status(push = TRUE) // Otherwise only update status. + if(needs_update) + window.send_message("update", get_payload()) /** * private * - * Push data to an already open UI. - * - * required data list The data to send. - * optional force bool If the update should be sent regardless of state. + * Updates the status, and returns TRUE if status has changed. */ -/datum/tgui/proc/push_data(data, static_data, force = FALSE) - // Update the window state. - update_status(push = FALSE) - // Cannot update UI if it is not set up yet. - if(!initialized) - return - // Cannot update UI, we have no visibility. - if(status <= UI_DISABLED && !force) - return - // Send the new JSON to the update() Javascript function. - user << output( - url_encode(get_json(data, static_data)), - "[window_id].browser:update") +/datum/tgui/proc/process_status() + var/prev_status = status + status = src_object.ui_status(user, state) + return prev_status != status /** * private * - * Updates the UI by interacting with the src_object again, which will hopefully - * call try_ui_update on it. - * - * optional force_open bool If force_open should be passed to ui_interact. + * Callback for handling incoming tgui messages. */ -/datum/tgui/proc/update(force_open = FALSE) - src_object.ui_interact(user, ui_key, src, force_open, master_ui, state) - -/** - * private - * - * Update the status/visibility of the UI for its user. - * - * optional push bool Push an update to the UI (an update is always sent for UI_DISABLED). - */ -/datum/tgui/proc/update_status(push = FALSE) - var/status = src_object.ui_status(user, state) - if(master_ui) - status = min(status, master_ui.status) - set_status(status, push) - if(status == UI_CLOSE) - close() - -/** - * private - * - * Set the status/visibility of the UI. - * - * required status int The status to set (UI_CLOSE/UI_DISABLED/UI_UPDATE/UI_INTERACTIVE). - * optional push bool Push an update to the UI (an update is always sent for UI_DISABLED). - */ -/datum/tgui/proc/set_status(status, push = FALSE) - // Only update if status has changed. - if(src.status != status) - if(src.status == UI_DISABLED) - src.status = status - if(push) - update() - else - src.status = status - // Update if the UI just because disabled, or a push is requested. - if(status == UI_DISABLED || push) - push_data(null, force = TRUE) - -/datum/tgui/proc/log_message(message) - log_tgui("[user] ([user.ckey]) using \"[title]\":\n[message]") +/datum/tgui/proc/on_message(type, list/payload, list/href_list) + // Pass act type messages to ui_act + if(type && copytext(type, 1, 5) == "act/") + process_status() + if(src_object.ui_act(copytext(type, 5), payload, src, state)) + SStgui.update_uis(src_object) + return FALSE + switch(type) + if("ready") + initialized = TRUE + if("pingReply") + initialized = TRUE + if("suspend") + close(can_be_suspended = TRUE) + if("close") + close(can_be_suspended = FALSE) + if("log") + if(href_list["fatal"]) + close(can_be_suspended = FALSE) + if("setSharedState") + if(status != UI_INTERACTIVE) + return + LAZYINITLIST(src_object.tgui_shared_states) + src_object.tgui_shared_states[href_list["key"]] = href_list["value"] + SStgui.update_uis(src_object) diff --git a/code/modules/tgui/tgui_window.dm b/code/modules/tgui/tgui_window.dm new file mode 100644 index 0000000000..3f271163c9 --- /dev/null +++ b/code/modules/tgui/tgui_window.dm @@ -0,0 +1,238 @@ +/** + * Copyright (c) 2020 Aleksej Komarov + * SPDX-License-Identifier: MIT + */ + +/datum/tgui_window + var/id + var/client/client + var/pooled + var/pool_index + var/status = TGUI_WINDOW_CLOSED + var/locked = FALSE + var/datum/tgui/locked_by + var/fatally_errored = FALSE + var/message_queue + var/sent_assets = list() + +/** + * public + * + * Create a new tgui window. + * + * required client /client + * required id string A unique window identifier. + */ +/datum/tgui_window/New(client/client, id, pooled = FALSE) + src.id = id + src.client = client + src.pooled = pooled + if(pooled) + client.tgui_windows[id] = src + src.pool_index = TGUI_WINDOW_INDEX(id) + +/** + * public + * + * Initializes the window with a fresh page. Puts window into the "loading" + * state. You can begin sending messages right after initializing. Messages + * will be put into the queue until the window finishes loading. + * + * optional inline_assets list List of assets to inline into the html. + */ +/datum/tgui_window/proc/initialize(inline_assets = list()) + log_tgui(client, "[id]/initialize") + if(!client) + return + status = TGUI_WINDOW_LOADING + fatally_errored = FALSE + message_queue = null + // Build window options + var/options = "file=[id].html;can_minimize=0;auto_format=0;" + // Remove titlebar and resize handles for a fancy window + if(client.prefs.tgui_fancy) + options += "titlebar=0;can_resize=0;" + else + options += "titlebar=1;can_resize=1;" + // Generate page html + var/html = SStgui.basehtml + html = replacetextEx(html, "\[tgui:windowId]", id) + // Process inline assets + var/inline_styles = "" + var/inline_scripts = "" + for(var/datum/asset/asset in inline_assets) + var/mappings = asset.get_url_mappings() + for(var/name in mappings) + var/url = mappings[name] + // Not urlencoding since asset strings are considered safe + if(copytext(name, -4) == ".css") + inline_styles += "\n" + else if(copytext(name, -3) == ".js") + inline_scripts += "\n" + asset.send() + html = replacetextEx(html, "\n", inline_styles) + html = replacetextEx(html, "\n", inline_scripts) + // Open the window + client << browse(html, "window=[id];[options]") + // Instruct the client to signal UI when the window is closed. + winset(client, id, "on-close=\"uiclose [id]\"") + +/** + * public + * + * Checks if the window is ready to receive data. + * + * return bool + */ +/datum/tgui_window/proc/is_ready() + return status == TGUI_WINDOW_READY + +/** + * public + * + * Checks if the window can be sanely suspended. + * + * return bool + */ +/datum/tgui_window/proc/can_be_suspended() + return !fatally_errored \ + && pooled \ + && pool_index > 0 \ + && pool_index <= TGUI_WINDOW_SOFT_LIMIT \ + && status == TGUI_WINDOW_READY + +/** + * public + * + * Acquire the window lock. Pool will not be able to provide this window + * to other UIs for the duration of the lock. + * + * Can be given an optional tgui datum, which will hook its on_message + * callback into the message stream. + * + * optional ui /datum/tgui + */ +/datum/tgui_window/proc/acquire_lock(datum/tgui/ui) + locked = TRUE + locked_by = ui + +/** + * Release the window lock. + */ +/datum/tgui_window/proc/release_lock() + // Clean up assets sent by tgui datum which requested the lock + if(locked) + sent_assets = list() + locked = FALSE + locked_by = null + +/** + * public + * + * Close the UI. + * + * optional can_be_suspended bool + */ +/datum/tgui_window/proc/close(can_be_suspended = TRUE) + if(!client) + return + if(can_be_suspended && can_be_suspended()) + log_tgui(client, "[id]/close: suspending") + status = TGUI_WINDOW_READY + send_message("suspend") + return + log_tgui(client, "[id]/close") + release_lock() + status = TGUI_WINDOW_CLOSED + message_queue = null + // Do not close the window to give user some time + // to read the error message. + if(!fatally_errored) + client << browse(null, "window=[id]") + +/** + * public + * + * Sends a message to tgui window. + * + * required type string Message type + * required payload list Message payload + * optional force bool Send regardless of the ready status. + */ +/datum/tgui_window/proc/send_message(type, list/payload, force) + if(!client) + return + var/message = json_encode(list( + "type" = type, + "payload" = payload, + )) + // Strip #255/improper. + message = replacetext(message, "\proper", "") + message = replacetext(message, "\improper", "") + // Pack for sending via output() + message = url_encode(message) + // Place into queue if window is still loading + if(!force && status != TGUI_WINDOW_READY) + if(!message_queue) + message_queue = list() + message_queue += list(message) + return + client << output(message, "[id].browser:update") + +/** + * public + * + * Makes an asset available to use in tgui. + * + * required asset datum/asset + */ +/datum/tgui_window/proc/send_asset(datum/asset/asset) + if(!client || !asset) + return + if(istype(asset, /datum/asset/spritesheet)) + var/datum/asset/spritesheet/spritesheet = asset + send_message("asset/stylesheet", spritesheet.css_filename()) + send_message("asset/mappings", asset.get_url_mappings()) + sent_assets += list(asset) + asset.send(client) + +/** + * private + * + * Sends queued messages if the queue wasn't empty. + */ +/datum/tgui_window/proc/flush_message_queue() + if(!client || !message_queue) + return + for(var/message in message_queue) + client << output(message, "[id].browser:update") + message_queue = null + +/** + * private + * + * Callback for handling incoming tgui messages. + */ +/datum/tgui_window/proc/on_message(type, list/payload, list/href_list) + switch(type) + if("ready") + // Status can be READY if user has refreshed the window. + if(status == TGUI_WINDOW_READY) + // Resend the assets + for(var/asset in sent_assets) + send_asset(asset) + status = TGUI_WINDOW_READY + if("log") + if(href_list["fatal"]) + fatally_errored = TRUE + // Pass message to UI that requested the lock + if(locked && locked_by) + locked_by.on_message(type, payload, href_list) + flush_message_queue() + return + // If not locked, handle these message types + switch(type) + if("suspend") + close(can_be_suspended = TRUE) + if("close") + close(can_be_suspended = FALSE)