Simple Window Handling

Jor'Mox
Posts: 1146
Joined: Wed Apr 03, 2013 2:19 am

Simple Window Handling

Post by Jor'Mox »

Sometimes, Geyser and Vyzor can be overly complicated for the task at hand, and learning a whole new system just to take advantage of one or two of their features can be a real pain. Also, while they both allow sizing and placement in either pixels or percents of the screen, neither seems to allow using both.

To address these issues, I made a very simple window management script to allow labels, miniConsoles and gauges to be positioned and sized any way you wish. You create the item using the normal commands, then add it to the window manager along with what type of item it is, position and size data, and an origin which specifies a corner where you want your position to be determined from. You can then call on new functions to resize, move, or relocate (change origin corners) for any item you have added. When a window resize event occurs, the items are all resized and repositioned automatically. No new objects are created, and you can still use all the normal functions exactly like you would expect.

Here is the code, some simple documentation is provided at the top:
Code: [show] | [select all] lua
-- Window Manager Script --
--	Zachary Hiland
--	6/16/2013
--
--	Functions Details
--
--	windowManager.add(name, type, x, y, width, height, origin) or windowManager.add(info_table)
--		adds a window to the windowManager
--		info_table must have a valid entry for each argument, by the same name, as in the normal function call
--		name - name of already created window (label, miniConsole, or gauge)
--		type - identify the type of window, must be label, miniConsole, or gauge
--		x - distance horizontally from the origin corner
--		y - distance vertically from the origin corner
--		width - width of window
--		height - height of window
--		origin - corner to act as origin for this window, must be topleft, topright, bottomleft, or bottomright
--		note: all measurements should be numbers, or strings with values measured in percent or pixels (or unlabeled)
--			if a measurement contains both percent and pixel values (or multiple values of the same type), they should be separated
--			by a "+" symbol. Example: "25% + 50px + 16"
--		important: no negative numbers
--
--      windowManager.remove(name)
--		removes a window from the windowManager
--
--      windowManager.move(name, x, y)
--		moves a window with the windowManager
--
--      windowManager.resize(name, width, height)
--		resizes a window with the windowManager
--
--      windowManager.relocate(name, origin)
--		changes the corner acting as the origin for a window in the windowManager
--
--      windowManager.getValue(name, value)
--		returns the calculated size or position values, or the origin, for a window in the windowManager
--
--      windowManager.math(value1, value2, operation)
--		allows addition and subtraction of measurements with each other,
--		and multiplication and division of a measurement by a normal number
--
--      windowManager.simplify(measurement)
--		returns a simplified version of a measurement (all percent and pixel values are combined together)
--
--      windowManager.refresh(name)
--		sets the size and location of a window in the windowManager using previously set values
--		note: generally this function does not need to be called
--
--      windowManager.refreshAll()
--		note: this function is called automatically when a sysWindowResizeEvent event occurs,
--			and generally does not need to be called manually

windowManager = windowManager or {}
windowManager.list = windowManager.list or {}

