diff --git a/SQL/migrate-2023/V014__ui_scale_option.sql b/SQL/migrate-2023/V014__ui_scale_option.sql new file mode 100644 index 00000000000..95a7aef1e29 --- /dev/null +++ b/SQL/migrate-2023/V014__ui_scale_option.sql @@ -0,0 +1,2 @@ +ALTER TABLE `ss13_player_preferences` ADD COLUMN `tgui_say_light_mode` TINYINT(1) NOT NULL DEFAULT 0 AFTER `tgui_lock`; +ALTER TABLE `ss13_player_preferences` ADD COLUMN `ui_scale` TINYINT(1) NOT NULL DEFAULT 1 AFTER `tgui_say_light_mode`; diff --git a/code/__DEFINES/tgui.dm b/code/__DEFINES/tgui.dm index 02d6a25a660..db68d7089a6 100644 --- a/code/__DEFINES/tgui.dm +++ b/code/__DEFINES/tgui.dm @@ -36,3 +36,9 @@ #define TGUI_CREATE_MESSAGE(type, payload) ( \ "%7b%22type%22%3a%22[type]%22%2c%22payload%22%3a[url_encode(json_encode(payload))]%7d" \ ) + +/// Creates a message packet for sending via output() specifically for opening tgsay using an embedded winget +// This is {"type":"open","payload":{"channel":channel,"mapfocus":[[map.focus]]}}, but pre-encoded. +#define TGUI_CREATE_OPEN_MESSAGE(channel) ( \ + "%7b%22type%22%3a%22open%22%2c%22payload%22%3a%7B%22channel%22%3a%22[channel]%22%2c%22mapfocus%22%3a\[\[map.focus\]\]%7d%7d" \ +) diff --git a/code/datums/browser.dm b/code/datums/browser.dm index 5ca494be4b1..c9d277230eb 100644 --- a/code/datums/browser.dm +++ b/code/datums/browser.dm @@ -1,13 +1,15 @@ /datum/browser var/mob/user - var/title - var/window_id // window_id is used as the window name for browse and onclose + var/title = "" + /// window_id is used as the window name for browse and onclose + var/window_id var/width = 0 var/height = 0 - var/atom/ref = null - var/window_options = "focus=0;can_close=1;can_minimize=1;can_maximize=0;can_resize=1;titlebar=1;" // window option is set using window_id - var/stylesheets[0] - var/scripts[0] + var/datum/weakref/source_ref = null + /// window option is set using window_id + var/window_options = "can_close=1;can_minimize=1;can_maximize=0;can_resize=1;titlebar=1;" + var/stylesheets = list() + var/scripts = list() var/title_image var/head_elements var/body_elements @@ -16,70 +18,96 @@ var/title_buttons = "" -/datum/browser/New(nuser, nwindow_id, ntitle = 0, nwidth = 0, nheight = 0, var/atom/nref = null, var/skip_common_stylesheet = FALSE) +/datum/browser/New(mob/user, window_id, title = "", width = 0, height = 0, atom/source = null, skip_common_stylesheet = FALSE) - user = nuser - window_id = nwindow_id - if (ntitle) - title = format_text(ntitle) - if (nwidth) - width = nwidth - if (nheight) - height = nheight - if (nref) - ref = nref + src.user = user + RegisterSignal(user, COMSIG_QDELETING, PROC_REF(user_deleted)) + src.window_id = window_id + if (title) + src.title = format_text(title) + if (width) + src.width = width + if (height) + src.height = height + if (source) + src.source_ref = WEAKREF(source) if(!skip_common_stylesheet) add_stylesheet("common", 'html/browser/common.css') // this CSS sheet is common to all UIs -/datum/browser/proc/set_user(nuser) - user = nuser +/datum/browser/proc/user_deleted(datum/source) + SIGNAL_HANDLER + user = null -/datum/browser/proc/set_title(ntitle) - title = format_text(ntitle) +/datum/browser/proc/set_user(mob/user) + src.user = user -/datum/browser/proc/add_head_content(nhead_content) - head_content = nhead_content +/datum/browser/proc/set_title(title) + src.title = format_text(title) -/datum/browser/proc/set_title_buttons(ntitle_buttons) - title_buttons = ntitle_buttons +/datum/browser/proc/add_head_content(head_content) + src.head_content += head_content -/datum/browser/proc/set_window_options(nwindow_options) - window_options = nwindow_options +/datum/browser/proc/set_head_content(head_content) + src.head_content = head_content + +/datum/browser/proc/set_title_buttons(title_buttons) + src.title_buttons = title_buttons + +/datum/browser/proc/set_window_options(window_options) + src.window_options = window_options /datum/browser/proc/set_title_image(ntitle_image) //title_image = ntitle_image /datum/browser/proc/add_stylesheet(name, file) - stylesheets["[ckey(name)].css"] = file - SSassets.transport.register_asset("[ckey(name)].css", file) + if (istype(name, /datum/asset/spritesheet)) + var/datum/asset/spritesheet/sheet = name + stylesheets["spritesheet_[sheet.name].css"] = "data/spritesheets/[sheet.name]" + else + var/asset_name = "[name].css" + + stylesheets[asset_name] = file + + if (!SSassets.cache[asset_name]) + SSassets.transport.register_asset(asset_name, file) /datum/browser/proc/add_script(name, file) scripts["[ckey(name)].js"] = file SSassets.transport.register_asset("[ckey(name)].js", file) -/datum/browser/proc/set_content(ncontent) - content = ncontent +/datum/browser/proc/set_content(content) + src.content = content -/datum/browser/proc/add_content(ncontent) - content += ncontent +/datum/browser/proc/add_content(content) + src.content += content /datum/browser/proc/get_header() - var/file - for (file in stylesheets) - head_content += "" + var/list/new_head_content = list() + for (var/file as anything in stylesheets) + new_head_content += "" - for (file in scripts) - head_content += "" + if(user.client?.window_scaling && user.client?.window_scaling != 1 && !user.client?.prefs.ui_scale && width && height) + new_head_content += {" + + "} + + for (var/file as anything in scripts) + new_head_content += "" var/title_attributes = "class='uiTitle'" if (title_image) title_attributes = "class='uiTitle icon' style='background-image: url([title_image]);'" + head_content += new_head_content.Join() return {" - -
+ + [head_content] @@ -97,18 +125,27 @@ /datum/browser/proc/get_content() return {" - [get_header()] - [content] - [get_footer()] + [get_header()] + [content] + [get_footer()] "} /datum/browser/proc/open(var/use_onclose = 1) + if(isnull(window_id)) //null check because this can potentially nuke goonchat + WARNING("Browser [title] tried to open with a null ID") + to_chat(user, SPAN_DANGER("The [title] browser you tried to open failed a sanity check! Please report this on GitHub!")) + return + var/window_size = "" - if (width && height) - window_size = "size=[width]x[height];" - if (stylesheets.len) + if(width && height) + if(user.client?.prefs.ui_scale) + var/scaling = user.client.window_scaling + window_size = "size=[width * scaling]x[height * scaling];" + else + window_size = "size=[width]x[height];" + if (length(stylesheets)) SSassets.transport.send_assets(user.client, stylesheets) - if (scripts.len) + if (length(scripts)) SSassets.transport.send_assets(user.client, scripts) user << browse(get_content(), "window=[window_id];[window_size][window_options]") if (use_onclose) @@ -118,9 +155,14 @@ /datum/browser/proc/setup_onclose() set waitfor = 0 for (var/i in 1 to 10) - if (user && winexists(user, window_id)) - onclose(user, window_id, ref) - break + if (!user?.client || !winexists(user, window_id)) + continue + var/atom/send_ref + if(source_ref) + send_ref = source_ref.resolve() + if(!send_ref) + source_ref = null + onclose(user, window_id, send_ref) /datum/browser/proc/update(var/force_open = 0, var/use_onclose = 1) if(force_open) @@ -130,6 +172,10 @@ /datum/browser/proc/close() user << browse(null, "window=[window_id]") + if(!isnull(window_id))//null check because this can potentially nuke goonchat + user << browse(null, "window=[window_id]") + else + WARNING("Browser [title] tried to close with a null ID") // This will allow you to show an icon in the browse window // This is added to mob so that it can be used without a reference to the browser object @@ -149,47 +195,43 @@ */ -// Registers the on-close verb for a browse window (client/verb/.windowclose) -// this will be called when the close-button of a window is pressed. -// -// This is usually only needed for devices that regularly update the browse window, -// e.g. canisters, timers, etc. -// -// windowid should be the specified window name -// e.g. code is : user << browse(text, "window=fred") -// then use : onclose(user, "fred") -// -// Optionally, specify the "ref" parameter as the controlled atom (usually src) +/// Registers the on-close verb for a browse window (client/verb/windowclose) +/// this will be called when the close-button of a window is pressed. +/// +/// This is usually only needed for devices that regularly update the browse window, +/// e.g. canisters, timers, etc. +/// +/// windowid should be the specified window name +/// e.g. code is : user << browse(text, "window=fred") +/// then use : onclose(user, "fred") +/// +/// Optionally, specify the "source" parameter as the controlled atom (usually src) // to pass a "close=1" parameter to the atom's Topic() proc for special handling. -// Otherwise, the user mob's machine var will be reset directly. -// -/proc/onclose(mob/user, windowid, var/atom/ref=null) +/// Otherwise, the user mob's machine var will be reset directly. +/// +/proc/onclose(mob/user, windowid, atom/source = null) if(!user || !user.client) return var/param = "null" - if(ref) - param = "[REF(ref)]" + if(source) + param = "[REF(source)]" winset(user, windowid, "on-close=\".windowclose [param]\"") -// the on-close client verb -// called when a browser popup window is closed after registering with proc/onclose() -// if a valid atom reference is supplied, call the atom's Topic() with "close=1" -// otherwise, just reset the client mob's machine var. -// -/client/verb/windowclose(var/atomref as text) - set hidden = 1 // hide this verb from the user's panel - set name = ".windowclose" // no autocomplete on cmd line +/// the on-close client verb +/// called when a browser popup window is closed after registering with proc/onclose() +/// if a valid atom reference is supplied, call the atom's Topic() with "close=1" +/// otherwise, just reset the client mob's machine var. +/client/verb/windowclose(atomref as text) + set hidden = TRUE // hide this verb from the user's panel + set name = ".windowclose" // no autocomplete on cmd line - if(atomref!="null") // if passed a real atomref - var/hsrc = locate(atomref) // find the reffed atom - if(hsrc) - usr = src.mob - src.Topic("close=1", list("close"="1"), hsrc) // this will direct to the atom's - return // Topic() proc via client.Topic() - - // no atomref specified (or not found) - // so just reset the user mob's machine var - if(src && src.mob) - src.mob.unset_machine() - return + if(atomref == "null") + return + // if passed a real atomref + var/atom/hsrc = locate(atomref) // find the reffed atom + var/href = "close=1" + if(!hsrc) + return + usr = src.mob + src.Topic(href, params2list(href), hsrc) // this will direct to the atom's Topic() proc via client.Topic() diff --git a/code/modules/admin/admin.dm b/code/modules/admin/admin.dm index edc430bf06d..27994791a39 100644 --- a/code/modules/admin/admin.dm +++ b/code/modules/admin/admin.dm @@ -48,6 +48,8 @@ var/global/enabled_spooking = 0 to_chat(usr, "Error: you are not an admin!") return + var/ui_scale = owner.prefs?.ui_scale + var/body = "