Drag and Drop GUI Framework

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

Drag and Drop GUI Framework

Post by Jor'Mox »

This script makes a GUI framework that allows the user to add Geyser windows/containers to any of 6 existing spots, 4 of which are tab switched to allow multiple windows/containers in them. The containers can be resized and windows can be moved around on the fly by the user (right click and drag on the resize labels or tabs to move them around). Important note, this script works entirely through the use of Geyser, and will not work with GUI objects from another GUI management system.

The script configures itself and creates all its necessary elements the first time GUIframe.addWindow is called. It is also at that point that it captures values from GUIframe.configs, changes to configs after this point will not be reflected in the behavior of the script. If you want to have your GUI remember how it has been adjusted by the user, it is necessary for your script to call GUIframe.saveSettings when you want to save the current configuration, and then GUIframe.loadSettings when you want to load those settings from the saved file.

Key Functions:
GUIframe.addWindow(window, name [this is the text shown on the tab], target container [top, bottom, topleft, bottomleft, topright, bottomright], hideTabText [optional])
GUIframe.removeWindow(name [as given in GUIframe.addWindow call])
GUIframe.enable(side [left, right, top, bottom])
GUIframe.disable(side, hide [optional, if false only disables resizing of that side])
GUIframe.saveSettings()
GUIframe.loadSettings(redraw screen [this changes the background color from and back to black])
GUIframe.activate(name)
GUIframe.styleTab(name, styleString)

Key Other Info:
GUIframe.configs - this is a table which a dependent script can write to in order to change how the GUI created by this script looks when initially setup. All options seen in GUIframe.defaults can be set in GUIframe.configs.

The attached .png files are a set of the arrow images I used with the resize labels, with varying levels of transparency, with the 20t version being the most transparent, 50t being the least transparent, and blue_arrows being fully opaque (intended for when the label is moused over).
Code: [show] | [select all] lua
-- Jor'Mox's GUIframe Script
-- 3/07/2019
-- v1.4.2

-- To resize frames or move tabs, right click and drag either the resize label or the tab
-- until the desired result is achieved.

-- To add a window to a frame for the script to manage, use the
-- GUIframe.addWindow(window, name, container, hideText) function, where the window
-- variable contains the Geyser object you want to add, the name variable contains
-- the name you want it to be referred to as, which also is used as the text printed
-- on the associated tab that is created, the container variable is a string containing
-- one of the following: bottom, top, topleft, topright, bottomleft, bottomright,
-- and the hideText variable is an optional boolean which, if true, prevents text being
-- written on the tab for this window.

-- To remove a window from GUIframe, use the GUIframe.removeWindow(name, container)
-- function, where the name variable is the same name you gave the window when adding it,
-- and the optional container variable is a string specifying which container to remove
-- the window from. If no container is specified, the window is removed regardless of
-- which container it is in.

-- Resizing of frames can be enabled or disabled using the GUIframe.enable(side) and
-- GUIframe.disable(side, hide) functions respectively. If the second argument to
-- GUIframe.disable is false, then the entire set of frames on that side is hidden, and
-- the border is adjusted as if that side had be resized to zero.

-- To save and load settings, use the GUIframe.saveSettings() and
-- GUIframe.loadSettings(redraw) functions. If the redraw argument is true, the border
-- background color is changed to black to force the area of the borders to be redrawn.
-- Additionally, the GUIframe.reinitialize() function can be used to force the script to
-- initialize itself again, going back to default settings.

-- To activate a tab without it being clicked, use the GUIframe.activate(name) function.
-- And to apply a stylesheet to a tab that is different from the default stylesheet, use
-- the GUIframe.styleTab(name, style) function, where the style variable contains a string
-- with the CSS to be applied. Since tabs are styled only when created or when this
-- function is used, there should be no concern with this styling being overwritten.

GUIframe = GUIframe or {}

local mainW, mainH = getMainWindowSize()
local halfW, halfH = math.floor(mainW/2), math.floor(mainH/2)

GUIframe.configs = GUIframe.configs or {}

GUIframe.defaults = {
    tabHeight = 20,
    tabStyle = [[
        background-color: green;
        border-width: 2px;
        border-style: outset;
        border-color: limegreen;
        border-top-left-radius: 10px;
        border-top-right-radius: 10px;
        margin-right: 1px;
        margin-left: 1px;
        qproperty-alignment: 'AlignCenter | AlignCenter';]],
    tabEchoStyle = '<center><p style="font-size:14px; color:white">',
    leftStartWidth = 50,
    leftStartHeight = halfH,
    rightStartWidth = 50,
    rightStartHeight = halfH,
    topStartHeight = 50,
    bottomStartHeight = 50,
    resizeHeight = 30,
    resizeWidth = 30,
    resizeHoverImage = "/imgs/blue_arrows.png",
    resizeRestImage = "/imgs/blue_arrows_30t.png",
    borderOffset = 5,
}

GUIframe.windows = GUIframe.windows or {}
GUIframe.tabs = GUIframe.tabs or {}
GUIframe.tabCoords = GUIframe.tabCoords or {}
GUIframe.sides = GUIframe.sides or {left = 'enabled', right = 'enabled', top = 'enabled', bottom = 'enabled'}

local resize_style = "border-image: url(%s%s);"

local configs = table.update(GUIframe.defaults, GUIframe.configs)
local tabsInfo, containerInfo, resizeInfo

local container_names = {'topLeftContainer', 'bottomLeftContainer', 'topRightContainer',
    'bottomRightContainer', 'bottomContainer', 'topContainer'}