local function calcScaledNum(scale,num)
	scale_table = string.split(scale," %+ ")
	if #scale_table > 2 then
		scale = windowManager.simplify(scale)
		scale_table = string.split(scale," %+ ")
	end
	scale = 0
	if #scale_table == 2 then
		scale = string.cut(scale_table[1],#scale_table[1] - 1) * num / 100
		scale = scale + string.gsub(scale_table[2],"px","")
	elseif string.find(scale_table[1],"%%") then
		scale = string.cut(scale_table[1],#scale_table[1] - 1) * num / 100
	else
		scale = string.gsub(scale_table[1],"px","")
	end
	scale = math.floor(scale + 0.5)
	return scale
end

function windowManager.simplify(measure)
	measure = string.gsub(measure,"([^%s%+%-]+)%s*-%s*([%d%-]+)","%1 + -%2")
	measure = string.gsub(measure,"%-%-","")
	local measure_table = string.split(measure,"+")
	local percent, pixel = 0,0
	for k,v in ipairs(measure_table) do
		v = string.trim(v)
		if string.find(v,"%%") then
			v = string.gsub(v,"%%","")
			percent = percent + v
		else
			v = string.gsub(v,"px","")
			pixel = pixel + v
		end
	end
	percent = math.floor(1000 * percent + .5) / 1000
	pixel = math.floor(1000 * pixel + .5) / 1000
	if percent == 0 then
		measure = pixel .. "px"
	elseif pixel == 0 then
		measure = percent .. "%"
	else
		measure = percent .. "% + " .. pixel .. "px"
	end
	return measure
end

function windowManager.math(measure,num,op)
	assert(table.contains({"multiply","divide", "add", "subtract"},op),"windowManager.math: bad argument #3 \"operation\", must be multiply or divide")
	if op == "divide" or op == "multiply" then
		if string.find(num,"%%") then
			num = string.gsub(num,"%%","") / 100
		end
		num = assert(tonumber(num),"windowManager.math: bad argument #2 \"num\", must be a number")
		measure = string.gsub(measure,"([^%s%+%-]+)%s*-%s*([%d%-]+)","%1 + -%2")
		local measure_table = string.split(measure,"+")
		if op == "divide" then num = 1 / num end
		for k,v in ipairs(measure_table) do
			v = string.trim(v)
			v = (string.gsub(v,"(%d+).*","%1") * num) .. string.gsub(v,".*%d+(.*)","%1")
			measure_table[k] = v
		end
		measure = table.concat(measure_table," + ")
	else
		if op == "add" then
			measure = measure .. " + " .. num
		elseif op == "subtract" then
			measure = measure .. " + -" .. num
		end
		measure = string.gsub(measure,"([^%s%+%-]+)%s*-%s*([%d%-]+)","%1 + -%2")
	end
	return windowManager.simplify(measure)
end

function windowManager.add(name, window_type, x, y, w, h, origin)
	local tbl = {}
	if type(name) == "table" then
		tbl = table.update(tbl,name)
		name = tbl.name
		x = tbl.x
		y = tbl.y
		w = tbl.width
		h = tbl.height
		origin = tbl.origin
		window_type = tbl.type
		tbl = {}
	end
	assert(name, "windowManager.add: bad argument #1 \"name\".")
	windowManager.list[name] = nil
	assert(type(window_type) == "string", "windowManager.add: bad argument #2 \"type\", must be string.")
	window_type = string.lower(window_type)
	assert(table.contains({"label","miniconsole","gauge"},window_type), "windowManager.add: invalid type")
	assert(x and y and w and h, "windowManager.add: must have x, y, width, and height.")
	origin = origin or "topleft"
	origin = string.lower(origin)
	assert(table.contains({"topleft","topright","bottomleft","bottomright"},origin),"windowManager.add: bad argument #7 \"origin\".")
	x = windowManager.simplify(x)
	y = windowManager.simplify(y)
	w = windowManager.simplify(w)
	h = windowManager.simplify(h)
	tbl = {
		type = window_type,
		x = x, y = y, h = h, w = w,
		origin = origin}
	windowManager.list[name] = tbl
	windowManager.refresh(name)
end

function windowManager.remove(name)
	windowManager.list[name] = nil
end

function windowManager.refresh(name, main_w, main_h)
	local info = assert(windowManager.list[name], "windowManager.refresh: no such window.")
	local x,y,w,h,origin,win_type = info.x, info.y, info.w, info.h, info.origin, info.type
	if not (main_w and main_h) then
		main_w, main_h = getMainWindowSize()
	end
	w = calcScaledNum(w,main_w)
	x = calcScaledNum(x,main_w)
	h = calcScaledNum(h,main_h)
	y = calcScaledNum(y,main_h)
	if string.find(origin,"right") then
		x = main_w - x - w
	end
	if string.find(origin,"bottom") then
		y = main_h - y - h
	end
	if win_type ~= "gauge" then
		moveWindow(name,x,y)
		resizeWindow(name,w,h)
	else
		moveGauge(name,x,y)
		resizeGauge(name,w,h)
	end
end

function windowManager.resize(name, w, h)
	assert(windowManager.list[name], "windowManager.resize: no such window.")
	assert(w and h, "windowManager.resize: must have both width and height.")
	w = windowManager.simplify(w)
	h = windowManager.simplify(h)
	windowManager.list[name].w = w
	windowManager.list[name].h = h
	windowManager.refresh(name)
end

function windowManager.move(name, x, y)
	assert(windowManager.list[name], "windowManager.move: no such window.")
	assert(x and y, "windowManager.move: must have both x and y.")
	x = windowManager.simplify(x)
	y = windowManager.simplify(y)
	windowManager.list[name].x = x
	windowManager.list[name].y = y
	windowManager.refresh(name)
end

function windowManager.relocate(name, origin)
	assert(windowManager.list[name], "windowManager.relocate: no such window.")
	origin = origin or "topleft"
	origin = string.lower(origin)
	assert(table.contains({"topleft","topright","bottomleft","bottomright"},origin),"windowManager.relocate: bad argument #2 \"origin\".")
	windowManager.list[name].origin = origin
	windowManager.refresh(name)
end

function windowManager.getValue(name, value)
	assert(windowManager.list[name], "windowManager.getValue: no such window.")
	assert(table.contains({"x","y","width","height","w","h","origin"},value),"windowManager.getValue: no such value.")
	local sys_w, sys_h = getMainWindowSize()
	if value == "width" then value = "w" end
	if value == "height" then value = "h" end
	local tmp = windowManager.list[name][value]
	if value == "w" or value == "x" then
		tmp = calcScaledNum(tmp,sys_w)
	elseif value == "h" or value == "y" then
		tmp = calcScaledNum(tmp,sys_h)
	end
	return tmp
end

function windowManager.refreshAll()
	local main_w, main_h = getMainWindowSize()
	for k,v in pairs(windowManager.list) do
		windowManager.refresh(k, main_w, main_h)
	end
end

registerAnonymousEventHandler("sysWindowResizeEvent", "windowManager.refreshAll")
Last edited by Jor'Mox on Sun Jun 16, 2013 6:18 pm, edited 3 times in total.

User avatar
Akaya
Posts: 414
Joined: Thu Apr 19, 2012 1:36 am

Re: Simple Window Handling

Post by Akaya »

very neat!

Jor'Mox
Posts: 1146
Joined: Wed Apr 03, 2013 2:19 am

Re: Simple Window Handling

Post by Jor'Mox »

Updated the code above so any currently managed window by the same name is overwritten when you add one in (before, it threw an error). Also, you can now specify measurements using subtraction as well as addition. (So, you can use something like this: "12% + 50 - 5%") That way you can easily combine values to specify exactly where you want something or how big you want it to be.

I also added in a function to allow you to get the calculated measurements used to position and size a window, just in case you need them for something else.

Jor'Mox
Posts: 1146
Joined: Wed Apr 03, 2013 2:19 am

Re: Simple Window Handling

Post by Jor'Mox »

So, I'm not completely done with this, still have a few bits of optimization I want to throw in to speed things up, as well as a math function for it so you can do basic math on the strings it uses for measurements. But, using this in place of Geyser to manage my GUI has produced dramatic speed improvements both in initialization times and when the window resizes. Granted, I have 4 gauges, 8 MiniConsoles, and over 100 labels that were all heavily nested in Geyser (I needed them to be so that I could manage the degree of customizability I wanted in terms of object layouts), and so I was seeing init times of several seconds, and resize times slow enough that it interrupted typing (when the command line resized) and drew several complaints. Now I'm seeing init times of around 1/10 of a second, and resizing is completely smooth.

User avatar
Vadi
Posts: 5041
Joined: Sat Mar 14, 2009 3:13 pm

Re: Simple Window Handling

Post by Vadi »

I haven't tried this yet, but the idea of being able to specify a % +/- pixels is one I can certainly agree with! I've ran into a need to use that when creating UIs, and saw Mozilla add this to their CSS positioning a while back - it's definitely useful.

There definitely is a noticeable delay when Geyser has to resize things, esp. on the input line changing the window - this seems like a good reason to investigate the cause behind that, then.

User avatar
demonnic
Posts: 886
Joined: Sat Dec 05, 2009 3:19 pm

Re: Simple Window Handling

Post by demonnic »

I also do think that the ability to use % + pixels should be integrated into Geyser... just as soon as I summon up some free time. I haven't found the magic incantation needed for that in awhile though.

Jor'Mox
Posts: 1146
Joined: Wed Apr 03, 2013 2:19 am

Re: Simple Window Handling

Post by Jor'Mox »

Having that functionality would dramatically improve Geyser, and allow for much more complicated layouts without the excessive use of container type objects. Though, I still have no idea why it is so slow when it comes to resizing. Yes, you have to jump through some hoops to calculate measurements based off of each container, but if you start at the highest level and work down into everything nested inside it, it doesn't seem like it would add too much in terms of extra cycles.

User avatar
demonnic
Posts: 886
Joined: Sat Dec 05, 2009 3:19 pm

Re: Simple Window Handling

Post by demonnic »

Agreed, the overhead on resizing is kind of nuts and it's high time we tracked it down and eliminated it with extreme prejudice.

Jor'Mox
Posts: 1146
Joined: Wed Apr 03, 2013 2:19 am

Re: Simple Window Handling

Post by Jor'Mox »

Here is the "optimized" version of my script. I'm sure someone could probably find a better way to parse the text than I have, but I'm not really all that skilled with it. Regardless, it has two added functions, a simplify function that combines all the percent and pixel values together to make a simplified measurement string, and a math function that allows you to add, subtract, multiply and divide (for multiplication and division, the second number has to be a real number, no percents or pixels).

Still running very fast, and the few bugs I had before have been worked out.
Code: [show] | [select all] lua
-- Window Manager Script --
--	Zachary Hiland
--	6/15/2013
--
--	Functions Details
--
--	windowManager.add(name, type, x, y, width, height, origin) or windowManager.add(info_table)
--		adds a window to the windowManager
--		info_table must have a valid entry for each argument, by the same name, as in the normal function call
--		name - name of already created window (label, miniConsole, or gauge)
--		type - identify the type of window, must be label, miniConsole, or gauge
--		x - distance horizontally from the origin corner
--		y - distance vertically from the origin corner
--		width - width of window
--		height - height of window
--		origin - corner to act as origin for this window, must be topleft, topright, bottomleft, or bottomright
--		note: all measurements should be numbers, or strings with values measured in percent or pixels (or unlabeled)
--			if a measurement contains both percent and pixel values (or multiple values of the same type), they should be separated
--			by a "+" symbol. Example: "25% + 50px + 16"
--		important: no negative numbers
--
--	windowManager.remove(name)
--		removes a window from the windowManager
--
--	windowManager.move(name, x, y)
--		moves a window with the windowManager
--
--	windowManager.resize(name, width, height)
--		resizes a window with the windowManager
--
--	windowManager.relocate(name, origin)
--		changes the corner acting as the origin for a window in the windowManager
--
--	windowManager.getValue(name, value)
--		returns the calculated size or position values, or the origin, for a window in the windowManager
--
--	windowManager.math(value1, value2, operation)
--		allows addition and subtraction of measurements with each other,
--		and multiplication and division of a measurement by a normal number
--
--	windowManager.simplify(measurement)
--		returns a simplified version of a measurement (all percent and pixel values are combined together)
--
--	windowManager.refresh(name)
--		sets the size and location of a window in the windowManager using previously set values
--		note: generally this function does not need to be called
--
--	windowManager.refreshAll()
--		note: this function is called automatically when a sysWindowResizeEvent event occurs,
--			and generally does not need to be called manually

windowManager = windowManager or {}
windowManager.list = windowManager.list or {}

local function calcScaledNum(scale,num)
	scale = string.gsub(scale,"([^%s%+%-]+)%s*-%s*([%d%-]+)","%1 + -%2")
	scale_table = string.split(scale,"+")
	scale = 0
	for k,v in ipairs(scale_table) do
		v=string.trim(v)
		if string.find(v,"%%") then
			v=string.gsub(v,"%%","")
			scale = scale + v * num / 100
		else
			v=string.gsub(v,"px","")
			scale = scale + v
		end
	end
	scale = math.floor(tonumber(scale) + 0.5)
	return scale
end

function windowManager.simplify(measure)
	measure = string.gsub(measure,"([^%s%+%-]+)%s*-%s*([%d%-]+)","%1 + -%2")
	measure = string.gsub(measure,"%-%-","")
	local measure_table = string.split(measure,"+")
	local percent, pixel = 0,0
	for k,v in ipairs(measure_table) do
		v = string.trim(v)
		if string.find(v,"%%") then
			v = string.gsub(v,"%%","")
			percent = percent + v
		else
			v = string.gsub(v,"px","")
			pixel = pixel + v
		end
	end
	percent = math.floor(1000 * percent + .5) / 1000
	pixel = math.floor(1000 * pixel + .5) / 1000
	if percent == 0 then
		measure = pixel .. "px"
	elseif pixel == 0 then
		measure = percent .. "%"
	else
		measure = percent .. "% + " .. pixel .. "px"
	end
	return measure
end

function windowManager.math(measure,num,op)
	assert(table.contains({"multiply","divide", "add", "subtract"},op),"windowManager.math: bad argument #3 \"operation\", must be multiply or divide")
	if op == "divide" or op == "multiply" then
		num = assert(tonumber(num),"windowManager.math: bad argument #2 \"num\", must be a number")
		measure = string.gsub(measure,"([^%s%+%-]+)%s*-%s*([%d%-]+)","%1 + -%2")
		local measure_table = string.split(measure,"+")
		if op == "divide" then num = 1 / num end
		for k,v in ipairs(measure_table) do
			v = string.trim(v)
			v = (string.gsub(v,"(%d+).*","%1") * num) .. string.gsub(v,".*%d+(.*)","%1")
			measure_table[k] = v
		end
		measure = table.concat(measure_table," + ")
	else
		if op == "add" then
			measure = measure .. " + " .. num
		elseif op == "subtract" then
			measure = measure .. " + -" .. num
		end
		measure = string.gsub(measure,"([^%s%+%-]+)%s*-%s*([%d%-]+)","%1 + -%2")
	end
	return windowManager.simplify(measure)
end

function windowManager.add(name, window_type, x, y, w, h, origin)
	local tbl = {}
	if type(name) == "table" then
		tbl = table.update(tbl,name)
		name = tbl.name
		x = tbl.x
		y = tbl.y
		w = tbl.width
		h = tbl.height
		origin = tbl.origin
		window_type = tbl.type
		tbl = {}
	end
	assert(name, "windowManager.add: bad argument #1 \"name\".")
	windowManager.list[name] = nil
	assert(type(window_type) == "string", "windowManager.add: bad argument #2 \"type\", must be string.")
	window_type = string.lower(window_type)
	assert(table.contains({"label","miniconsole","gauge"},window_type), "windowManager.add: invalid type")
	assert(x and y and w and h, "windowManager.add: must have x, y, width, and height.")
	origin = origin or "topleft"
	origin = string.lower(origin)
	assert(table.contains({"topleft","topright","bottomleft","bottomright"},origin),"windowManager.add: bad argument #7 \"origin\".")
	x = windowManager.simplify(x)
	y = windowManager.simplify(y)
	w = windowManager.simplify(w)
	h = windowManager.simplify(h)
	tbl = {
		type = window_type,
		x = x, y = y, h = h, w = w,
		origin = origin}
	windowManager.list[name] = tbl
	windowManager.refresh(name)
end

function windowManager.remove(name)
	windowManager.list[name] = nil
end

function windowManager.refresh(name, main_w, main_h)
	local info = assert(windowManager.list[name], "windowManager.refresh: no such window.")
	local x,y,w,h,origin,win_type = info.x, info.y, info.w, info.h, info.origin, info.type
	if not (main_w and main_h) then
		main_w, main_h = getMainWindowSize()
	end
	w = calcScaledNum(w,main_w)
	x = calcScaledNum(x,main_w)
	h = calcScaledNum(h,main_h)
	y = calcScaledNum(y,main_h)
	if string.find(origin,"right") then
		x = main_w - x - w
	end
	if string.find(origin,"bottom") then
		y = main_h - y - h
	end
	if win_type ~= "gauge" then
		moveWindow(name,x,y)
		resizeWindow(name,w,h)
	else
		moveGauge(name,x,y)
		resizeGauge(name,w,h)
	end
end

function windowManager.resize(name, w, h)
	assert(windowManager.list[name], "windowManager.resize: no such window.")
	assert(w and h, "windowManager.resize: must have both width and height.")
	w = windowManager.simplify(w)
	h = windowManager.simplify(h)
	windowManager.list[name].w = w
	windowManager.list[name].h = h
	windowManager.refresh(name)
end

function windowManager.move(name, x, y)
	assert(windowManager.list[name], "windowManager.move: no such window.")
	assert(x and y, "windowManager.move: must have both x and y.")
	x = windowManager.simplify(x)
	y = windowManager.simplify(y)
	windowManager.list[name].x = x
	windowManager.list[name].y = y
	windowManager.refresh(name)
end

function windowManager.relocate(name, origin)
	assert(windowManager.list[name], "windowManager.relocate: no such window.")
	origin = origin or "topleft"
	origin = string.lower(origin)
	assert(table.contains({"topleft","topright","bottomleft","bottomright"},origin),"windowManager.relocate: bad argument #2 \"origin\".")
	windowManager.list[name].origin = origin
	windowManager.refresh(name)
end

function windowManager.getValue(name, value)
	assert(windowManager.list[name], "windowManager.getValue: no such window.")
	assert(table.contains({"x","y","width","height","w","h","origin"},value),"windowManager.getValue: no such value.")
	local sys_w, sys_h = getMainWindowSize()
	if value == "width" then value = "w" end
	if value == "height" then value = "h" end
	local tmp = windowManager.list[name][value]
	if value == "w" or value == "x" then
		tmp = calcScaledNum(tmp,sys_w)
	elseif value == "h" or value == "y" then
		tmp = calcScaledNum(tmp,sys_h)
	end
	return tmp
end

function windowManager.refreshAll()
	local main_w, main_h = getMainWindowSize()
	for k,v in pairs(windowManager.list) do
		windowManager.refresh(k, main_w, main_h)
	end
end

registerAnonymousEventHandler("sysWindowResizeEvent", "windowManager.refreshAll")

User avatar
Vadi
Posts: 5041
Joined: Sat Mar 14, 2009 3:13 pm

Re: Simple Window Handling

Post by Vadi »

One Lua optimization you could do is localize the function calls that you use 2+ times (http://www.lua.org/pil/4.2.html, http://springrts.com/wiki/Lua_Performan ... :_Localize)

Here is an example (also using a substring version of string.find, and a for #, which is quicker than ipairs):
Code: [show] | [select all] lua
local function calcScaledNum(scale,num)
        local gsub, trim, find = string.gsub, string.trim, string.find

        scale = gsub(scale,"([^%s%+%-]+)%s*-%s*([%d%-]+)","%1 + -%2")
        scale_table = string.split(scale,"+")
        scale = 0
        for i = 1, #scale_table do
                local v=trim(scale_table[i])
                if find(v,"%", 1, true) then
                        v=gsub(v,"%%","")
                        scale = scale + v * num / 100
                else
                        v=gsub(v,"px","")
                        scale = scale + v
                end
        end
        scale = math.floor(tonumber(scale) + 0.5)
        return scale
end

Post Reply