/** * tgui * * /tg/station user interface library */ /** * tgui datum (represents a UI). */ /datum/tgui /// The mob who opened/is using the UI. var/mob/user /// The object which owns the UI. var/datum/src_object /// The title of te UI. var/title /// The window_id for browse() and onclose(). 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 /// 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 map z-level to display. var/map_z_level = 1 /// The Parent UI var/datum/tgui/parent_ui /// Children of this UI var/list/children = list() /** * public * * Create a new UI. * * required user mob The mob who opened/is using the UI. * required src_object datum The object or datum which owns the UI. * required interface string The interface used to render the UI. * optional title string The title of the UI. * optional parent_ui datum/tgui The parent of this UI. * 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, interface, title, datum/tgui/parent_ui, ui_x, ui_y) src.user = user src.src_object = src_object src.window_key = "[REF(src_object)]-main" src.interface = interface if(title) src.title = title src.state = src_object.tgui_state() src.parent_ui = parent_ui if(parent_ui) parent_ui.children += src // Deprecated if(ui_x && ui_y) src.window_size = list(ui_x, ui_y) /** * public * * Open this UI (and initialize it with data). */ /datum/tgui/proc/open() if(!user.client) return null if(window) return null process_status() if(status < STATUS_UPDATE) 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.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 * * Close the UI, and all its children. */ /datum/tgui/proc/close(can_be_suspended = TRUE, logout = FALSE) if(closing) return closing = TRUE for(var/datum/tgui/child in children) child.close(can_be_suspended, logout) // 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, logout) src_object.tgui_close(user) SStgui.on_close(src) state = null if(parent_ui) parent_ui.children -= src parent_ui = null qdel(src) /** * public * * Enable/disable auto-updating of the UI. * * required autoupdate bool Enable/disable auto-updating. */ /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(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 >= 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. * * return list */ /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, "map" = (using_map && using_map.path) ? using_map.path : "Unknown", "mapZLevel" = map_z_level, "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), ), ) var/data = custom_data || with_data && src_object.tgui_data(user, src, state) if(data) json_data["data"] = data var/static_data = with_static_data && src_object.tgui_static_data(user) if(static_data) json_data["static_data"] = static_data if(src_object.tgui_shared_states) json_data["shared"] = src_object.tgui_shared_states return json_data /** * 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, parent_ui) 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) if(parent_ui) status = min(status, parent_ui.status) 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 * * 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/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) 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 != STATUS_INTERACTIVE) return LAZYINITLIST(src_object.tgui_shared_states) src_object.tgui_shared_states[href_list["key"]] = href_list["value"] SStgui.update_uis(src_object)