• Listen to a special audio message from Bill Roper to the Hive Workshop community (Bill is a former Vice President of Blizzard Entertainment, Producer, Designer, Musician, Voice Actor) 🔗Click here to hear his message!
  • Read Evilhog's interview with Gregory Alper, the original composer of the music for WarCraft: Orcs & Humans 🔗Click here to read the full interview.

UIWindow

Level 19
Joined
Mar 18, 2012
Messages
1,716

UIWindow


Struct UIWindow is the parent struct you base your windows in an UIScreen on.



JASS:
library UIWindow uses UIButton, UIBorder, UIBasic, UIScreen/* v1.0
*****************************************************************************
*
*   Each custom UI is build of one UIScreen having a unlimited amount of UIWindows.
*   Struct UIWindow is the parent struct you base your windows in an UIScreen on.
*
*   Realistic example: 
*       struct Inventory extends UIScreen
*       struct Backpack extends UIWindow.
*       struct Equipmenet extends UIWindow ...
*
****************************************************************************
*/
    
    globals
        //*  Required room for data storage:
        //*  ===============================
        private constant hashtable TABLE   = InitHashtable()
        private constant hashtable TRACKER = InitHashtable()
    endglobals
    
    //*  Parent struct of all custom windows.
    struct UIWindow
        
        //*  Stub methods:
        //*  =============
        public stub method onHover    takes UIScreen whichScreen, UIButton hovered returns nothing
        endmethod
        public stub method onClick    takes UIScreen whichScreen, UIButton clicked returns nothing
        endmethod
        public stub method onShow     takes UIScreen whichScreen, boolean flag returns nothing
        endmethod
        public stub method onShowPage takes UIScreen whichScreen, boolean flag returns nothing
        endmethod
    
        //*  Readonly members:
        //*  =================
        readonly UIScreen screen
        readonly UIBorder border
        readonly Table    cells 
        readonly real     lastClick
        readonly real     originX
        readonly real     originY
        readonly real     width
        readonly real     height
    
        //*  I honestly don't know how this method performs on Battle.net
        //* In single player games it works flawless. ( latency? )
        method isDoubleClick takes nothing returns boolean
            return (TimerGetElapsed(UI_STAMP) - lastClick < UI_MOUSE_DOUBLE_CLICK)
        endmethod
    
        //*  Get the amount of buttons in a window.
        readonly static constant integer TABLE_SIZE = -1
        method getButtonCount takes nothing returns integer
            return cells[TABLE_SIZE]
        endmethod
        
        //*  User-friendly type-casting.
        method getButton takes integer index returns UIButton
            debug call ThrowWarning(not cells.has(index), "UIWindow", "getButton", "index", this, "No UIButton saved on this index [" + I2S(index) + "]!")
            return cells[index]
        endmethod
        
        method operator enabled takes nothing returns boolean
            return screen.isWindowOfTypeOpen(getType())
        endmethod
        
        //*  User-friendly getters.
        method operator user takes nothing returns player
            return screen.user
        endmethod
        method operator userId takes nothing returns integer
            return screen.userId
        endmethod
        
        //*  Get window dimensions:
        //*  ======================
        //! textmacro UI_WINDOW_SIZE_OPERATOR takes NAME, SUFFIX, EXTRA
            public method operator $NAME$$SUFFIX$ takes nothing returns real
                return origin$SUFFIX$ $EXTRA$
            endmethod
        //! endtextmacro
        
        //! runtextmacro optional UI_WINDOW_SIZE_OPERATOR("max", "X", "+ width")
        //! runtextmacro optional UI_WINDOW_SIZE_OPERATOR("max", "Y", "+ height")
        //! runtextmacro optional UI_WINDOW_SIZE_OPERATOR("min", "X", "")
        //! runtextmacro optional UI_WINDOW_SIZE_OPERATOR("min", "Y", "")
        //! runtextmacro optional UI_WINDOW_SIZE_OPERATOR("center", "X", "+ width*.5")
        //! runtextmacro optional UI_WINDOW_SIZE_OPERATOR("center", "Y", "+ height*.5")
        
        //*  Multiple pages:
        //*  ===============
        //*  The page API is still in its infancy.
        //* I will work on it when I have time.
        readonly integer pages
        readonly integer page
        readonly integer pageSize 
        private  boolean circle//*  When nextPage() hits the maximum, should we go back to page 1? ( same for prevPage() )
  debug private  boolean invalid

        //*  Optional getters:
        //*  =================
        //*  Optional getters:
        //*  =================
        static if LIBRARY_UIBoard then
            method operator board takes nothing returns UIBoard 
                return screen.userId
            endmethod
        endif
        static if LIBRARY_UISound then
            method operator audio takes nothing returns UISound
                return screen.userId
            endmethod
        endif
        static if LIBRARY_UITextbox and LIBRARY_Font then
            method operator textbox takes nothing returns UITextbox
                return screen.userId
            endmethod        
        endif
        
        //*  Window Dummy:
        //*  =============
        //*  The dummy inherits the window coordinates. Just pass in a offset.
        method createDummy takes player p, integer dummyId, real offsetX, real offsetY, real offsetZ, real face, real scale returns unit
            local boolean prev = ToogleUnitIndexer(false)
            set bj_lastCreatedUnit = CreateUnit(p, dummyId, originX + offsetX, originY + offsetY, 1)
            call ToogleUnitIndexer(prev)
            call SetUnitFacing(bj_lastCreatedUnit, face + 1.)
            call SetUnitFacing(bj_lastCreatedUnit, face)
            call SetUnitScale(bj_lastCreatedUnit, scale*screen.scale, 0., 0.)
            call UnitAddAbility(bj_lastCreatedUnit, 'Aloc')
            call UnitAddAbility(bj_lastCreatedUnit, 'Amrf')
            call UnitRemoveAbility(bj_lastCreatedUnit, 'Amrf')
            call SetUnitFlyHeight(bj_lastCreatedUnit, offsetZ, 0.)
            call SetUnitX(bj_lastCreatedUnit, originX + offsetX)
            call SetUnitY(bj_lastCreatedUnit, originY + offsetY)
            if (GetLocalClient() != user) then
                call SetUnitVertexColor(bj_lastCreatedUnit, 0, 0, 0, 0)// More safe then hide/show unit for a local client.
            endif
            call PauseUnit(bj_lastCreatedUnit, true)
            return bj_lastCreatedUnit
        endmethod
        
        //*  Pages API:
        //*  ==========
        //* Set(), Next(), Prev() ... all return a boolean for success.
        private method showPage takes boolean flag returns nothing
            local UIButton b
            local integer i    = pageSize*(page - 1)
            local integer size = i + pageSize
            loop
                exitwhen (i == size)
                set b = getButton(i)
                if (b.enabled != flag) then
                    set b.enabled = flag
                    call b.show(flag)
                endif
                set i = i + 1
            endloop
            call onShowPage(screen, flag)
        endmethod
        
        method setPage takes integer index returns boolean
            if not (enabled)  or (index > pages) or (index == page) or (index < 1) then
                return false
            endif
            call showPage(false)
            set page = index
            call showPage(true)
            return true
        endmethod
        
        method prevPage takes nothing returns boolean
            local integer prev = page - 1
            if (pages == 1) or not (enabled) then
                return false
            elseif (prev < 1) then
                if not (circle) then
                    return false
                endif
                set prev = pages
            endif 
            call showPage(false)
            set page = prev
            call showPage(true)
            return true
        endmethod        
        
        method nextPage takes nothing returns boolean
            local integer next = page + 1
            if (pages == 1) or not (enabled) then
                return false
            elseif (next > pages) then
                if not (circle) then
                    return false
                endif
                set next = 1
            endif
            call showPage(false)
            set page = next
            call showPage(true)
            return true
        endmethod
        
        //*  Pages are copies of the first page. API may change in the future.
        //* They also can inherit Button data if you want.
        private static thistype var   = 0 
        private static boolean  trans = false
        private static method newPage takes nothing returns nothing
            local thistype this  = thistype.var
            local boolean import = thistype.trans 
            local integer index  = 0
            local integer size   = cells[thistype.TABLE_SIZE]
            local UIButton copy  = 0
            set pages            = pages + 1
            loop
                exitwhen index == pageSize
                set copy = getButton(index)
                if (copy.defaultId == 0) then
                    set cells[size] = UIButton.createImg(copy.typeId, user, copy.defaultStr, copy.track, copy.scale, false)
                else
                    set cells[size] = UIButton.createDest(copy.typeId, user, copy.defaultId, copy.track, copy.scale, false)
                endif
                if (import) then
                    set UIButton(cells[size]).data = copy.data
                endif
                set size        = size +1 
                set index = index + 1
            endloop
            set cells[thistype.TABLE_SIZE] = size
        endmethod
        
        //*  Pages API:
        //*  ==========
        //*  Add() generates x copies of the first page.
        method addPages takes integer amount, boolean import, boolean circulate returns nothing
            debug call ThrowError((amount   <=  0), "UIWindow", "addPages", "amount",  this, "Inavlid argument ( amount <= 0 )!")
            debug call ThrowError((amount   >= 10), "UIWindow", "addPages", "amount",  this, "The maximum number of allowed pages is 9!")
            debug call ThrowError((invalid),        "UIWindow", "addPages", "invalid", this, "Additional Pages Can Only Be Set Once!")
            debug set invalid = true
            set pageSize = cells[thistype.TABLE_SIZE]
            set circle   = circulate
            loop
                exitwhen 0 >= amount
                set thistype.var   = this
                set thistype.trans = import
                call ExecuteFunc(thistype.newPage.name)
                set amount = amount - 1
            endloop
        endmethod
        
        //*  Show a window:
        //*  ==============
        method show takes boolean flag returns nothing
            if (this == 0) then
                return
            endif
            
            if (flag) and (GetPlayerUI(screen.user) == screen) then
                if (screen.openWindowOfType(getType())) then
                    set page = 1
                else
                    return
                endif
                
            elseif not (flag) then 
                if not (screen.closeWindowOfType(getType())) then
                    return
                endif
            else
                return
            endif
            
            if (GetLocalClient() == screen.user) then
                call border.show(flag)
            endif
            
            call onShow(screen, flag)
            call showPage(flag)
        endmethod
        
        //**
        //*  Track search & save:
        //*  ====================
        //*
        //* Trackables are poorly supported in Warcraft III. In fact we can't
        //* remove handles of type "trackable" once they are no longer used.
        //* Therefore I came up with an algorithm to search out of "all ever created Tracks in UIWindows"
        //* these which match 100% to the one we wish to create and use that Track instance instead.
        //*
        //*  library TileDefinition creates unique integers per tile in your map.
        //* Tracks are saved in stacks on the tile id in an hashtable.
        //* With an 64x64 track model the loop will exit after (1 - 3) * players loops.
        //* For bigger model it will be 1 * players, for smallers the loopup time will increase.
        //*
        //* Due to an hashtable lookup during both trackable events ( hover & click ),
        //* a Track instance can evaluate any UIWindow onHover/onClick stub method without
        //* running into code collision with other UIs which use the same Track instance.
        
        //*  Credits to IcemanBo:
        //*  ====================
        static if not LIBRARY_TileDefinition then
            private static integer WorldTiles_X = 0
            private static integer WorldTiles_Y = 0
            private static method setVars takes nothing returns nothing
                set WorldTiles_X = R2I(WorldBounds.maxX - WorldBounds.minX) / 128 + 1
                set WorldTiles_Y = R2I(WorldBounds.maxY - WorldBounds.minY) / 128 + 1
            endmethod
            private static method GetTileId takes real x, real y returns integer
                local integer xI = R2I(x - WorldBounds.minX + 64) / 128
                local integer yI = R2I(y - WorldBounds.minY + 64) / 128
                if ((xI < 0) or (xI >= .WorldTiles_X) or (yI < 0) or (yI >= .WorldTiles_Y)) then
                    return -1
                endif
                return (yI * .WorldTiles_X + xI)
            endmethod
        endif
        
        private method saveTrack takes Track track returns Track
            local integer id   = GetTileId(track.x, track.y)
            local integer size = LoadInteger(TRACKER, id, 0) + 1
            call SaveInteger(TRACKER, id,  size, track) 
            call SaveInteger(TRACKER, id, -size, userId) 
            call SaveInteger(TRACKER, id,  0,    size) 
            return track
        endmethod
        
        private method searchTrack takes string model, real x, real y, real z, real face returns Track
            local integer id   = GetTileId(x, y)
            local integer size = LoadInteger(TRACKER, id, 0)
            local Track t  
            loop
                exitwhen 0 == size
                if (LoadInteger(TRACKER, id, -size) == userId) then
                    set t = LoadInteger(TRACKER, id, size)
                    if (t.x == x) and (t.y == y) and (t.z == z) and (t.facing == face) and (t.model == model) then
                        return t
                    endif
                endif
                set size = size - 1
            endloop
            return saveTrack(Track.createForPlayer(model, x, y, z, face, user))
        endmethod

        //*  UIButton API:
        //*  =============
        //*  Adds an UIButton to your custom UI. The function uses a search & save algorithm for Tracks
        //* ( read above ) to optimize the trackable handle count in your map.
        method addDestButton takes string model, real x, real y, real z, real face, integer objectId, real scale returns UIButton
            local integer typeId = getType()
            local integer size   = getButtonCount()
            local Track track    = searchTrack(model, originX + x, originY + y, z, face)  
            debug call ThrowError(invalid, "UIWindow", "addButton", "invalid", this, "Can't add further UIButtons, once method addPages is called!")
            set cells[size]      = UIButton.createDest(typeId, user, objectId, track, scale*screen.scale, false)
            set cells[TABLE_SIZE]= size + 1
            set pageSize         = size + 1
            call SaveInteger(TABLE, screen, -track, this)
            call SaveInteger(TABLE, screen,  track, size)
            return cells[size]
        endmethod
        
        method addImageButton takes string model, real x, real y, real z, real face, string file, real scale returns UIButton
            local integer typeId = getType()
            local integer size   = getButtonCount()
            local Track track    = searchTrack(model, originX + x, originY + y, z, face)       
            debug call ThrowError(invalid, "UIWindow", "addButton", "invalid", this, "Can't add further UIButtons, once method addPages is called!")
            set cells[size]      = UIButton.createImg(typeId, user, file, track, scale*screen.scale, false)
            set cells[TABLE_SIZE]= size + 1
            set pageSize         = size + 1
            call SaveInteger(TABLE, screen, -track, this)
            call SaveInteger(TABLE, screen,  track, size)
            return cells[size]
        endmethod
        
        //*  Wrapper methods to ease the usage of UIBorder. ( adds originX / originY to the desired coordinates )
        method frame takes real borderScale returns nothing
            call border.construct(originX + width*.5, originY + height*.5, width, height, borderScale*screen.scale)
        endmethod
        method frameEx takes real extraX, real extraY, real borderScale returns nothing
            call border.construct(originX + width*.5, originY + height*.5, width + extraX, height + extraY, borderScale*screen.scale)
        endmethod
        method addBorder takes real x, real y, real sizeX,real sizeY, real borderScale returns nothing 
            call border.construct(originX + x + sizeX*.5, originY + y + sizeY*.5, sizeX, sizeY, borderScale*screen.scale)
        endmethod
        method addBorderImage takes string file, real x, real y, real sizeX, real sizeY, integer imageType returns image
            return border.addImage(file, sizeX, sizeY, originX + x, originY + y, imageType)
        endmethod
        method addScreenImage takes string file, real x, real y, real sizeX, real sizeY, integer imageType returns image
            return border.addImage(file, sizeX, sizeY, screen.originX + x, screen.originY + y, imageType)
        endmethod
        
        //*  Creates a new UIWindow for the player allocated in the UIScreen argument! 
        static method create takes UIScreen whichScreen, real x, real y, real sx, real sy returns thistype
      debug local boolean  error1 = (x + sx > whichScreen.width) 
      debug local boolean  error2 = (y + sy > whichScreen.height)
      debug local boolean  error3 = (x < 0.) 
      debug local boolean  error4 = (y < 0.) 
      
            local thistype this = thistype.allocate()
            set cells           = Table.create()
            set originX         = whichScreen.originX + x
            set originY         = whichScreen.originY + y
            set width           = sx
            set height          = sy
            set screen          = whichScreen
            set border          = UIBorder.create()
            set pages           = 1
            set page            = 1
            set pageSize        = 0
            debug set invalid   = false
            
            debug call ThrowError(error1, "UIWindow", "create", "error1", this, "X coordinates of the window are out of the screen bounds [" + R2S(x + sx) + "]!")
            debug call ThrowError(error2, "UIWindow", "create", "error2", this, "Y coordinates of the window are out of the screen bounds [" + R2S(y + sy) + "]!")
            debug call ThrowError(error3, "UIWindow", "create", "error3", this, "X coordinates of the window are out of the screen bounds [" + R2S(x) + "]!")
            debug call ThrowError(error4, "UIWindow", "create", "error4", this, "Y coordinates of the window are out of the screen bounds [" + R2S(y) + "]!")
            return this         
        endmethod
        
        //*  Trackable events:
        //*  =================
        private static method onAnyHover takes nothing returns boolean
            local UIScreen UI      = GetPlayerUI(Track.tracker)
            local thistype this    = LoadInteger(TABLE, UI, -Track.instance)
            local integer  index   = LoadInteger(TABLE, UI,  Track.instance) + (pageSize*(page - 1))
            local UIButton hovered = cells[index]
            if (this != 0) and (hovered.enabled) then
                call onHover(UI, hovered)
            endif      
            return false
        endmethod
        
        private static method onAnyClick takes nothing returns boolean
            local UIScreen UI      = GetPlayerUI(Track.tracker)
            local thistype this    = LoadInteger(TABLE, UI, -Track.instance)
            local integer  index   = LoadInteger(TABLE, UI,  Track.instance) + (pageSize*(page - 1)) 
            local UIButton clicked = cells[index]
            if (0 != this) and (clicked.enabled) then
                call onClick(UI, clicked)
                set lastClick = TimerGetElapsed(UI_STAMP)
            endif      
            return false
        endmethod
        
        //*  Trackables remain! As clean as possible.
        method destroy takes nothing returns nothing
            local integer index = 0
            local integer size  = getButtonCount()
            call deallocate()
            loop
                exitwhen index == size
                call getButton(index).destroy()                
                set index = index + 1
            endloop
            call cells.destroy()
            call border.destroy()
        endmethod
        
        //*  Executed from UIScreen:
        //*  =======================
        //*  via function.execute()
        static method recursiveShow    takes thistype this, boolean flag returns nothing
            call show(flag)
        endmethod
        static method recursiveDestroy takes thistype this returns nothing
            call destroy()
        endmethod
        
        private static method init takes nothing returns nothing
            call Track.registerAnyClick(function thistype.onAnyClick)
            call Track.registerAnyHover(function thistype.onAnyHover)
            static if not LIBRARY_TileDefinition then
                call thistype.setVars()
            endif
        endmethod
        implement UIInit
        
        //*  Only available in debug mode. Highlights the borders of your screen.
        //* So you can check ingame, that you settings are correct.
        debug public method highlight takes nothing returns nothing
            debug local string flash = "MFPB"
            debug call AddLightning(flash, false, originX, originY, originX, originY + height)
            debug call AddLightning(flash, false, originX, originY + height, originX + width, originY + height)
            debug call AddLightning(flash, false, originX + width, originY, originX, originY)
            debug call AddLightning(flash, false, originX + width, originY + height, originX + width, originY)
        debug endmethod
        
    endstruct
    

    
endlibrary

Changelog:
v1.1 - UIBorders are now also the same way relative to originX/originY as UIWindows are.
Last edited:
Top