Simple Window Manager v3.0

Share your scripts and packages with other Mudlet users.
Post Reply
Jor'Mox
Posts: 1142
Joined: Wed Apr 03, 2013 2:19 am

Simple Window Manager v3.0

Post by Jor'Mox »

Vyzor and Geyser are both fairly powerful window management systems, but they can be overkill for simple GUI layouts, and require learning a large amount of new syntax to interact with them. Also, my experience with Geyser left me frustrated with how complicated it was to position objects using a mix of percent and pixel values. So I created this script to address those issues. It handles window positioning and sizing, and adjusts position and size as the main window is resized, but stays out of the way the rest of the time. You create the GUI objects you want (labels, miniconsoles, gauges, mapper, and autowrap [a miniconsole window for which SWM automatically adjusts wordwrap as the window size changes]), using the windowManager.create function, or create the GUI objects yourself and pass them to SWM manually using the windowManager.add function. You can set size and position using any mix of pixel and percent values, and you can designate a corner as the origin corner for that object. There is no need to remember to add your object's height or width to its x or y values so that it isn't displayed off screen. Also, so long as you are managing a relatively small number of GUI objects, this runs dramatically faster than Geyser when the window is resized (hopefully also when using a large number of objects, but I haven't tested a complex setup in Geyser recently).

I tried to include sufficient documentation as comments at the top. If there are any comments or questions, please feel free to let me know.
Code: [show] | [select all] lua
-- Window Manager Script --
--  Zachary Hiland
--  3/26/2017
--  v3.00a
--
--  Functions Details
--
--  Generic Functions
--      
--      windowManager.create(name, type, x, y, width, height, origin, [font_size]) or windowManager.add(info_table)
--      creates an object and adds it 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 for the object to be created (label, miniConsole, gauge, mapper, menu, or autowrap)
--          Note: for a mapper window, use any name you like, so long as it isn't the name of some other object
--      type - identify the type of window, must be label, miniConsole, gauge, mapper, autowrap, or menu
--      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
--      font_size - only used for miniconsoles and autowrap windows, will be automatically set as the font size for the created window
--
--      note: all measurements should be numbers, or strings with values measured in percent or pixels
--          if a measurement contains both percent and pixel values (or multiple values of the same type),
--          they should be separated by a "+" or "-" symbol. Example: "25% + 50"
--
--      windowManager.add(name, type, x, y, width, height, origin, font_size) or windowManager.create(info_table)
--      adds an already created window to the windowManager, uses the exact same arguments as those in the "create" function
--
--      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
--      
--      note: to get size and position information on windows as they are stored (rather than the calculated
--      actual number of pixels), use windowManager.list[name]
--      x, y, h, and w values will always be shown as a string in the following format "## +/- ##%"
--      
--  Functions Used for Compound Number Type (With Raw Number and Percentage Value)
--      
--      windowManager.newCompoundNumber(number, percent, optional reference)
--      makes an object of the compound number type, taking up to three arguments of the number type
--      the reference number is used when evaluating the percentage value, resulting in that percent
--      of the reference number added to the base number.
--      
--      windowManager.makeCompound(number, optional reference)
--      makes an object of the compound number type, taking a number or string for the first argument
--      and a number for the second argument, if a string is given, it must contain a simple expression
--      of the desired raw number and percentages to be used, separated by a space, with or without a +/- sign
--      Example: "25% - 50", "50 25%", or even "50 + -25%"
--      no more than one number and one percent value can be used
--      
--      note: compound number objects created by either of these two functions can be used for any measurement
--      argument in the above functions, and can be manipulated with regular math operators (+, -, *, /)
--      two such numbers can be added or subtracted, one can be multiplied or divided by a regular number
--      or one can have a regular number, a percentage (as a string), or a string containing both a number
--      and a percentage added or subtracted to it, without the use of special functions
--      
--  Functions Important for Mapper Window
--
--      windowManager.show(name)
--      shows a window that is managed by windowManager (you can also use showWindow or showGauge functions)
--
--      windowManager.hide(name)
--      hides a window that is managed by windowManager (you can also use hideWindow or hideGauge functions)
--      
--  Functions Important for Autowrap Windows
--
--      windowManager.createBuffer(name)
--      creates a buffer to be used with an autowrap window, only needed if miniconsole added manually to windowManager
--
--      windowManager.append(name)
--      appends text to a miniconsole or autowrap window, works just like appendBuffer()
--
--      windowManager.echo(name, text)
--      echoes text to a miniconsole or autowrap window. cecho, decho, and hecho are also available.
--
--      windowManager.clear(name)
--      clears a miniconsole or autowrap window.
--
--      windowManager.setFontSize(name, font_size)
--      sets the font size for a miniconsole or autowrap window.
--
--  Functions Not Called by Users
--      
--      windowManager.refresh(name)
--      redraws 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 {}

-- localizing global variables
local getMainWindowSize, calcFontSize, setWindowWrap, clearWindow = getMainWindowSize, calcFontSize, setWindowWrap, clearWindow
local setMiniConsoleFontSize, copy, appendBuffer, createBuffer = setMiniConsoleFontSize, copy, appendBuffer, createBuffer
local selectCurrentLine, moveCursor, getMainWindowSize = selectCurrentLine, moveCursor, getMainWindowSize
local find, gmatch, gsub, sub, lower = string.find, string.gmatch, string.gsub, string.sub, string.lower
local floor, contains, update = math.floor, table.contains, table.update
local tonumber, error, type, pcall, getmetatable, setmetatable, pairs = tonumber, error, type, pcall, getmetatable, setmetatable, pairs

local make_compound, list, refresh, getValue
local signs = {["-"] = -1, ["+"] = 1}
local window_origins = {"topleft","topright","bottomleft","bottomright"}
local window_values = {"x","y","width","height","w","h","origin"}

local function round(num, idp)
    if idp then
        assert(idp > 0 and idp == floor(idp), "Invalid decimal place!")
        local mult = 10^idp
        return floor(num * mult + 0.5) / mult
    else
        return floor(num + 0.5)
    end
end

local function is_compound(test) return type(test) == "table" and test.type == "compound number" end

local function pick_compound(a,b)
    if is_compound(a) then
        return a, b
    else
        return b, a
    end
end

local compound_number_class
compound_number_class = {
    __add = function(a, b)
        local tmp,self,value,status
        self,value = pick_compound(a,b)
        status, value = pcall(make_compound,value)
        if value then
            tmp = compound_number_class:new(self.number + value.number, self.percent + value.percent, self.reference)
        else
            error("Compound Number Class: Add: value must be a number or a percentage",1)
        end
        return tmp
    end,
    
    __sub = function(a, b)
        local tmp,self,value,status
        self,value = pick_compound(a,b)
        status, value = pcall(make_compound,value)
        if value then
            tmp = compound_number_class:new(self.number - value.number, self.percent - value.percent, self.reference)
        else
            error("Compound Number Class: Subtract: value must be a number or a percentage",1)
        end
        return tmp
    end,
    
    __mul = function(a, b)
        local tmp
        a,b = pick_compound(a,b)
        if tonumber(b) then
            tmp = compound_number_class:new(a.number * b, a.percent * b, a.reference)
        else
            error("Compound Number Class: Multiply: value must be a number or a percentage",1)
        end
        return tmp
    end,
    
    __div = function(a, b)
        local tmp
        if tonumber(b) then
            tmp = compound_number_class:new(a.number / b, a.percent / b, a.reference)
        else
            error("Compound Number Class: Divide: value must be a number or a percentage",1)
        end
        return tmp
    end,
    
    __unm = function(self)
        local tmp = compound_number_class:new(0 - self.number, 0 - self.percent, self.reference)
        return tmp
    end,
    
    __tostring = function(self)
        local str = self.number
        if self.percent >= 0 then
            str = str .. " + "
        else
            str = str .. " - "
        end
        str = str .. self.percent .. "%"
        return str
    end,
    
    __index = function(self, key)
        if key == "value" then
            return self:getValue()
        else
            return getmetatable(self)[key] or self._values[key]
        end
    end,
    
    __newindex = function(self,key,value)
        if key == "number" or key == "percent" or key == "reference" then
            if tonumber(value) then
                self._values[key] = tonumber(value)
            else
                error("Compound Number Class: property values must be numbers",1)
            end
        else
            error("Compound Number Class: cannot add index/key to class object",1)
        end
    end,
    
    getValue = function(self,rnd)
        local val = self.number + self.reference * (self.percent / 100)
        if rnd then val = round(val, (rnd ~= 0 and rnd)) end
        return val
    end,
    
    new = function(self,number,percent,reference)
        local o = {_values = {number = number or 0, percent = percent or 0, reference = reference or 1, type = "compound number"}}
        setmetatable(o,self)
        return o
    end,
}

local function rewrap_window(name)
    local info = list[name]
    local buffer = name .. "_windowManager_buffer"
    local wrap = floor(getValue(name,"width") / calcFontSize(info.font))
    local line, moved
    setWindowWrap(name,wrap)
    clearWindow(name)
    line = 0
    moved = moveCursor(buffer,1,line)
    while moved do
        selectCurrentLine(buffer)
        copy(buffer)
        line = line + 1
        moved = moveCursor(buffer,1,line)
        appendBuffer(name)
    end
end

local window_creation_functions = {
    label = createLabel,
    miniconsole = createMiniConsole,
    gauge = createGauge,
    menu = createMenu,
    autowrap = function(name)
        createMiniConsole(name,0,0,0,0)
        windowManager.makeBuffer(name)
    end}
local window_refresh_functions = {
    label = function(name,x,y,w,h) moveWindow(name,x,y) resizeWindow(name,w,h) end,
    miniconsole = function(name,x,y,w,h) moveWindow(name,x,y) resizeWindow(name,w,h) end,
    gauge = function(name,x,y,w,h) moveGauge(name,x,y) resizeGauge(name,w,h) end,
    menu = function(name,x,y,w,h) moveMenu(name,x,y) resizeMenu(name,w,h) end,
    autowrap = function(name,x,y,w,h) moveWindow(name,x,y) resizeWindow(name,w,h) rewrap_window(name) end,
    mapper = function(name,x,y,w,h,hide) if not hide then createMapper(x,y,w,h) end end,
    }
local window_hide_functions = {
    label = hideWindow,
    miniconsole = hideWindow,
    autowrap = hideWindow,
    gauge = hideGauge,
    menu = hideMenu,
    mapper = function(name) list[name].hide = true createMapper(0,0,0,0) end,
}
local window_show_functions = {
    label = showWindow,
    miniconsole = showWindow,
    autowrap = showWindow,
    gauge = showGauge,
    menu = showMenu,
    mapper = function(name) list[name].hide = false refresh(name) end
}

local function run_console(func,name,text)
    local info = list[name]
    if not info then error("windowManager." .. func .. ": no such window.",2) end
    if info.type == "miniconsole" or info.type == "autowrap" then
        func(name, text)
        func(name .. "_windowManager_buffer", text)
    end
end

function windowManager.newCompoundNumber(number,percent,reference) return compound_number_class:new(number,percent,reference) end

function windowManager.makeCompound(value,reference)
    local sign, number, percent
    if type(value) == "number" or tonumber(value) then
        number = tonumber(value)
    elseif type(value) == "string" then
        for w in gmatch(value,"[^%s]+") do
            if find(w,"%%") then
                percent = gsub(w,"%%","")
                percent = tonumber(percent) * (sign or 1)
            elseif signs[w] then
                sign = signs[w]
            else
                number = tonumber(w) * (sign or 1)
            end
        end
    elseif is_compound(value) then
        return value
    else
        error("makeCompound: value must be a number or a string")
    end
    return compound_number_class:new(number or 0, percent or 0, reference or 1)
end

function windowManager.create(name, window_type, ...)
    local tbl = {}
    local is_table
    if type(name) == "table" then
        update(tbl,name)
        name = tbl.name
        window_type = tbl.type
        is_table = true
    end
    if type(window_type) ~= "string" then
        error("windowManager.create: bad argument #2 \"type\", must be string.",2)
    end
    window_type = lower(window_type)
    if window_type ~= "mapper" and not window_creation_functions[window_type] then
        error("windowManager.create: invalid type",2)
    end
    if window_creation_functions[window_type] then window_creation_functions[window_type](name,0,0,0,0,1) end
    if is_table then
        return windowManager.add(tbl)
    else
        return windowManager.add(name, window_type, ...)
    end
end

function windowManager.makeBuffer(name)
    createBuffer(name .. "_windowManager_buffer")
    setWindowWrap(name .. "_windowManager_buffer",1000)
end

function windowManager.add(name, window_type, x, y, w, h, origin, font)
    local tbl = {}
    if type(name) == "table" then
        tbl = 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
        font = tbl.font_size
        tbl = {}
    end
    font = tonumber(font) or 10
    if not name then
        error("windowManager.add: bad argument #1 \"name\".",2)
    end
    list[name] = nil
    if type(window_type) ~= "string" then
        error("windowManager.add: bad argument #2 \"type\", must be string.",2)
    end
    window_type = lower(window_type)
    if window_type ~= "mapper" and not window_creation_functions[window_type] then
        error("windowManager.add: invalid type",2)
    end
    if not (x and y and w and h) then
        error("windowManager.add: must have x, y, width, and height.",2)
    end
    origin = lower(origin or "topleft")
    if not contains(window_origins,origin) then
        error("windowManager.add: bad argument #7 \"origin\".",2)
    end
    tbl = {
        type = window_type,
        x = tostring(make_compound(x)), y = tostring(make_compound(y)),
        h = tostring(make_compound(h)), w = tostring(make_compound(w)),
        origin = origin}
    if window_type == "autowrap" or window_type == "miniconsole" then
        tbl.font = font
        setMiniConsoleFontSize(name, font)
    end
    list[name] = tbl
    refresh(name)
end

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

function windowManager.resize(name, w, h)
    local info = list[name]
    if not info then error("windowManager.resize: no such window.",2) end
    if not (w and h) then error("windowManager.resize: must have both width and height.",2) end
    list[name].w = tostring(make_compound(w))
    list[name].h = tostring(make_compound(h))
    refresh(name)
end

function windowManager.move(name, x, y)
    local info = list[name]
    if not info then error("windowManager.move: no such window.",2) end
    if not (x and y) then error("windowManager.move: must have both x and y.",2) end
    list[name].x = tostring(make_compound(x))
    list[name].y = tostring(make_compound(y))
    refresh(name)
end

function windowManager.relocate(name, origin)
    local info = list[name]
    if not info then error("windowManager.relocate: no such window.",2) end
    origin = lower(origin)
    if not contains(window_origins,origin) then
        error("windowManager.relocate: bad argument #2 \"origin\".",2)
    end
    list[name].origin = origin
    refresh(name)
end

function windowManager.hide(name)
    local info = list[name]
    if not info then error("windowManager.hide: no such window.",2) end
    window_hide_functions[info.type](name)
end

function windowManager.show(name)
    local info = list[name]
    if not info then error("windowManager.show: no such window.",2) end
    window_show_functions[info.type](name)
end

function windowManager.setFontSize(name, font_size)
    local info = list[name]
    if not info then error("windowManager.setFontSize: no such window.",2) end
    if info.type == "miniconsole" or info.type == "autowrap" then
        list[name].font = font_size
        setMiniConsoleFontSize(name, font_size)
        refresh(name)
    end
end

function windowManager.clear(name) run_console(clearWindow,name) end
function windowManager.append(name) run_console(appendBuffer,name) end
function windowManager.echo(name, text) run_console(echo,name,text) end
function windowManager.cecho(name, text) run_console(cecho,name,text) end
function windowManager.hecho(name, text) run_console(hecho,name,text) end
function windowManager.decho(name, text) run_console(decho,name,text) end

function windowManager.getValue(name, value)
    local info = list[name]
    if not info then error("windowManager.getValue: no such window.",2) end
    if not contains(window_values,value) then
        error("windowManager.getValue: no such value.",2)
    end
    local sys_w, sys_h = getMainWindowSize()
    if value == "width" or value == "height" then value = sub(value,1,1) end
    local tmp = list[name][value]
    if value == "w" or value == "x" then
        tmp = make_compound(tmp,sys_w):getValue(0)
    elseif value == "h" or value == "y" then
        tmp = make_compound(tmp,sys_h):getValue(0)
    end
    return tmp
end

function windowManager.refresh(name, main_w, main_h)
    local info = list[name]
    if not info then error("windowManager.refresh: no such window.",2) end
    local x,y,w,h
    local origin,win_type = info.origin, info.type
    if not (main_w and main_h) then
        main_w, main_h = getMainWindowSize()
    end
    w = make_compound(info.w,main_w):getValue(0)
    x = make_compound(info.x,main_w):getValue(0)
    h = make_compound(info.h,main_h):getValue(0)
    y = make_compound(info.y,main_h):getValue(0)
    if find(origin,"right") then
        x = main_w - x - w
    end
    if find(origin,"bottom") then
        y = main_h - y - h
    end
    window_refresh_functions[win_type](name,x,y,w,h,info.hide)
end

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

make_compound, list, refresh, getValue = windowManager.makeCompound, windowManager.list, windowManager.refresh, windowManager.getValue

registerAnonymousEventHandler("sysWindowResizeEvent", "windowManager.refreshAll")
Like the previous version of this script, you can use strings storing the number of pixels, as well as the percentage of screen size to specify position and size of any object you use with it. For example: "50 + 25%"
In addition, a new type has been created, which I'm calling a "compound number", which handles these two values for you, and allows you to use simple math operators to interact with them. Two different functions can be used to create such an object. The first is windowManager.newCompoundNumber, which takes three arguments, the first is the raw number portion, the second is the percentage portion (as a number), and the optional third argument is a reference value that the percent is based off of when calculating an absolute value (note that the reference value of any argument you use with this script is not used, as it always references the current screen size). The second is windowManager.makeCompound , which takes two arguments, the first being either a raw number or a string with a percentage or a combination of a number and a percentage (exactly the same thing you could use on its own as a measurement argument to any of the functions in this script), and the second being an optional reference number.

Once you have created a compound number object, you can use regular math operators to manipulate it, including adding or subtracting any of a number, another compound number, or a string (the same as specified above). You can also multiply or divide by a number. You can also get the calculated value of the number by referencing its "value" property (i.e. var.value), and you can generate a string representation of it by using the tostring function (i.e. tostring(var) ), which will produce a string like the ones above, though it will always have both a raw number and a percentage value, even if one of those values is zero.

This script also works with the generalized menu GUI objects that I created and posted here.

Dakhor
Posts: 7
Joined: Mon Apr 03, 2017 8:18 am

Re: Simple Window Manager v3.0

Post by Dakhor »

Getting this to work was fairly easy even for a beginner to Mudlet. (Thanx JorMox)

More info and examples on how to get it to work can be found on the thread to the the previous version of the Simple Window Manager - do note that some things have changed with the new version and as they are stated in the thread might not be 100% compatible...

Thread here - http://forums.mudlet.org/viewtopic.php?f=6&t=3529

For reference sake what I did was

"Install" the code in the main post of this thread as a script
"Install" the code below as another script (the code below creates a console "myConsole" that can be hidden by clicking the button "myLabel""

Then I created an alias to call the function test() in the second script above and the things got started

Now it is time to experiment.

/DaK

(I only write this to help beginners get things going - I have done nothing but simply "install" code found here)
Jor'Mox wrote:This should do something like what you are talking about.
Code: [show] | [select all] lua
local is_visible = false

function toggle_console()
	if is_visible then
		windowManager.hide("myConsole")
		is_visible = false
	else
		windowManager.show("myConsole")
		is_visible = true
	end
end

function test()
	windowManager.create("myLabel","label","10%","10%","20%","10%","topleft")
	windowManager.create("myConsole","miniconsole","10%","20%","40%","40%","topleft")
	windowManager.show("myLabel")
	windowManager.hide("myConsole")
	echo("myLabel",[[<font color ="#FFFFFF">My Label</font>]])
	setLabelClickCallback("myLabel","toggle_console")
end

Post Reply