diff --git a/code/__defines/tgui.dm b/code/__defines/tgui.dm
new file mode 100644
index 0000000000..0cf23d7809
--- /dev/null
+++ b/code/__defines/tgui.dm
@@ -0,0 +1,21 @@
+/// Maximum number of windows that can be suspended/reused
+#define TGUI_WINDOW_SOFT_LIMIT 5
+/// Maximum number of open windows
+#define TGUI_WINDOW_HARD_LIMIT 9
+
+/// Maximum ping timeout allowed to detect zombie windows
+#define TGUI_PING_TIMEOUT 4 SECONDS
+
+/// Window does not exist
+#define TGUI_WINDOW_CLOSED 0
+/// Window was just opened, but is still not ready to be sent data
+#define TGUI_WINDOW_LOADING 1
+/// Window is free and ready to receive data
+#define TGUI_WINDOW_READY 2
+/// Window is in use by a tgui datum
+#define TGUI_WINDOW_ACTIVE 3
+
+/// Get a window id based on the provided pool index
+#define TGUI_WINDOW_ID(index) "tgui-window-[index]"
+/// Get a pool index of the provided window id
+#define TGUI_WINDOW_INDEX(window_id) text2num(copytext(window_id, 13))
\ No newline at end of file
diff --git a/code/_helpers/logging.dm b/code/_helpers/logging.dm
index 2aea0d47d0..deb4c8c670 100644
--- a/code/_helpers/logging.dm
+++ b/code/_helpers/logging.dm
@@ -176,8 +176,18 @@
/proc/log_unit_test(text)
to_world_log("## UNIT_TEST: [text]")
-/proc/log_tgui(text)
- WRITE_LOG(diary, "\[[time_stamp()]]TGUI: [text]")
+/proc/log_tgui(user_or_client, text)
+ var/entry = ""
+ if(!user_or_client)
+ entry += "no user"
+ else if(istype(user_or_client, /mob))
+ var/mob/user = user_or_client
+ entry += "[user.ckey] (as [user])"
+ else if(istype(user_or_client, /client))
+ var/client/client = user_or_client
+ entry += "[client.ckey]"
+ entry += ":\n[text]"
+ WRITE_LOG(diary, entry)
/proc/report_progress(var/progress_message)
admin_notice("[progress_message]", R_DEBUG)
diff --git a/code/controllers/subsystems/tgui.dm b/code/controllers/subsystems/tgui.dm
index d30a47c655..91533a7783 100644
--- a/code/controllers/subsystems/tgui.dm
+++ b/code/controllers/subsystems/tgui.dm
@@ -12,10 +12,14 @@ 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 and ui_key.
+ 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')
@@ -24,24 +28,92 @@ SUBSYSTEM_DEF(tgui)
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)
+ return null
+ return window
+
+/**
+ * public
+ *
+ * Force closes all tgui windows.
+ *
+ * required user mob
+ */
+/datum/controller/subsystem/tgui/proc/force_close_all_windows(mob/user)
+ 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)
+ // 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
*
@@ -50,24 +122,28 @@ SUBSYSTEM_DEF(tgui)
* 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.tgui_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.
+/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 <= STATUS_CLOSE)
+ ui.close()
+ return null
+ ui.send_update()
+ return ui
/**
* private
@@ -80,17 +156,15 @@ SUBSYSTEM_DEF(tgui)
*
* 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))
+/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 // 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
+ for(var/datum/tgui/ui in open_uis_by_src[key]) // Find UIs for this object.
+ // Make sure we have the right user
+ if(ui.user == user)
return ui
-
return null // Couldn't find a UI!
/**
@@ -103,17 +177,16 @@ SUBSYSTEM_DEF(tgui)
* 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.tgui_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
+ var/count = 0
+ var/key = "[REF(src_object)]"
+ if(isnull(open_uis_by_src[key]) || !istype(open_uis_by_src[key], /list))
+ return count // Couldn't find any UIs for this object.
+ for(var/datum/tgui/ui in open_uis_by_src[key])
+ // Check the UI is valid.
+ if(ui && ui.src_object && ui.user && ui.src_object.tgui_host(ui.user))
+ ui.process(force = 1) // Update the UI.
+ count++ // Count each UI we update.
+ return count
/**
* private
@@ -125,34 +198,32 @@ SUBSYSTEM_DEF(tgui)
* 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.tgui_host(ui.user)) // Check the UI is valid.
- ui.close() // Close the UI.
- close_count++ // Count each UI we close.
- return close_count
+ 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])
+ if(ui && ui.src_object && ui.user && ui.src_object.tgui_host(ui.user)) // Check the UI is valid.
+ ui.close() // Close the UI.
+ count++ // Count each UI we close.
+ return count
/**
* private
*
- * Close *ALL* UIs
+ * 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/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.tgui_host(ui.user)) // Check the UI is valid.
- ui.close() // Close the UI.
- close_count++ // Count each UI we close.
- return close_count
+ var/count = 0
+ for(var/key in open_uis_by_src)
+ for(var/datum/tgui/ui in open_uis_by_src[key])
+ if(ui && ui.src_object && ui.user && ui.src_object.tgui_host(ui.user)) // Check the UI is valid.
+ ui.close() // Close the UI.
+ count++ // Count each UI we close.
+ return count
/**
* private
@@ -161,20 +232,18 @@ SUBSYSTEM_DEF(tgui)
*
* 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_tguis) || !istype(user.open_tguis, /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_tguis)
- 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
+/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
/**
* private
@@ -183,20 +252,18 @@ SUBSYSTEM_DEF(tgui)
*
* 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_tguis) || !istype(user.open_tguis, /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_tguis)
- 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
+/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
@@ -206,17 +273,13 @@ SUBSYSTEM_DEF(tgui)
* 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_tguis |= ui
- var/list/uis = open_uis[src_object_key][ui.ui_key]
+ 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
- processing_uis |= ui
+ open_uis |= ui
/**
* private
@@ -228,25 +291,19 @@ SUBSYSTEM_DEF(tgui)
* 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 FALSE // 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 FALSE // 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_tguis.Remove(ui)
- var/Ukey = ui.ui_key
- var/list/uis = open_uis[src_object_key][Ukey] // Remove it from the list of open UIs.
+ 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(!uis.len)
- var/list/uiobj = open_uis[src_object_key]
- uiobj.Remove(Ukey)
- if(!uiobj.len)
- open_uis.Remove(src_object_key)
-
- return TRUE // Let the caller know we did it.
+ if(length(uis) == 0)
+ open_uis_by_src.Remove(key)
+ return TRUE
/**
* private
@@ -271,15 +328,16 @@ SUBSYSTEM_DEF(tgui)
* return bool If the UIs were transferred.
**/
/datum/controller/subsystem/tgui/proc/on_transfer(mob/source, mob/target)
- if(!source || isnull(source.open_tguis) || !istype(source.open_tguis, /list) || open_uis.len == 0)
- return FALSE // The old mob had no open UIs.
-
- if(isnull(target.open_tguis) || !istype(target.open_tguis, /list))
- target.open_tguis = list() // Create a list for the new mob if needed.
-
- for(var/datum/tgui/ui in source.open_tguis)
- ui.user = target // Inform the UIs of their new owner.
- target.open_tguis.Add(ui) // Transfer all the UIs.
-
- source.open_tguis.Cut() // Clear the old list.
- return TRUE // Let the caller know we did it.
+ // 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
\ No newline at end of file
diff --git a/code/game/machinery/computer/camera.dm b/code/game/machinery/computer/camera.dm
index 897db600a7..f87d69a9a7 100644
--- a/code/game/machinery/computer/camera.dm
+++ b/code/game/machinery/computer/camera.dm
@@ -24,8 +24,8 @@
QDEL_NULL(camera)
return ..()
-/obj/machinery/computer/security/tgui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/tgui_state/state = GLOB.tgui_default_state)
- camera.tgui_interact(user, ui_key, ui, force_open, master_ui, state)
+/obj/machinery/computer/security/tgui_interact(mob/user, datum/tgui/ui = null)
+ camera.tgui_interact(user, ui)
/obj/machinery/computer/security/attack_hand(mob/user)
add_fingerprint(user)
diff --git a/code/game/machinery/computer/crew.dm b/code/game/machinery/computer/crew.dm
index 03453a8964..1af31afe80 100644
--- a/code/game/machinery/computer/crew.dm
+++ b/code/game/machinery/computer/crew.dm
@@ -28,8 +28,8 @@
return
tgui_interact(user)
-/obj/machinery/computer/crew/tgui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = 0, datum/tgui/master_ui = null, datum/tgui_state/state = GLOB.tgui_default_state)
- crew_monitor.tgui_interact(user, ui_key, ui, force_open)
+/obj/machinery/computer/crew/tgui_interact(mob/user, datum/tgui/ui = null)
+ crew_monitor.tgui_interact(user, ui)
/obj/machinery/computer/crew/interact(mob/user)
crew_monitor.tgui_interact(user)
diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm
index 36f11e736f..0fe7af5a77 100644
--- a/code/modules/client/client procs.dm
+++ b/code/modules/client/client procs.dm
@@ -46,6 +46,10 @@
//del(usr)
return
+ // Tgui Topic middleware
+ if(!tgui_Topic(href_list))
+ return
+
//Admin PM
if(href_list["priv_msg"])
var/client/C = locate(href_list["priv_msg"])
diff --git a/code/modules/clothing/glasses/hud_vr.dm b/code/modules/clothing/glasses/hud_vr.dm
index 2eb45e02ff..ce49c88949 100644
--- a/code/modules/clothing/glasses/hud_vr.dm
+++ b/code/modules/clothing/glasses/hud_vr.dm
@@ -85,12 +85,12 @@
These have been upgraded with medical records access and virus database integration."
mode = "med"
action_button_name = "AR Console (Crew Monitor)"
- tgarscreen_path = /datum/tgui_module/crew_monitor
+ tgarscreen_path = /datum/tgui_module/crew_monitor/glasses
enables_planes = list(VIS_CH_ID,VIS_CH_HEALTH_VR,VIS_CH_STATUS_R,VIS_CH_BACKUP,VIS_AUGMENTED)
ar_interact(var/mob/living/carbon/human/user)
if(tgarscreen)
- tgarscreen.tgui_interact(user, state = GLOB.tgui_glasses_state)
+ tgarscreen.tgui_interact(user)
return 1
/obj/item/clothing/glasses/omnihud/sec
diff --git a/code/modules/mob/living/silicon/subystems.dm b/code/modules/mob/living/silicon/subystems.dm
index b498021a16..e5d50d5824 100644
--- a/code/modules/mob/living/silicon/subystems.dm
+++ b/code/modules/mob/living/silicon/subystems.dm
@@ -2,7 +2,7 @@
var/register_alarms = 1
var/datum/nano_module/alarm_monitor/all/alarm_monitor
var/datum/nano_module/atmos_control/atmos_control
- var/datum/tgui_module/crew_monitor/crew_monitor
+ var/datum/tgui_module/crew_monitor/robot/crew_monitor
var/datum/nano_module/law_manager/law_manager
var/datum/nano_module/power_monitor/power_monitor
var/datum/nano_module/rcon/rcon
@@ -67,7 +67,7 @@
set category = "Subystems"
set name = "Crew Monitor"
- crew_monitor.tgui_interact(usr, state = GLOB.tgui_self_state)
+ crew_monitor.tgui_interact(usr)
/****************
* Law Manager *
diff --git a/code/modules/nifsoft/software/06_screens.dm b/code/modules/nifsoft/software/06_screens.dm
index 85861a52be..64a1faac5c 100644
--- a/code/modules/nifsoft/software/06_screens.dm
+++ b/code/modules/nifsoft/software/06_screens.dm
@@ -5,7 +5,7 @@
access = access_medical
cost = 625
p_drain = 0.025
- var/datum/tgui_module/crew_monitor/arscreen
+ var/datum/tgui_module/crew_monitor/nif/arscreen
New()
..()
@@ -17,7 +17,7 @@
activate()
if((. = ..()))
- arscreen.tgui_interact(nif.human, "main", null, 1, state = GLOB.tgui_nif_state)
+ arscreen.tgui_interact(nif.human)
return TRUE
deactivate()
diff --git a/code/modules/recycling/disposal.dm b/code/modules/recycling/disposal.dm
index 5924b22e34..fa19b46e1f 100644
--- a/code/modules/recycling/disposal.dm
+++ b/code/modules/recycling/disposal.dm
@@ -242,10 +242,10 @@
return
// user interaction
-/obj/machinery/disposal/tgui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = 0, datum/tgui/master_ui = null, datum/tgui_state/state = GLOB.tgui_default_state)
- ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
+/obj/machinery/disposal/tgui_interact(mob/user, datum/tgui/ui = null)
+ ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
- ui = new(user, src, ui_key, "DisposalBin", name, 300, 250, master_ui, state)
+ ui = new(user, src, "DisposalBin")
ui.open()
/obj/machinery/disposal/tgui_data(mob/user)
diff --git a/code/modules/tgui/external.dm b/code/modules/tgui/external.dm
index c76d28e1d7..7e75f9343c 100644
--- a/code/modules/tgui/external.dm
+++ b/code/modules/tgui/external.dm
@@ -11,14 +11,10 @@
* 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/tgui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/tgui_state/state = GLOB.tgui_default_state)
+/datum/proc/tgui_interact(mob/user, datum/tgui/ui = null)
return FALSE // Not implemented.
/**
@@ -38,10 +34,10 @@
* 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.
*
@@ -53,18 +49,19 @@
/**
* 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_tgui_static_data(mob/user, datum/tgui/ui, ui_key = "main")
- ui = SStgui.try_update_ui(user, src, ui_key, ui)
+/datum/proc/update_tgui_static_data(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
// If there was no ui to update, there's no static data to update either.
if(!ui)
- return
- ui.push_data(null, tgui_static_data(user), TRUE)
+ ui = SStgui.get_open_ui(user, src)
+ if(ui)
+ ui.send_full_update()
/**
* public
@@ -86,17 +83,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/tgui_base_html(html)
- return html
+/datum/proc/ui_assets(mob/user)
+ return list()
/**
* private
@@ -108,6 +100,15 @@
/datum/proc/tgui_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/tgui_state(mob/user)
+ return GLOB.tgui_default_state
+
/**
* global
*
@@ -120,9 +121,17 @@
/**
* global
*
- * Used to track UIs for a mob.
+ * Tracks open UIs for a user.
*/
-/mob/var/list/open_tguis = list()
+/mob/var/list/tgui_open_uis = list()
+
+/**
+ * global
+ *
+ * Tracks open windows for a user.
+ */
+/client/var/list/tgui_windows = list()
+
/**
* public
*
@@ -139,17 +148,44 @@
*
* required uiref ref The UI that was closed.
*/
-/client/verb/tguiclose(ref as text)
+/client/verb/tguiclose(window_id as text)
// Name the verb, and hide it from the user panel.
set name = "uiclose"
set hidden = TRUE
- // Get the UI based on the ref.
- var/datum/tgui/ui = locate(ref)
+ var/mob/user = src && src.mob
+ if(!user)
+ return
+ // Close all tgui datums based on window_id.
+ SStgui.force_close_window(user, window_id)
- // 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/modules/camera.dm b/code/modules/tgui/modules/camera.dm
index 20c6aeb0ef..301d43d9d5 100644
--- a/code/modules/tgui/modules/camera.dm
+++ b/code/modules/tgui/modules/camera.dm
@@ -76,9 +76,9 @@
qdel(cam_foreground)
return ..()
-/datum/tgui_module/camera/tgui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/tgui_state/state = GLOB.tgui_default_state)
+/datum/tgui_module/camera/tgui_interact(mob/user, datum/tgui/ui = null)
// Update UI
- ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
+ ui = SStgui.try_update_ui(user, src, ui)
// Show static if can't use the camera
if(!active_camera?.can_use())
show_camera_static()
@@ -99,7 +99,7 @@
user.client.register_map_obj(cam_background)
user.client.register_map_obj(cam_foreground)
// Open UI
- ui = new(user, src, ui_key, tgui_id, name, 870, 708, master_ui, state)
+ ui = new(user, src, tgui_id, name)
ui.open()
/datum/tgui_module/camera/tgui_data()
@@ -256,8 +256,8 @@
/datum/tgui_module/camera/ntos
tgui_id = "NtosCameraConsole"
-/datum/tgui_module/camera/ntos/tgui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/tgui_state/state = GLOB.tgui_ntos_state)
- . = ..(user, ui_key, ui, force_open, master_ui, GLOB.tgui_ntos_state)
+/datum/tgui_module/camera/ntos/tgui_state()
+ return GLOB.tgui_ntos_state
/datum/tgui_module/camera/ntos/tgui_static_data()
. = ..()
diff --git a/code/modules/tgui/modules/crew_monitor.dm b/code/modules/tgui/modules/crew_monitor.dm
index 5ab6de92a9..4caaf31e8c 100644
--- a/code/modules/tgui/modules/crew_monitor.dm
+++ b/code/modules/tgui/modules/crew_monitor.dm
@@ -2,7 +2,7 @@
name = "Crew monitor"
tgui_id = "CrewMonitor"
-/datum/tgui_module/crew_monitor/tgui_act(action, params)
+/datum/tgui_module/crew_monitor/tgui_act(action, params, datum/tgui/ui)
if(..())
return TRUE
@@ -19,8 +19,11 @@
if(hassensorlevel(H, SUIT_SENSOR_TRACKING))
AI.ai_actual_track(H)
return TRUE
+ if("setZLevel")
+ ui.set_map_z_level(params["mapZLevel"])
+ SStgui.update_uis(src)
-/datum/tgui_module/crew_monitor/tgui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = 0, datum/tgui/master_ui = null, datum/tgui_state/state = GLOB.tgui_default_state)
+/datum/tgui_module/crew_monitor/tgui_interact(mob/user, datum/tgui/ui = null)
var/z = get_z(user)
var/list/map_levels = using_map.get_map_levels(z, TRUE, om_range = DEFAULT_OVERMAP_RANGE)
@@ -30,9 +33,9 @@
ui.close()
return null
- ui = SStgui.try_update_ui(user, src, ui_key, ui, force_open)
+ ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
- ui = new(user, src, ui_key, tgui_id, name, 800, 600, master_ui, state)
+ ui = new(user, src, tgui_id)
ui.autoupdate = TRUE
ui.open()
@@ -55,8 +58,8 @@
/datum/tgui_module/crew_monitor/ntos
tgui_id = "NtosCrewMonitor"
-/datum/tgui_module/crew_monitor/ntos/tgui_interact(mob/user, ui_key = "main", datum/tgui/ui = null, force_open = FALSE, datum/tgui/master_ui = null, datum/tgui_state/state = GLOB.tgui_ntos_state)
- . = ..(user, ui_key, ui, force_open, master_ui, GLOB.tgui_ntos_state)
+/datum/tgui_module/crew_monitor/ntos/tgui_state(mob/user)
+ return GLOB.tgui_ntos_state
/datum/tgui_module/crew_monitor/ntos/tgui_static_data()
. = ..()
@@ -80,3 +83,18 @@
if(action == "PC_minimize")
host.computer.minimize_program(usr)
return TRUE
+
+// Subtype for glasses_state
+/datum/tgui_module/crew_monitor/glasses
+/datum/tgui_module/crew_monitor/glasses/tgui_state(mob/user)
+ return GLOB.tgui_glasses_state
+
+// Subtype for self_state
+/datum/tgui_module/crew_monitor/robot
+/datum/tgui_module/crew_monitor/robot/tgui_state(mob/user)
+ return GLOB.tgui_self_state
+
+// Subtype for nif_state
+/datum/tgui_module/crew_monitor/nif
+/datum/tgui_module/crew_monitor/nif/tgui_state(mob/user)
+ return GLOB.tgui_nif_state
diff --git a/code/modules/tgui/states.dm b/code/modules/tgui/states.dm
index a12864ba95..111ee9e30b 100644
--- a/code/modules/tgui/states.dm
+++ b/code/modules/tgui/states.dm
@@ -1,7 +1,6 @@
/**
- * tgui states
- *
- * Base state and helpers for states. Just does some sanity checks, implement a state for in-depth checks.
+ * Base state and helpers for states. Just does some sanity checks,
+ * implement a proper state for in-depth checks.
*/
/**
@@ -26,9 +25,10 @@
// . = max(., STATUS_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(., STATUS_UPDATE)
+ if(user.client)
+ var/clientviewlist = getviewsize(user.client.view)
+ if(get_dist(src_object, user) < max(clientviewlist[1], clientviewlist[2]))
+ . = max(., STATUS_UPDATE)
// Check if the state allows interaction
var/result = state.can_use_topic(src_object, user)
@@ -46,7 +46,8 @@
* return UI_state The state of the UI.
*/
/datum/tgui_state/proc/can_use_topic(src_object, mob/user)
- return STATUS_CLOSE // Don't allow interaction by default.
+ // Don't allow interaction by default.
+ return STATUS_CLOSE
/**
* public
@@ -56,21 +57,26 @@
* return UI_state The state of the UI.
*/
/mob/proc/shared_tgui_interaction(src_object)
- if(!client) // Close UIs if mindless.
+ // Close UIs if mindless.
+ if(!client)
return STATUS_CLOSE
- else if(stat) // Disable UIs if unconcious.
+ // Disable UIs if unconcious.
+ else if(stat)
return STATUS_DISABLED
- else if(incapacitated()) // Update UIs if incapicitated but concious.
+ // Update UIs if incapicitated but concious.
+ else if(incapacitated())
return STATUS_UPDATE
return STATUS_INTERACTIVE
/mob/living/silicon/ai/shared_tgui_interaction(src_object)
- if(lacks_power()) // Disable UIs if the AI is unpowered.
+ // Disable UIs if the AI is unpowered.
+ if(lacks_power())
return STATUS_DISABLED
return ..()
/mob/living/silicon/robot/shared_tgui_interaction(src_object)
- if(!cell || cell.charge <= 0 || lockcharge) // 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 STATUS_DISABLED
return ..()
@@ -87,7 +93,8 @@
* return UI_state The state of the UI.
*/
/atom/proc/contents_tgui_distance(src_object, mob/living/user)
- return user.shared_living_tgui_distance(src_object) // Just call this mob's check.
+ // Just call this mob's check.
+ return user.shared_living_tgui_distance(src_object)
/**
* public
@@ -99,7 +106,8 @@
* return UI_state The state of the UI.
*/
/mob/living/proc/shared_living_tgui_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 STATUS_CLOSE
var/dist = get_dist(src_object, src)
diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm
index 573f8c118d..ee256287cb 100644
--- a/code/modules/tgui/tgui.dm
+++ b/code/modules/tgui/tgui.dm
@@ -14,34 +14,28 @@
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 = STATUS_INTERACTIVE
/// Topic state used to determine status/interactability.
var/datum/tgui_state/state = null
- /// The parent UI.
- var/datum/tgui/master_ui
- /// Children of this UI.
- var/list/datum/tgui/children = list()
+ /// Asset data to be sent with every update
+ var/list/asset_data
// The map z-level to display.
var/map_z_level = 1
@@ -52,39 +46,24 @@
*
* 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/tgui_state/state = GLOB.tgui_default_state)
+/datum/tgui/New(mob/user, datum/src_object, interface, title, ui_x, ui_y)
src.user = user
src.src_object = src_object
- src.ui_key = ui_key
- 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/tgui_assets = get_asset_datum(/datum/asset/simple/tgui)
- var/datum/asset/fa = get_asset_datum(/datum/asset/simple/fontawesome)
- tgui_assets.send(user)
- fa.send(user)
+ src.title = title
+ src.state = src_object.tgui_state()
+ // Deprecated
+ if(ui_x && ui_y)
+ src.window_size = list(ui_x, ui_y)
/**
* public
@@ -93,84 +72,48 @@
*/
/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 < STATUS_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.nanoui_fancy)
- // window_options += "titlebar=0;can_resize=0;"
- // 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.tgui_base_html(html)
- // Replace template tokens with important UI data
- 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.tgui_data(user)
- if(!initial_static_data)
- initial_static_data = src_object.tgui_static_data(user)
- _initial_update = url_encode(get_json(initial_data, initial_static_data))
-
+ 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()
+ else
+ window.send_message("ping")
+ for(var/datum/asset/asset in src_object.ui_assets(user))
+ 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).
- *
- * optional template string The name of the new interface.
- * optional data list The new initial data.
- */
-/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.tgui_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.tgui_close(user)
+ SStgui.on_close(src)
state = null
- master_ui = null
qdel(src)
/**
@@ -178,50 +121,159 @@
*
* Enable/disable auto-updating of the UI.
*
- * required state bool Enable/disable auto-updating.
+ * required autoupdate 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/tgui_state/state)
+ src.state = state
+
+/**
+ * public
+ *
+ * Makes an asset available to use in tgui.
+ *
+ * required asset datum/asset
+ */
+/datum/tgui/proc/send_asset(var/datum/asset/asset)
+ if(!user.client)
+ return
+ // if(istype(asset, /datum/asset/spritesheet))
+ // var/datum/asset/spritesheet/spritesheet = asset
+ // LAZYINITLIST(asset_data)
+ // LAZYADD(asset_data["styles"], list(spritesheet.css_filename()))
+ asset.send(user)
+
+/**
+ * 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 >= STATUS_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 >= STATUS_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.nanoui_fancy,
- "observer" = isobserver(user),
- "window" = window_id,
"map" = (using_map && using_map.path) ? using_map.path : "Unknown",
"mapZLevel" = map_z_level,
-
- "ref" = "\ref[src]"
+ "window" = list(
+ "key" = window_key,
+ "size" = window_size,
+ "fancy" = FALSE, // user.client.prefs.tgui_fancy,
+ "locked" = FALSE, // 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.tgui_data(user)
+ if(data)
json_data["data"] = data
- if(!isnull(static_data))
+ var/static_data = with_static_data && src_object.tgui_static_data(user)
+ if(static_data)
json_data["static_data"] = static_data
-
- // Send shared states
+ if(asset_data)
+ json_data["assets"] = asset_data
if(src_object.tgui_shared_states)
json_data["shared"] = src_object.tgui_shared_states
+ return json_data
- // Generate the JSON.
- var/json = json_encode(json_data)
- // Strip #255/improper.
- json = replacetext(json, "\proper", "")
- json = replacetext(json, "\improper", "")
- return json
+/**
+ * private
+ *
+ * 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.tgui_host(user)
+ // 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 != STATUS_DISABLED && (autoupdate || force))
+ src_object.tgui_interact(user, src)
+ return
+ // Update status only
+ var/needs_update = process_status()
+ if(status <= STATUS_CLOSE)
+ close()
+ return
+ if(needs_update)
+ window.send_message("update", get_payload())
+
+/**
+ * private
+ *
+ * Updates the status, and returns TRUE if status has changed.
+ */
+/datum/tgui/proc/process_status()
+ var/prev_status = status
+ status = src_object.tgui_status(user, state)
+ return prev_status != status
+
+/datum/tgui/proc/log_message(message)
+ log_tgui("[user] ([user.ckey]) using \"[title]\":\n[message]")
+
+/datum/tgui/proc/set_map_z_level(nz)
+ map_z_level = nz
/**
* private
@@ -230,144 +282,20 @@
* 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 != STATUS_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
+/datum/tgui/proc/on_message(type, list/payload, list/href_list)
+ // Pass act type messages to tgui_act
+ if(type && copytext(type, 1, 5) == "act/")
+ process_status()
+ if(src_object.tgui_act(copytext(type, 5), payload, src, state))
SStgui.update_uis(src_object)
- // if("tgui:setFancy")
- // var/value = text2num(params["value"])
- // user.client.prefs.nanoui_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"])
- if("tgui:setZLevel")
- set_map_z_level(params["mapZLevel"])
- // Update the window state.
- update_status(push = FALSE)
-
- else
- // Update the window state.
- update_status(push = FALSE)
- // Call tgui_act() on the src_object.
- if(src_object.tgui_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.
- */
-/datum/tgui/process(force = FALSE)
- var/datum/host = src_object.tgui_host(user)
- if(!src_object || !host || !user) // If the object or user died (or something else), abort.
- 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.
-
-/**
- * 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.
- */
-/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 <= STATUS_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")
-
-/**
- * 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.
- */
-/datum/tgui/proc/update(force_open = FALSE)
- src_object.tgui_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.tgui_status(user, state)
- if(master_ui)
- status = min(status, master_ui.status)
-
- set_status(status, push)
- if(status == STATUS_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 == STATUS_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 == STATUS_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/set_map_z_level(nz)
- map_z_level = nz
\ No newline at end of file
+ return FALSE
+ switch(type)
+ if("ready", "pingReady")
+ initialized = TRUE
+ if("log")
+ if(href_list["fatal"])
+ close(can_be_suspended = FALSE)
+ if("setSharedState")
+ LAZYINITLIST(src_object.tgui_shared_states)
+ src_object.tgui_shared_states[href_list["key"]] = href_list["value"]
+ SStgui.update_uis(src_object)
\ No newline at end of file
diff --git a/code/modules/tgui/tgui_window.dm b/code/modules/tgui/tgui_window.dm
new file mode 100644
index 0000000000..4d3265ae29
--- /dev/null
+++ b/code/modules/tgui/tgui_window.dm
@@ -0,0 +1,203 @@
+/**
+ * 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
+
+/**
+ * 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.
+ */
+/datum/tgui_window/proc/initialize()
+ 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
+ // TODO: Make this static
+ var/html = SStgui.basehtml
+ html = replacetextEx(html, "\[tgui:windowId]", id)
+ // Send required assets
+ var/datum/asset/asset
+ asset = get_asset_datum(/datum/asset/simple/tgui)
+ asset.send(client)
+ asset = get_asset_datum(/datum/asset/simple/fontawesome)
+ asset.send(client)
+ // 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()
+ 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")
+ locked = FALSE
+ locked_by = null
+ 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_LOADING)
+ if(!message_queue)
+ message_queue = list()
+ message_queue += list(message)
+ return
+ client << output(message, "[id].browser:update")
+
+/**
+ * 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 = 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)
diff --git a/tgui/.eslintrc.yml b/tgui/.eslintrc.yml
index 67e74085c7..9fd4db9fd2 100644
--- a/tgui/.eslintrc.yml
+++ b/tgui/.eslintrc.yml
@@ -8,6 +8,8 @@ env:
es6: true
browser: true
node: true
+globals:
+ Byond: readonly
plugins:
- react
settings:
@@ -388,7 +390,7 @@ rules:
## Enforce a particular style for multiline comments
# multiline-comment-style: error
## Enforce newlines between operands of ternary expressions
- multiline-ternary: [error, always-multiline]
+ # multiline-ternary: [error, always-multiline]
## Require constructor names to begin with a capital letter
# new-cap: error
## Enforce or disallow parentheses when invoking a constructor with no
diff --git a/tgui/.gitattributes b/tgui/.gitattributes
index 0016cc3bf6..9382416e69 100644
--- a/tgui/.gitattributes
+++ b/tgui/.gitattributes
@@ -2,9 +2,18 @@
## Enforce text mode and LF line breaks
*.js text eol=lf
+*.jsx text eol=lf
+*.ts text eol=lf
+*.tsx text eol=lf
*.css text eol=lf
+*.scss text eol=lf
*.html text eol=lf
*.json text eol=lf
+*.yml text eol=lf
+*.md text eol=lf
+*.bat text eol=lf
+yarn.lock text eol=lf
+bin/tgui text eol=lf
## Treat bundles as binary and ignore them during conflicts
*.bundle.* binary merge=tgui-merge-bundle
diff --git a/tgui/README.md b/tgui/README.md
index 5ddeb18fdd..7ae1bddb6a 100644
--- a/tgui/README.md
+++ b/tgui/README.md
@@ -67,8 +67,9 @@ Run one of the following:
game as you code it. Very useful, highly recommended.
- In order to use it, you should start the game server first, connect to it
and wait until the world has been properly loaded and you are no longer
- in the lobby. Start tgui dev server. You'll know that it's hooked correctly
- if data gets dumped to the log when tgui windows are opened.
+ in the lobby. Start tgui dev server, and once it has finished building,
+ press F5 on any tgui window. You'll know that it's hooked correctly if
+ you see a green bug icon in titlebar and data gets dumped to the console.
- `bin/tgui --dev --reload` - reload byond cache once.
- `bin/tgui --dev --debug` - run server with debug logging enabled.
- `bin/tgui --dev --no-hot` - disable hot module replacement (helps when
@@ -134,11 +135,11 @@ logs and time spent on rendering. Use this information to optimize your
code, and try to keep re-renders below 16ms.
**Kitchen Sink.**
-Press `Ctrl+Alt+=` to open the KitchenSink interface. This interface is a
+Press `F12` to open the KitchenSink interface. This interface is a
playground to test various tgui components.
**Layout Debugger.**
-Press `Ctrl+Alt+-` to toggle the *layout debugger*. It will show outlines of
+Press `F11` to toggle the *layout debugger*. It will show outlines of
all tgui elements, which makes it easy to understand how everything comes
together, and can reveal certain layout bugs which are not normally visible.
diff --git a/tgui/bin/tgui b/tgui/bin/tgui
index eb1f200b31..97a86159e6 100644
--- a/tgui/bin/tgui
+++ b/tgui/bin/tgui
@@ -52,6 +52,7 @@ task-dev-server() {
task-eslint() {
cd "${base_dir}"
eslint ./packages "${@}"
+ echo "tgui: eslint check passed"
}
## Mr. Proper
@@ -153,6 +154,13 @@ if [[ ${1} == '--lint-harder' ]]; then
exit 0
fi
+if [[ ${1} == '--fix' ]]; then
+ shift 1
+ task-install
+ task-eslint --fix "${@}"
+ exit 0
+fi
+
## Analyze the bundle
if [[ ${1} == '--analyze' ]]; then
task-install
diff --git a/tgui/docs/component-reference.md b/tgui/docs/component-reference.md
index b853fee4de..6c10124049 100644
--- a/tgui/docs/component-reference.md
+++ b/tgui/docs/component-reference.md
@@ -279,7 +279,7 @@ Example (button):
-+-
+++