local tab_names = {'topLeftTabs', 'topRightTabs', 'bottomLeftTabs', 'bottomRightTabs'}
local resizeLabels = {'resizeLeft', 'resizeRight', 'resizeTop', 'resizeBottom'}
local sides = {"top","bottom","left","right"}
local side_containers = {
    left = {"topLeftContainer","bottomLeftContainer","topLeftTabs","bottomLeftTabs"},
    right = {"topRightContainer","bottomRightContainer","topRightTabs","bottomRightTabs"},
    top = {"topContainer"},
    bottom = {"bottomContainer"}
}

local function get_window_coords(win, update) -- gets coords for window, stores data in tabCoords table as needed
    local x, y = win:get_x(), win:get_y()
    local w, h = win:get_width(), win:get_height()
    if update then
        GUIframe.tabCoords[win.name]  = {x = x, y = y, w = w, h = h}
    end
    return x, y, w, h
end

local function check_overlap(tab, x, y) -- checks to see if given coords overlap tab or tab container
    if type(tab) == "string" then tab = GUIframe[tab] or GUIframe.tabs[tab] end
    if tab.hidden or tab.auto_hidden then return false end
    local info = GUIframe.tabCoords[tab.name]
    local x1, y1 = info.x, info.y
    local x2, y2 = x1 + info.w, y1 + info.h
    return (x >= x1 and x <= x2 and y >= y1 and y <= y2)
end

local function update_tab(tab, x, y, w, h) -- resizes and moves tab and updates tab coords table
    tab:move(x, y)
    tab:resize(w,h)
    local info = GUIframe.tabCoords[tab.name] or {}
    info.x, info.y = tab:get_x(), tab:get_y()
    info.w, info.h = tab:get_width(), tab:get_height()
    if table.contains(tab_names, tab.name) then
        info.container = true
    end
    GUIframe.tabCoords[tab.name] = info
end

local function get_containers(pos)
    if type(pos) == "table" then pos = pos.name end
    for _,w in ipairs({'right','left','container','tabs'}) do
        pos = pos:gsub(w,w:title())
    end
    local con, tab
    if string.find(pos,"Container") then
        con = GUIframe[pos]
        if not con then return end
        tabs = con.tabs
    elseif string.find(pos,"Tabs") then
        tabs = GUIframe[pos]
        if not tab then return end
        con = tabs.con
    else
        con = GUIframe[pos.."Container"]
        tabs = GUIframe[pos.."Tabs"]
    end
    return con, tabs
end

local function config()
    configs = table.update(GUIframe.defaults, GUIframe.configs)
    GUIframe.windows = {}
    GUIframe.tabCoords = {}

    tabsInfo = {
        topLeftTabs = {name = 'topLeftTabs', x = 0, y = 0, width = configs.leftStartWidth,
            height = configs.tabHeight},
        bottomLeftTabs = {name = 'bottomLeftTabs', x = 0, y = configs.leftStartHeight,
            width = configs.leftStartWidth, height = configs.tabHeight},
        topRightTabs = {name = 'topRightTabs', x = mainW - configs.rightStartWidth, y = 0,
            width = configs.rightStartWidth, height = configs.tabHeight},
        bottomRightTabs = {name = 'bottomRightTabs', x = mainW - configs.rightStartWidth,
            y = configs.rightStartHeight, width = configs.rightStartWidth, height = configs.tabHeight},
    }
    containerInfo = {
        topLeftContainer = {name = 'topLeftContainer', x = 0, y = configs.tabHeight,
            width = configs.leftStartWidth, height = configs.leftStartHeight - configs.tabHeight},
        bottomLeftContainer = {name = 'bottomLeftContainer', x = 0, y = configs.leftStartHeight + configs.tabHeight,
            width = configs.leftStartWidth, height = configs.leftStartHeight - configs.tabHeight},
        topRightContainer = {name = 'topRightContainer', x = mainW - configs.rightStartWidth,
            y = configs.tabHeight, width = configs.rightStartWidth,
            height = configs.rightStartHeight - configs.tabHeight},
        bottomRightContainer = {name = 'bottomRightContainer', x = mainW - configs.rightStartWidth,
            y = configs.rightStartHeight + configs.tabHeight, width = configs.rightStartWidth,
            height = configs.rightStartHeight - configs.tabHeight},
        bottomContainer = {name = 'bottomContainer', x = configs.leftStartWidth,
            y = mainH - configs.bottomStartHeight, height = configs.bottomStartHeight,
            width = mainW - configs.leftStartWidth - configs.rightStartWidth},
        topContainer = {name = 'topContainer', x = configs.leftStartWidth, y = 0, height = configs.topStartHeight,
            width = mainW - configs.leftStartWidth - configs.rightStartWidth}
    }
    resizeInfo = {
        resizeLeft = {name = 'resizeLeft', x = configs.leftStartWidth,
            y = configs.leftStartHeight - configs.resizeHeight / 2, height = configs.resizeHeight,
            width = configs.resizeWidth},
        resizeRight = {name = 'resizeRight', x = configs.rightStartWidth - configs.resizeWidth,
            y = configs.rightStartHeight - configs.resizeHeight / 2, height = configs.resizeHeight,
            width = configs.resizeWidth},
        resizeTop = {name = 'resizeTop', x = halfW - configs.resizeWidth / 2,
            y = configs.topStartHeight, height = configs.resizeHeight, width = configs.resizeWidth},
        resizeBottom = {name = 'resizeBottom', x = halfW - configs.resizeWidth / 2,
            y = mainH - configs.bottomStartHeight - configs.resizeHeight, height = configs.resizeHeight,
            width = configs.resizeWidth}
    }

    for name, cons in pairs(containerInfo) do
        GUIframe[name] = Geyser.Container:new(cons)
    end
    for name, cons in pairs(tabsInfo) do
        GUIframe[name] = Geyser.Container:new(cons)
        local cname = name:gsub("Tabs","Container")
        GUIframe[cname].tabs = GUIframe[name]
        GUIframe[name].con = GUIframe[cname]
    end
    local style = resize_style
    local path = getMudletHomeDir()
    path = path:gsub("[\\/]","/")
    configs.resizeRestImage = configs.resizeRestImage:gsub("[\\/]","/")
    configs.resizeHoverImage = configs.resizeHoverImage:gsub("[\\/]","/")
    local no_image
    if not (io.exists(path .. configs.resizeHoverImage) and io.exists(path .. configs.resizeRestImage)) then
        debugc("GUIframe: config: resize image(s) not found")
        path = "255,20,147,"
        style = "background-color: rgba(%s%s);"
        no_image = true
    end

    for name, cons in pairs(resizeInfo) do
        GUIframe[name] = Geyser.Label:new(cons)
        GUIframe[name]:setColor(0,0,0,0)
        GUIframe[name]:setStyleSheet(string.format(style, path, (no_image and "100") or configs.resizeRestImage))
        GUIframe[name]:setOnEnter("GUIframe."..name..".setStyleSheet", GUIframe[name],
            string.format(style, path, (no_image and "255") or configs.resizeHoverImage))
        GUIframe[name]:setOnLeave("GUIframe."..name..".setStyleSheet", GUIframe[name],
            string.format(style, path, (no_image and "100") or configs.resizeRestImage))
        GUIframe[name]:setClickCallback("GUIframe.buttonClick", name)
        GUIframe[name]:setReleaseCallback("GUIframe.buttonRelease", name)
        GUIframe[name]:setMoveCallback("GUIframe.buttonMove", name)
    end
    setBorderLeft(configs.leftStartWidth + configs.borderOffset)
    setBorderRight(configs.rightStartWidth + configs.borderOffset)
    setBorderTop(configs.topStartHeight + configs.borderOffset)
    setBorderBottom(configs.bottomStartHeight + configs.borderOffset)
    GUIframe.initialized = true
end

local function deselectContainer(container, tabs)
    -- hide all windows in container
    for _, win in pairs(container.windowList) do
        win:hide()
        win.active = false
    end
    -- unhighlight all tabs in tabs container
    if tabs then
        for _, tab in pairs(tabs.windowList) do
            local name = tab.name:gsub("Tab","")
            local show = GUIframe.windows[name].showText
            if show then
                tab:echo(configs.tabEchoStyle..name)
            end
	    end
	end
end

local function adjustTabs(tabs)
    if not GUIframe.initialized then error("GUIframe not initialized",2) end
    -- remove duplicated window names
    local found = {}
    for k,v in ipairs(tabs.windows) do
        if not table.contains(found,v) and tabs.windowList[v] and not tabs.windowList[v].isClicked then
            table.insert(found,v)
        end
    end
    -- calculate tab width and set height
    local w, h = math.floor(100 / #tabs.windows), configs.tabHeight
    local function wrap(num) return tostring(num) .. "%" end
    -- resize and reposition all tabs
    local shown, first
    for k,v in ipairs(found) do
        local tab = tabs.windowList[v]
        if not first then first = v:gsub("Tab","") end
        if not shown and tab.active then
            shown = v
        elseif tab.active then
            tab.active = false
        end
        update_tab(tab, wrap(w * (k-1)), 0, wrap(w), h)
    end
    if first and not shown and GUIframe.windows[first] then GUIframe.windows[first]:show() end
    tabs.space_pos = nil
end

local function reorderTabs(tabs, name, pos)
    local windows = tabs.windows
    while table.contains(windows, name) do
        table.remove(windows, table.index_of(windows, name))
    end
    table.insert(windows, pos, name)
end

local function makeSpace(tabs, tab, pos)
    if not GUIframe.initialized then error("GUIframe not initialized",2) end
    local windows = table.deepcopy(tabs.windows)
    local space_pos = tabs.space_pos
    local tab_pos = table.index_of(windows, tab.name)
    -- calculate tab width and set height
    local num_tabs = #windows + 1
    if tab_pos then
        num_tabs = num_tabs - 1
        if pos > tab_pos then pos = pos - 1 end
        if pos == space_pos then pos = pos + 1 end
    elseif space_pos and pos >= space_pos then
        pos = pos + 1
    end
    local w, h = math.floor(100 / num_tabs), configs.tabHeight
    local function wrap(num) return tostring(num) .. "%" end
    -- resize and reposition all tabs
    if tab_pos then table.remove(windows,tab_pos) end
    for k,v in ipairs(windows) do
        if k >= pos then
            update_tab(tabs.windowList[v], wrap(w * k), 0, wrap(w), h)
        else
            update_tab(tabs.windowList[v], wrap(w * (k-1)), 0, wrap(w), h)
        end
    end
    tabs.space_pos = pos
end

local function round(num,roundTo)
	local b, r = math.modf(num/roundTo)
	if r >= 0.5 then
		b = b + 1
	end
	return b * roundTo
end

local function setBorder(side, val)
    local funcs = {left = setBorderLeft, right = setBorderRight, top = setBorderTop, bottom = setBorderBottom}
    val = math.max(val,0)
	funcs[side](val)
end

local function resizeContainers(side, w, h)
    if table.contains({"left", "right"}, side) then
        local info = {
            left = {resize = "resizeLeft", cons = {"topLeftContainer","bottomLeftContainer"},
                tabs = {"topLeftTabs","bottomLeftTabs"}, x = 0, w = w},
            right = {resize = "resizeRight", cons = {"topRightContainer","bottomRightContainer"},
                tabs = {"topRightTabs","bottomRightTabs"}, x = w, w = mainW - w}
        }
        info = info[side]
        -- move and resize top, bottom and tab containers
        update_tab(GUIframe[info.tabs[1]], info.x, 0, info.w, configs.tabHeight)
        update_tab(GUIframe[info.tabs[2]], info.x, h, info.w, configs.tabHeight)
        GUIframe[info.cons[1]]:resize(info.w, h - configs.tabHeight)
        GUIframe[info.cons[1]]:move(info.x, configs.tabHeight)
        GUIframe[info.cons[2]]:resize(info.w, mainH - h - configs.tabHeight)
        GUIframe[info.cons[2]]:move(info.x, h + configs.tabHeight)
        -- adjust border size
        setBorder(side, info.w + configs.borderOffset)

        -- adjust width of top and bottom containers
        local x, y
        x = (GUIframe.sides.left ~= "hidden" and GUIframe.topLeftContainer:get_width()) or 0
        w = ((GUIframe.sides.right ~= "hidden" and GUIframe.topRightContainer:get_x()) or mainW) - x
        for _, con in ipairs({GUIframe.topContainer, GUIframe.bottomContainer}) do
            y, h = con:get_y(), con:get_height()
            con:resize(w, h)
            con:move(x, y)
        end
    elseif table.contains({"top", "bottom"}, side) then
        local x = 0
        w = mainW
        if GUIframe.sides.left ~= "hidden" then
            w = w - GUIframe.topLeftContainer:get_width()
            x = GUIframe.topLeftContainer:get_width()
        end
        if GUIframe.sides.right ~= "hidden" then w = w - GUIframe.topRightContainer:get_width() end
        local info = {top = {con = "topContainer", y = 0, h = h}, bottom = {con = "bottomContainer", y = h, h = mainH - h}}
        local con = GUIframe[info[side].con]
        con:resize(w, info[side].h)
        con:move(x, info[side].y)
        setBorder(side, info[side].h + configs.borderOffset)
    end
end

local function refresh()
    if not GUIframe.initialized then error("GUIframe not initialized",2) end
    mainW, mainH = getMainWindowSize()
    local rH, rW = configs.resizeHeight, configs.resizeWidth
    local x, y, w
    -- adjust bottom left and right container heights
    for _, C in ipairs({GUIframe.bottomLeftContainer, GUIframe.bottomRightContainer}) do
        C:resize(C:get_width(), mainH - C:get_y())
    end
    -- reposition right containers
    w = GUIframe.topRightContainer:get_width()
    for _, C in ipairs({GUIframe.topRightContainer, GUIframe.topRightTabs,
        GUIframe.bottomRightContainer, GUIframe.bottomRightTabs}) do
        C:move(mainW - w,C:get_y())
    end
    -- resize and reposition bottom and top containers
    w, x = mainW, 0
    if GUIframe.sides.left ~= "hidden" then
        w = w - GUIframe.topLeftContainer:get_width()
        x = GUIframe.topLeftContainer:get_width()
    end
    if GUIframe.sides.right ~= "hidden" then w = w - GUIframe.topRightContainer:get_width() end
    for _, C in ipairs({GUIframe.topContainer, GUIframe.bottomContainer}) do
        C:resize(w, C:get_height())
        C:move(x, C.name == "topContainer" and 0 or mainH - C:get_height())
    end
    -- reposition resize labels
    x, y = GUIframe.topLeftContainer:get_width(), GUIframe.bottomLeftTabs:get_y()
    GUIframe.resizeLeft:move(x, y - rH / 2)
    x, y = GUIframe.topRightContainer:get_x(), GUIframe.bottomRightTabs:get_y()
    GUIframe.resizeRight:move(x - rW, y - rH / 2)
    x = (GUIframe.topContainer:get_width() - rW) / 2
    if GUIframe.sides.left ~= "hidden" then x = x + GUIframe.topLeftContainer:get_width() end
    y = GUIframe.topContainer:get_height()
    GUIframe.resizeTop:move(x, y)
    y = GUIframe.bottomContainer:get_y()
    GUIframe.resizeBottom:move(x, y - rH)
end

-- enables the resize label for the given side and shows all associated containers if hidden
function GUIframe.enable(side)
    if not GUIframe.initialized then error("GUIframe not initialized",2) end
    if not table.contains(sides,side) then error("GUIframe.enable: invalid side",2) end
    local cons = side_containers[side]
    for _, con in ipairs(cons) do
        GUIframe[con]:show()
        for _,win in pairs(GUIframe[con].windowList) do -- loop can be removed after Geyser fix comes in
            if win.active then win:show() end
        end
    end
    if table.contains({"left","right"}, side) then
        setBorder(side, GUIframe[cons[1]]:get_width() + configs.borderOffset)
    else
        setBorder(side, GUIframe[cons[1]]:get_height() + configs.borderOffset)
    end
    GUIframe["resize"..side:title()]:show()
    GUIframe.sides[side] = "enabled"
    refresh()
end

-- disables and hides the resize label for the given side, and hides all associated containers if indicated
function GUIframe.disable(side, hide)
    if not GUIframe.initialized then error("GUIframe not initialized",2) end
    if not table.contains(sides,side) then error("GUIframe.disable: invalid side",2) end
    local cons = side_containers[side]
    GUIframe.sides[side] = "disabled"
    if hide then
        for _, con in ipairs(cons) do
            GUIframe[con]:hide()
            for _, win in pairs(GUIframe[con].windowList) do -- loop can be removed after Geyser fix comes in
                if win.type == "mapper" then win:hide() end
            end
        end
        local border = _G["setBorder"..side:title()]
        border(0)
        GUIframe.sides[side] = "hidden"
    end
    GUIframe["resize"..side:title()]:hide()
    refresh()
end

-- adds a Geyser window or container to the given container, with a tab showing the given name if applicable
function GUIframe.addWindow(window, name, container, hideText)
    if not GUIframe.initialized then config() end
    if type(container) == "table" then container = container.name end
    local con, tabs = get_containers(container)
    if not con then error("GUIframe.addWindow: invalid container name",2) end
    if not name then error("GUIframe.addWindow: name argument required",2) end
    -- remove window from any containers
    for _, tcon in ipairs(container_names) do
        if table.contains(GUIframe[tcon].windows, window.name) then
            GUIframe.removeWindow(name, tcon)
        end
    end
    deselectContainer(con,tabs)
    -- add tab for window, if applicable
    if tabs then
        local showText = not hideText
        window.showText = showText
        local lbl = Geyser.Label:new({name = name.."Tab", x = 0, y = 0, width = 10, height = 10},tabs)
        lbl:setStyleSheet(configs.tabStyle)
        if showText then
            lbl:echo(configs.tabEchoStyle.."<b>"..name)
        end
        lbl:setClickCallback("GUIframe.buttonClick", name)
        lbl:setReleaseCallback("GUIframe.buttonRelease", name)
        lbl:setMoveCallback("GUIframe.buttonMove", name)
        GUIframe.tabs[name] = lbl
        adjustTabs(tabs)
    end
    -- add window to container and set size and position
    con:add(window)
    window:resize("100%","100%")
    window:move(0,0)
    window:show()
    GUIframe.windows[name] = window
	raiseEvent("sysWindowResizeEvent")
end

-- removes a named Geyser window or container from the named container (using name given in GUIframe.addWindow)
function GUIframe.removeWindow(name, container)
    if not GUIframe.initialized then error("GUIframe not initialized",2) end
    if not container then container = GUIframe.windows[name].container end
    local con, tabs = get_containers(container)

    if not con or not table.contains(container_names, con.name) then
        error("GUIframe.removeWindow: invalid container name",2)
    end
    if not name then error("GUIframe.removeWindow: name argument required",2) end
    if tabs then
        local lbl = tabs.windowList[name.."Tab"]
        if lbl then
            tabs:remove(lbl)
            adjustTabs(tabs)
            lbl:hide()
        end
    end
    local window = GUIframe.windows[name]
    con:remove(window)
    window:hide()
end

-- saves the current GUI setup, including the size of the different containers and what windows go in which container
function GUIframe.saveSettings()
    if not GUIframe.initialized then error("GUIframe not initialized",2) end
    local saveTbl = {}
    local w, h = GUIframe.topLeftContainer:get_width(), GUIframe.bottomLeftTabs:get_y()
    saveTbl.left = {w = w, h = h}
    w, h = GUIframe.topRightContainer:get_width(), GUIframe.bottomRightTabs:get_y()
    saveTbl.right = {w = w, h = h}
    w, h = GUIframe.topContainer:get_width(), GUIframe.topContainer:get_height()
    saveTbl.top = {w = w, h = h}
    w, h = GUIframe.bottomContainer:get_width(), GUIframe.bottomContainer:get_height()
    saveTbl.bottom = {w = w, h = h}

    -- get added windows and containers they are assigned to
    local windows = {}
    local text = {}
    for k,v in pairs(GUIframe.windows) do
        local con = v.container.name
        windows[con] = windows[con] or {}
        table.insert(windows[con], k)
        text[con] = text[con] or {}
        text[con][k] = v.showText
    end
    -- reorder windows to match tab order for tabbed containers
    for con, wins in pairs(windows) do
        if con:find("Left") or con:find("Right") then
            local tabs = GUIframe[con].tabs.windows
            local new = {}
            for k,v in ipairs(tabs) do
                local wname = v:gsub("Tab","")
                table.insert(new, {wname, text[con][wname]})
            end
            windows[con] = new
        end
    end
    saveTbl.windows = windows
    saveTbl.sides = GUIframe.sides
    table.save(getMudletHomeDir() .. "/GUIframeSave.lua", saveTbl)
end

-- loads GUI setup from a previous save
function GUIframe.loadSettings(redraw)
    if not GUIframe.initialized then error("GUIframe not initialized",2) end
    local saveTbl = {}
    local path = getMudletHomeDir() .. "/GUIframeSave.lua"
    path = path:gsub("\\","/")
    mainW, mainH = getMainWindowSize()
    if not io.exists(path) then debugc("GUIframe.loadSettings: save file doesn't exist.") return end
    table.load(path, saveTbl)
    resizeContainers("left", saveTbl.left.w, saveTbl.left.h)
    resizeContainers("right", mainW - saveTbl.right.w, saveTbl.right.h)
    resizeContainers("top", saveTbl.top.w, saveTbl.top.h)
    resizeContainers("bottom", saveTbl.bottom.w, mainH - saveTbl.bottom.h)
    for con, wins in pairs(saveTbl.windows) do
        for _,name in ipairs(wins) do
            if type(name) == "string" then
                GUIframe.addWindow(GUIframe.windows[name], name, con)
            else
                local n, s = name[1], not name[2]
                GUIframe.addWindow(GUIframe.windows[n], n, con, s)
            end
        end
    end

    for side, state in pairs(saveTbl.sides) do
        if state == "enabled" then
            GUIframe.enable(side)
        elseif state == "disabled" then
            GUiframe.disable(side,false)
        elseif state == "hidden" then
            GUIframe.disable(side,true)
        end
    end
    -- force redraw of screen
    if redraw then
        setBackgroundColor(1,1,1)
        setBackgroundColor(0,0,0)
    end
end

-- can be called to force the script to run its config function again
function GUIframe.reinitialize()
    config()
end

-- can be called to activate a given tab without clicking on it
function GUIframe.activate(name)
    if not GUIframe.initialized then error("GUIframe not initialized",1) end
	local window = GUIframe.windows[name]
	if window then
        local con, tabs = get_containers(window.container.name)
        -- hide and unhighlight other windows and tabs
        deselectContainer(con, tabs)
        -- show selected window
        window:show()
        window.active = true
        -- highlight selected tab
        if window.showText then
            GUIframe.tabs[name]:echo(configs.tabEchoStyle.."<b>"..name)
        end
    end
end

-- can be called to apply a style to a given tab
function GUIframe.styleTab(name, style)
    if not GUIframe.initialized then error("GUIframe not initialized",1) end
	local tab = GUIframe.tabs[name]
	if tab then
	    tab:setStyleSheet(style)
    end
end

-- internally used function to handle button click callbacks
function GUIframe.buttonClick(name, event)
    if not GUIframe.initialized then error("GUIframe not initialized",2) end
	if table.contains(resizeLabels,name) then
	    if event.button == "RightButton" then
	        local lbl = GUIframe[name]
	        lbl.difX, lbl.difY = event.x, event.y
	        lbl.savedX, lbl.savedY = getMousePosition()
            GUIframe[name].isClicked = true
        end
	elseif event.button == "LeftButton" then
        local window = GUIframe.windows[name]
        local con, tabs = get_containers(window.container.name)
        -- hide and unhighlight other windows and tabs
        deselectContainer(con, tabs)
        -- show selected window
        window:show()
        window.active = true
        -- highlight selected tab
        if window.showText then
            GUIframe.tabs[name]:echo(configs.tabEchoStyle.."<b>"..name)
        end
    elseif event.button == "RightButton" then
        local window, tab = GUIframe.windows[name], GUIframe.tabs[name]
        tab.savedX, tab.savedY = getMousePosition()
        tab.difX, tab.difY, tab.isClicked = event.x, event.y, true
        -- force update of coords for all tabs and tab containers
        GUIframe.tabCoords = {}
        for _, name in ipairs(tab_names) do
            get_window_coords(GUIframe[name], true)
            for tname, tab in pairs(GUIframe[name].windowList) do
                get_window_coords(tab, true)
            end
        end
    end
    raiseEvent("GUIframe.buttonClick",name,event)
end

-- internally used function to handle button release callbacks
function GUIframe.buttonRelease(name, event)
    if not GUIframe.initialized then error("GUIframe not initialized",2) end
	if table.contains(resizeLabels,name) then
	    if event.button == "RightButton"  then
            local lbl = GUIframe[name]
            lbl.savedX, lbl.savedY, lbl.difX, lbl.difY, lbl.isClicked = nil, nil, nil, nil, false
        end
	elseif event.button == "RightButton" then
	    local window, tab = GUIframe.windows[name], GUIframe.tabs[name]
	    local con, tabs = get_containers(window.container.name)
	    tab.difX, tab.difY, tab.savedX, tab.savedY, tab.isClicked = nil, nil, nil, nil, false
	    hideWindow("show_container")
	    for _, tname in ipairs(tab_names) do
	        local info = GUIframe[tname]
	        if info.mouse_over then
	            local pos = info.space_pos
	            info.mouse_over = nil
	            GUIframe.addWindow(window, name, tname:gsub("Tabs",""), not window.showText)
	            if pos then
    	            reorderTabs(info, tab.name, pos)
    	            adjustTabs(info)
    	        end
	        end
	    end
	    adjustTabs(tabs)
	end
	raiseEvent("GUIframe.buttonRelease",name,event)
end

-- internally used function to handle button move callbacks
function GUIframe.buttonMove(name, event)
    if not GUIframe.initialized then error("GUIframe not initialized",2) end
	if table.contains(resizeLabels,name) then
	    lbl = GUIframe[name]
	    if lbl.isClicked then
	        local w, h = getMousePosition()
	        w, h = round(w - lbl.difX, 10), round(h - lbl.difY, 10)
            mainW, mainH = getMainWindowSize()
            local side, cW, cH, rX, rY
            local minX = GUIframe.sides.left ~= "hidden" and GUIframe.topLeftContainer:get_width() or 0
            local maxX = GUIframe.sides.right ~= "hidden" and GUIframe.topRightContainer:get_x() or mainW
            local minY = GUIframe.sides.top ~= "hidden" and GUIframe.topContainer:get_height() or 0
            local maxY = GUIframe.sides.left ~= "hidden" and GUIframe.bottomContainer:get_y() or mainH
            local mid, min, max = GUIframe.topContainer:get_width(), math.min, math.max
            local tabH, rH, rW = configs.tabHeight, configs.resizeHeight, configs.resizeWidth
	        w, h = max(w, 0), max(h, 0)
            local info = { -- specify position of resize labels and size of containers
                resizeLeft = {side = "left", x = min(w, maxX - rW),
                    y = min(max(h + rH / 2,tabH), mainH - tabH) - rH / 2,
                    w = min(w, maxX - rW), h = min(max(h + rH / 2,tabH), mainH-tabH) },
                resizeRight = {side = "right", x = min(max(w, minX), mainW),
                    y = min(max(h + rH / 2, tabH), mainH - tabH) - rH / 2,
                    w = min(max(w, minX), mainW - rW) + rW, h = min(max(h + rH / 2, tabH), mainH - tabH) },
                resizeTop = {side = "top", x = minX + (mid - rW) / 2,
                    y = min(h, maxY - rH), w = maxX - minX, h = min(h, maxY - rH) },
                resizeBottom = {side = "bottom", x = minX + (mid - rW) / 2,
                    y = min(max(h, minY) - rH, mainH), w = maxX - minX, h = min(max(h, minY) + rH, mainH)} }
            info = info[name]
            lbl:move(info.x, info.y)
            resizeContainers(info.side, info.w, info.h)
        end
    else
        local window, tab = GUIframe.windows[name], GUIframe.tabs[name]
        local con, tabs = get_containers(window.container.name)
        local x, y = getMousePosition()
        local over_con, over_tab
        if tab and tab.isClicked then
            moveWindow(tab.name, x - tab.difX, y - tab.difY)
            -- check to see if mouse is over any tab containers
            for _, tcon in ipairs(tab_names) do
                if check_overlap(tcon, x, y) then
                    over_con = tcon
                    GUIframe[tcon].mouse_over = true
                    local info = GUIframe.tabCoords[tcon]
                    local tx, ty, tw, th = info.x, info.y, info.w, info.h
                    createLabel("show_container", 0, 0, 0, 0, 1)
                    moveWindow("show_container", tx, ty)
                    resizeWindow("show_container", tw, th)
                    setLabelStyleSheet("show_container",[[
                        background-color: black;
                        border: 2px solid white;]])
                    showWindow("show_container")
                    lowerWindow("show_container")
                    -- check to see if mouse is over any tabs
                    for tname, info in pairs(GUIframe.tabs) do
                        if tname ~= name and check_overlap(info, x, y) then
                            over_tab = info.name
                            local windows = GUIframe[tcon].windows
                            local index = table.index_of(windows,over_tab)
                            makeSpace(GUIframe[tcon],tab,index)
                            break
                        end
                    end
                    break
                end
            end
            -- remove any unnecessary spaces in tab containers
            for _, name in ipairs(tab_names) do
                if name ~= over_con then
                    adjustTabs(GUIframe[name])
                    GUIframe[name].mouse_over = nil
                end
            end
        end
	end
	raiseEvent("GUIframe.buttonMove",name,event)
end

-- internally used function to handle sysWindowResizeEvent
function GUIframe.eventHandler(event,...)
    if event == "sysWindowResizeEvent" and GUIframe.initialized then
        refresh()
    end
end

registerAnonymousEventHandler("sysWindowResizeEvent","GUIframe.eventHandler")
Edit: Updated the script to fix a bug that popped up for someone using this on a fresh profile (because I clearly didn't do a thorough enough job checking for such things).

Edit2: Decided to do a significant rewrite of the script to make it remain in the background until called upon.

Edit3: Squashed a few more minor bugs. Also decided to try to streamline the code somewhat.

Edit4: Change to try to fix resize label image loading in Windows.

Edit5: Change to ensure frame sides remain the same when calling GUIframe.loadSettings regardless of the size of the current main window.

Edit6: Hopefully fixing path issues for Windows.

Edit7:
  • Should now properly position resize labels to match values in GUIframe.defaults if those are set in another script.
  • Hopefully fixing issue with selected tab not matching shown window when loading settings.
  • Added optional argument to GUIframe.addWindow to hide tab text (true to hide text, false or nil to show text).
  • Added function to activate a specific tab: GUIframe.activate(name)
  • Added function to apply custom stylesheet to a specific tab: GUIframe.styleTab(name, styleString)
Edit8: Bug fix to correct tab text disappearing when moved.
Last edited by Jor'Mox on Sat Mar 09, 2019 4:45 pm, edited 19 times in total.

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

Re: Drag and Drop GUI Framework

Post by Jor'Mox »

I forgot, here is a brief demo video of this script in action.
Mudlet Frame Demo.mp4
(4.9 MiB) Downloaded 1825 times

edit: Zaphob: Watch on Youtube:
https://www.youtube.com/watch?v=VWbugxsCmNA
Attachments
blue_arrows_50t.png
blue_arrows_50t.png (8.35 KiB) Viewed 21854 times
blue_arrows_40t.png
blue_arrows_40t.png (7.53 KiB) Viewed 21854 times
blue_arrows_30t.png
blue_arrows_30t.png (6.73 KiB) Viewed 21854 times
blue_arrows_20t.png
blue_arrows_20t.png (5.94 KiB) Viewed 21854 times
blue_arrows.png
blue_arrows.png (8.93 KiB) Viewed 21854 times
Last edited by Jor'Mox on Fri Jan 11, 2019 1:09 am, edited 1 time in total.

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

Re: Drag and Drop GUI Framework

Post by Jor'Mox »

Someone asked on Discord that I share the setup I used for the demo video, so here it is. I had this in an alias and was using it while doing development and testing, so it is pretty basic. But I think it shows how little needs to be done to make use of this script.
Code: [show] | [select all] lua
mapper = Geyser.Mapper:new({name = "myMapper"})
chat = Geyser.MiniConsole:new({name = "myChat"})
redbox = Geyser.Label:new({name = "RedBox", color = "red", x = 0, y = 0, width = 0, height = 0})
bluebox = Geyser.Label:new({name = "BlueBox", color = "blue", x = 0, y = 0, width = 0, height = 0})
greenbox = Geyser.Label:new({name = "GreenBox", color = "green", x = 0, y = 0, width = 0, height = 0})
orangebox = Geyser.Label:new({name = "OrangeBox", color = "orange", x = 0, y = 0, width = 0, height = 0})
yellowbox = Geyser.Label:new({name = "YellowBox", color = "yellow", x = 0, y = 0, width = 0, height = 0})

GUIframe.addWindow(mapper,"Map","topright")
GUIframe.addWindow(chat,"Chat","topright")
GUIframe.addWindow(redbox,"Red","bottomright")
GUIframe.addWindow(bluebox,"Blue","bottomright")
GUIframe.addWindow(greenbox,"Green","bottomright")
GUIframe.addWindow(orangebox,"Orange","top")
GUIframe.addWindow(yellowbox,"Yellow","bottom")

Caled
Posts: 403
Joined: Thu Apr 09, 2009 4:45 am

Re: Drag and Drop GUI Framework

Post by Caled »

Vadi suggested a nice demo for people may be to combine Jor'Mox excellent work with Akaya's Geyser Template from this thread:
https://forums.mudlet.org/viewtopic.php?f=6&t=4098

Which is a simple beginning point for building your own UI. It's been around for a long time and even as someone relatively confident with Geyser, I've used this several times to build from over the years.

I had a look to see if the two could be combined, and it can be. I doubt I would ever use the result - using GUIframe is so easy, most of the 'laying out' part is bypassed because of the drag-and-drop nature of the script. But, it may be useful to some people wondering how to use GUIframe. Another 'demo' if you will.
Attachments
AkayasGeyserTemplate-with-JorMoxsGUIFrame.xml
(4.98 KiB) Downloaded 1151 times

Tamarindo
Posts: 10
Joined: Sat Jul 28, 2018 10:53 pm

Re: Drag and Drop GUI Framework

Post by Tamarindo »

The GUIframe + Geyser implementation is in place on StickMUD. Find it on the default list of games in Mudlet. Great script work authors and we appreciate the enhancements you have made!

jieiku
Posts: 33
Joined: Fri Mar 01, 2019 1:25 pm
Contact:

Re: Drag and Drop GUI Framework

Post by jieiku »

Thank You! I really like this script.

I was trying this out today, when I right click a tab and move it to another box, the label text on the tab dissapears, and the tab is just blank.

I am using Mudlet 3.17.1 on Ubuntu 18.04 with KDE

Edit1: I just got another idea inspired by kde desktop toolbox.

you could have an additional GUI label, that shows a small locked or unlocked padlock, that can be placed anywhere on the screen.

left clicking this label will lock or unlock the gui, displaying or hiding the drag-able resize labels.

while in the unlocked state there could also be a save button/label visible next to the lock button.

jieiku
Posts: 33
Joined: Fri Mar 01, 2019 1:25 pm
Contact:

Re: Drag and Drop GUI Framework

Post by jieiku »

Is it possible to modify the label of a container tab?

In my Case I would like to modify the Mapper label that says "Map" to instead say the area name of the Area I am currently in.

I already parse this data, for the mapper so it would be cool if I could update the tab handle to the name of the "Area - Room"

for example "Aylor - Phoenix Square"
Last edited by jieiku on Thu Mar 07, 2019 4:47 pm, edited 1 time in total.

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

Re: Drag and Drop GUI Framework

Post by Jor'Mox »

EDIT: See below.
Last edited by Jor'Mox on Thu Mar 07, 2019 4:13 pm, edited 1 time in total.

jieiku
Posts: 33
Joined: Fri Mar 01, 2019 1:25 pm
Contact:

Re: Drag and Drop GUI Framework

Post by jieiku »

Thanks for the bug fix and for telling me how to do this:

Code: Select all

GUIframe.tabs.Map:echo("Aylor")
Edit, I created a function:

Code: Select all

function mapLabel(maptext)
	GUIframe.tabs.Map:echo('<p style="font-size:13px; color = white"><b>' .. maptext)
end
Then in my Script that processes the Room GMCP data I did this:

Code: Select all

mapLabel(room_data.zone:gsub("^%l", string.upper) .. " - " .. room_data.name)
Now as I walk around the mud the Tab/Label at the top of my Mapper displays the zone and room name such as : Aylor - Phoenix Square
Last edited by jieiku on Thu Mar 07, 2019 7:25 pm, edited 2 times in total.

jieiku
Posts: 33
Joined: Fri Mar 01, 2019 1:25 pm
Contact:

Re: Drag and Drop GUI Framework

Post by jieiku »

Here are some Ideas that I have for extended Drag and Drop GUI Framework:

* Floating miniconsoles, that snap relative to another regions corner.
This is different than the tabs that snap into a container, because it actually floats above other elements.
For example my minimap, that I have manually snapped to the top right corner of the main output window.
These floating windows would snap in this fashion when you drag a Tab near to the corner of another container.
When a container is a floating window/miniconsole it should have some resize handles of its own.

* Lock/Unlock button, another label like your draggable blue arrows.
when this button is clicked it affectively locks the gui, by hiding all the draggable arrows.

* Save button, this button could be positioned next to the Lock/Unlock button.
it would also only be visible when in the unlocked state.
another idea would be to have it autosave when you put the UI into locked state.
or maybe an option to configure it to save every time it is locked.

* Favorite Tab selection, In a group of tabs, be able to mark a tab as favorite,
so that on guiLoad it is the displayed/activated tab.
If no favorite is set then show the left most tab out of a group of tabs.
I currently do this: GUIframe.activate("Chat"), without this the "Tell" tab would end up being active.
Maybe just having the left most tab be the default would make things natural enough.
Either that or have it remember which tab was last active between sessions.
I realize I solved my problem simple enough with GUIframe.activate("Chat"), but I
am thinking making things more intuitive/natural is good because not everyone is a Pro at configuring.

* Add a configuration option to display tabs with or without the rounded edges.
I prefer the square look myself, I realize the outside edges might not actually be clickable, but
I think that is a non issue for most people, they can figure out that they need to click toward middle.
What is cool about my Lock/Unlock idea is you can add all sorts of buttons for configuring Drag-n-Drop without
it cluttering the screen, because then once you lock your UI they would all be hidden.
So you could for example have Another label that is clickable that lets you toggle between rounded or square tabs.
this button label would be hidden while the UI is in the locked state, could be placed near the lock and save buttons.

* Dynamically allocate new tab regions, instead of having 6 predefined regions.
Out of all my ideas, I think this one would be the most complex, but it also offers the most creative freedom.
Would be based on the position of the mouse while dragging a tab.
If you drag the tab near to or between other tabs, it would dock into that container like it normally does.
However If you drag the tab to the Left Center edge of a Container, it would then split that container vertically, and
that tab would then be docked on the left and the old container on the right.
Similarly if you drag a tab to the bottom edge of an existing container, it would be split horizontally, and
that tab would be docked to the bottom and the old container on the top.

Photoshopped image illustrating some of my ideas (please excuse the poor graphics):
dragNdrop.png

Post Reply