• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

UIPackage [ Full Screen System ]

Status
Not open for further replies.
Level 19
Joined
Mar 18, 2012
Messages
1,716

UIPackage


For your custom UI needs.


Click to view an ingame screenshot:
216087-albums7785-picture104227.jpg


Click to view the required code to create the window shown above ( fully MUI ):
JASS:
library UIDemo initializer Init uses UIPackage

    //*  Main screen. Always extend from UIScreen.
    struct DemoScreen extends UIScreen
        //*  add your code here.
    endstruct

    //*  Window within the screen. Always extend from UIWindow.
    struct DemoWindow extends UIWindow
    
        //*  stub method onHover from UIWindow.
        private method onHover takes DemoScreen screen, UIButton hovered returns nothing
            call BJDebugMsg("Data on this button is " + I2S(hovered.data)) 
        endmethod    
    
        //*  stub method onClick from UIWindow.
        private method onClick takes DemoScreen screen, UIButton clicked returns nothing
            if (clicked.data == 1) then//*  Close button.
                call screen.show(false)
            endif
            //*  UISound API.
            call audio.bigButtonClick()
        endmethod
    
        
        static method onCreate takes thistype this returns nothing
            local string s    = "war3mapImported\\64x64Track.mdx" 
            local UIButton b1 = addDestButton(s, 128, 128, 0., 270., 'cwcc', 1.)
            local UIButton b2 = addDestButton(s, 256, 128, 0., 270., 'cwdp', 1.)

            //*  Buttons can have data. You can read the data when they are hovered or clicked.
            set b1.data = 1
            set b2.data = 'A00A'
            
            //*  Draw a border around the window. "1." is the border scaling.
            call frame(1.)
        endmethod
    
    endstruct
    
    //*  Window within the screen. Always extend from UIWindow.
    struct DemoWindow3 extends UIWindow
    
        static method onCreate takes thistype this returns nothing 
            local string s = "UI\\Widgets\\EscMenu\\Human\\human-options-button-background-disabled.blp"
            call frame(1.)
            call SetImageColor(addScreenImage(s, 0, 0, screen.width, screen.height, 4), 150, 255, 150, 255)
        endmethod
    
    endstruct
    
   //*  Window within the screen. Always extend from UIWindow.
    struct DemoWindow2 extends UIWindow
    
        static method onCreate takes thistype this returns nothing
            local string s          = "war3mapImported\\64x64Track.mdx" 
            local UIButton b1
            local UIButton b2
            
            //*  Add buttons to the window.
            //* Arguments:
            //* ==========
            //* s - Trackable file
            //* x, y, z, face, objectId, scale
            //*
            //* x and y are relative to the window sizes.
            //* Example: x = 32 means window.originX + 32
            //*          y = 32 means window.originY + 32
            
            //*    |
            //*    |
            //*  32|  b1    b2
            //*    |_________
            //*  0/0  32   64
            set b1 = addDestButton(s, 32, 32, 0., 270., 'B004', .75)
            
            
            set UI_BORDER_BACKGROUND = ""
            //*  Draw a border around the window. "1." is the border scaling.
            call frame(1.)
        endmethod
    
    endstruct
    
    
    globals
        private DemoScreen demo = 0
    endglobals
    
    private function InitDemo takes nothing returns nothing
        local UIWindow window 
        //*  Create a new UIScreen. The best width, height ratio is ~ 1.8
        set demo   = DemoScreen.create(Player(0), -2000, -2000, 1632, 906)
        
        //*  Add a window to the screen.
        set window = DemoWindow.create(demo, 100, 100, 384, 256)
        
        //*  JassHelper limitation. We have to add the window by ourself.
        call demo.addWindowOfType(window.getType(), window, true)
        
        //*  Another JassHelper limitation. Can't execute onCreate....
        //* We can do that also by ourself
        call DemoWindow.onCreate(window)
        
        set window = DemoWindow2.create(demo, 700, 500, 64, 70)
        call demo.addWindowOfType(window.getType(), window, true)
        call DemoWindow2.onCreate(window)
        
        set window = DemoWindow3.create(demo, 700, 100, 256, 256)
        call demo.addWindowOfType(window.getType(), window, true)
        call DemoWindow3.onCreate(window)
        
        //*  In debug you can draw a lightings to see everything looks good.
        debug call demo.highlight()
        debug call window.highlight()
    endfunction
    
    //*  We show the UI via Esc event:
    //*  =============================
    private function OnEsc takes nothing returns nothing
        call demo.show(not demo.enabled)
    endfunction
    
    private function Init takes nothing returns nothing
        local trigger t = CreateTrigger()
        call TriggerRegisterPlayerEventEndCinematic(t, Player(0))
        call TriggerAddAction(t, function OnEsc)
        call InitDemo()
    endfunction
    
endlibrary
The UIPackage consists out of the following libraries:

UIPackageDescription
[td]Collection of the entire UIPackage API.
[tr][TD][TD]Collection of data and functions used all over the UI package.
[tr][td][td]Handles the camerasetup while viewing a custom UI.
[tr][TD][TD]Struct UIScreen is the parent struct you base your UIs on.
[tr][td][td]Struct UIWindow is the parent struct you base your windows in an UIScreen on.
[tr][TD][TD]Creates borders, including a background image for full screen systems.
[tr][td][td]Creates interactable buttons for a UIWindow.
[tr][TD]
UISound ( optional )
[TD]Play sounds while beeing in an UI.
[tr][td]
UIBoard (optional )
[td]Creates a multiboard per player within the UI.
[tr][TD]
UIText ( optional )
[TD]Creates textboxes from TextSplats. ( not yet started to code )
[tr]

Changelog

Demo: Inventory

So I started to create a full screen inventory using the UIPackage.
It's still far from beeing ready, but if you want you can take a look into it.

One thing which I discovered is that you should only place multiple UI
on the same location, if they are identical. Otherwise the trackable events
are not reliable.
 

Attachments

  • Inventory Interface Map.w3x
    592.4 KB · Views: 132
Last edited:
Level 23
Joined
Apr 16, 2012
Messages
4,041
debug call BJDebugMsg(SCOPE_PREFIX + "Error: Invalid imageType argument[" + R2S(imageType) + "]!")

SCOPE_PREFIX is actually pretty ugly, you can use thistype inside the string, and it will convert into the struct name, despite being inside string.
 
Looks like a great start. Could we get a screenshot with some different sizes to see how it looks?

And I like your "fields" idea. It gives the user a lot of power, yet the API is still nice and neat. Speaking of fields, I recommend adding fields for the dimensions of the background (width/height). That way the user can specify exactly how he wants the background image to fit into the frame.

Keep it up!

@btdonald: The annoying thing about using images vs. dests is that icon images would have to have a 1px transparent border around them. This is (1) tedious to do if you want to support a lot of icons (2) takes up a bunch of file space. That is why it is generally better to use destructables with a replaceable texture.
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
@btdonald: The annoying thing about using images vs. dests is that icon images would have to have a 1px transparent border around them. This is (1) tedious to do if you want to support a lot of icons (2) takes up a bunch of file space. That is why it is generally better to use destructables with a replaceable texture.
The problem with that, though, is that you need one object editor destructable for each icon you want to use. On top of that, you actually need to register the rawcodes of these destructables to the system.

Both solutions have their disadvantages.
If you do the image stacking properly, the icon borders might not be visible as they get covered by images on top of the icons; so that's a possible solution to that. It requires tiling the background image, though.
 
Level 11
Joined
Dec 3, 2011
Messages
366
@btdonald: The annoying thing about using images vs. dests is that icon images would have to have a 1px transparent border around them. This is (1) tedious to do if you want to support a lot of icons (2) takes up a bunch of file space. That is why it is generally better to use destructables with a replaceable texture.

Images can be change or modify easier than dests but the proplem come from transparent border around it, but I think we can solve it.
 
Level 14
Joined
Jun 27, 2008
Messages
1,325
Its not about changing the images, there are tools which do that. But you would need to reimport them all, which takes up a lot of space.

If you dont mind using wurst, we have some code there which automatically creates the destructables and retrieves their rawcodes:

createIcon("path/to/icon.tga", x, y)
-> when executed on compiletime, creates a doodad with the texture "path/to/icon.tga".
-> when executed on runtme, places the doodad on the ground

I dont see how this could be done in vJass/JNGP, but at least some objectmerger magic could make it a bit easier.
 
Level 5
Joined
Dec 1, 2008
Messages
120
The problem with that, though, is that you need one object editor destructable for each icon you want to use. On top of that, you actually need to register the rawcodes of these destructables to the system.

Both solutions have their disadvantages.
If you do the image stacking properly, the icon borders might not be visible as they get covered by images on top of the icons; so that's a possible solution to that. It requires tiling the background image, though.
I don't understand how you see the "need to register the rawcodes" as a con if using destructables. You still need to register the image paths if you use images.

Also, from my own experience, there's always gonna be clear seam between images, which is why I switched to using destructables.

Plus, when using images, to make new border styles, you're required to Photoshop a little. With destructables that's just a matter of changing the replaceable texture in Object Editor.
 
Level 11
Joined
Dec 3, 2011
Messages
366
Its not about changing the images, there are tools which do that. But you would need to reimport them all, which takes up a lot of space.

If you dont mind using wurst, we have some code there which automatically creates the destructables and retrieves their rawcodes:

createIcon("path/to/icon.tga", x, y)
-> when executed on compiletime, creates a doodad with the texture "path/to/icon.tga".
-> when executed on runtme, places the doodad on the ground

I dont see how this could be done in vJass/JNGP, but at least some objectmerger magic could make it a bit easier.

Yep :)) Object merger can Save Our System :ogre_icwydt:
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
It's not like it would make much of a difference to use destructables instead of images, it's just that I hate systems that spam object data. If we can avoid that by masking the image borders with a tiled menu background, then I'd say this is worth the extra effort of having to import the background tiles manually.

Importing a few background tiles is imho superior than spaming object data; but that's just my personal oppinion. In the end, Bpower has to evaluate what he thinks works best for his system.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
Some click and local UI events could be good. UIWidget would also be nice. You could populate a window with UI Widgets.

A UI Editor in Java or something would be super excellent. In it, you'd create the UI and then it'd compile the required vJASS code using this system ; ). It could be like Winforms or Qt. Integrating it into WE would be even cooler. It could automatically compile the code during save and pass the code on to jasshelper. It'd also be able to automatically generate destructables or whatever you needed to do.

I'm thinking a UI lib needs to be a tool that's integrated into WE.
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
Some click and local UI events could be good. UIWidget would also be nice. You could populate a window with UI Widgets.

A UI Editor in Java or something would be super excellent. In it, you'd create the UI and then it'd compile the required vJASS code using this system ; ). It could be like Winforms or Qt. Integrating it into WE would be even cooler. It could automatically compile the code during save and pass the code on to jasshelper. It'd also be able to automatically generate destructables or whatever you needed to do.

I'm thinking a UI lib needs to be a tool that's integrated into WE.
Why overcomplicate the workflow when you can just tinker with the drawing methods until you get an optically pleasing result? With SSDs, compiling map code and loading the test map from within Newgen only takes a couple of seconds; you can just trial and error the layout until it looks good.
 
Level 5
Joined
Dec 1, 2008
Messages
120
I have a nice way to reduce the amount of object data and imports requires for the destructable method.

One model for border. Uses replaceable texture to change the style (Human, Orc..). Changing the destructables animation changes it's type (left, top, bottom-left, etc..).

One model for checkbox. Changing the destructables animation changes it's state, which are "enabled unchecked", "enabled checked", "disabled unchecked", and "disabled checked".

I also have a model for button, but I don't remember how that works.
 
Project on ice at the moment, but I will get to it when I have more time.

On my laptop, image rendering produced a high fps drop ( down to ~19 ) while destructables
worked perfectly ( ~58 - 60 ). Could be related to my machine, because it's not
working good anymore.

I had similar results when I tried using image overlays a long time ago. Adding enough images rendered on the screen caused a huge fps drop that I had to gut the feature. But I wasn't sure if it was just my machine either (back then). :p I guess this confirms their toll on performance.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
I started working on this again and updated the UIBorder library.
It's now as safe as possible concerning invalid image handles ( id == -1)

Sadly I can't provide tests, if destructables are better in performance than images, because
on my new computer it does always display 60 fps.

Next comes the excact same library, but for destructables ( same API ).
 
Level 11
Joined
Dec 3, 2011
Messages
366
I started working on this again and updated the UIBorder library.
It's now as safe as possible concerning invalid image handles ( id == -1)

Sadly I can't provide tests, if destructables are better in performance than images, because
on my new computer it does always display 60 fps.

Next comes the excact same library, but for destructables ( same API ).

Same as on my computer. With destructables, we can't change position of the destructable. It's a disavantage.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Updated:

I added a small demo, which you can start/stop by pressing ESC.
I tried to find a good solution to add multiple pages in one customUI ( inspired by backpacks in RPGS ), probably it's still bugged.

I moved completly away from destructables, as I run into a few strange problems with them.
( the mor you scale them, the more z offset they have. )

Still there will be a library, which supports destructables instead of images for those
who like to create object data / not like to work/import with tgas/blps
JASS:
        // 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.
        static if UIButton.USE_DESTRUCTABLE_HANDLE then
            method addButton takes string model, real x, real y, real z, real face, integer objectId, real scale returns UIButton
                local integer typeId = getType()
                local integer size   = cells[TABLE_SIZE]
                local Track track    = searchTrack(model, centerX + x, centerY + 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.create(typeId, user, objectId, track, scale, false)
                set cells[TABLE_SIZE]= size + 1
                call SaveInteger(UIWindow.TABLE, anyType, -track, this)
                call SaveInteger(UIWindow.TABLE, anyType,  track, size)
                return cells[size]
            endmethod
        elseif UIButton.USE_IMAGE_HANDLE then
            method addButton takes string model, real x, real y, real z, real face, string file, real scale returns UIButton
                local integer typeId = getType()
                local integer size   = cells[TABLE_SIZE]
                local Track track    = searchTrack(model, centerX + x, centerY + 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.create(typeId, user, file, track, scale, false)
                set cells[TABLE_SIZE]= size + 1
                call SaveInteger(UIWindow.TABLE, anyType, -track, this)
                call SaveInteger(UIWindow.TABLE, anyType,  track, size)
                return cells[size]
            endmethod
        endif
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Did you change something?

For me the map compile and runs all 4 cases:
1.debug mode on, vexorians jasshelper
2.debug mode on, cohadars jasshelper
3.debug mode off, vexorians jasshelper
4.debug mode off, cohadars jasshelper.

You should get an error message for error1 or error2, which says what you did wrong.
JASS:
            debug call ThrowError(error1, "UIMainWindow", "create", "error", 0, "X coordinates of the main window out of WorldBounds")
            debug call ThrowError(error2, "UIMainWindow", "create", "error", 0, "Y coordinates of the main window out of WorldBounds")
 
Level 11
Joined
Dec 3, 2011
Messages
366
Did you change something?

For me the map compile and runs all 4 cases:
1.debug mode on, vexorians jasshelper
2.debug mode on, cohadars jasshelper
3.debug mode off, vexorians jasshelper
4.debug mode off, cohadars jasshelper.

You should get an error message for error1 or error2, which says what you did wrong.
JASS:
            debug call ThrowError(error1, "UIMainWindow", "create", "error", 0, "X coordinates of the main window out of WorldBounds")
            debug call ThrowError(error2, "UIMainWindow", "create", "error", 0, "Y coordinates of the main window out of WorldBounds")

No. Only save the map.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Update:

UIPackage now requires ImageUtils

UIButton can now use destructables or/and images.

Therefore two creator methods exists ( UIButton.createDest, UIButton.createImg )
One button instance is limited to one type ( destructable or image )!

I was trying to find the fastest solution to determine what type is used as button.

1. extending struct is too slow here in my opinion. ( However most logical solution )
2. delegates can't solve this problem either. ( Since jst one delegate is working per struct )
3. Finally I decided that I add a simple if then condition to determine what type an instance user. :(
JASS:
        method purge takes nothing returns nothing
            if (anyType == TYPE_IMAGE) then
                if (img != null) then
                    call ReleaseImage(img)
                    set img = null
                endif
            elseif (anyType == TYPE_DESTRUCTABLE) then
                if (dest != null) then
                    call RemoveDestructable(dest)
                    set dest = null
                endif
            endif
        endmethod
4. I was hoping that type handle can store destructables and images,
but it turned out that i.e. RemoveDestructable(handle) is not valid.

Next to do is making UIBorder work with destructable.
Here is just one of the two UIBorders valid per map. Either destructables or images.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
A teaser how UIPackage can be used.
Let's assume we talk about a custom Inventory and we
want to add a backpack to the entire Inventory UI window.
JASS:
library Backpack initializer Init uses WindowUI

    globals
        private Backpack UI = 0
    endglobals

    struct Backpack extends UIWindow

        // actually passes integer anyType as first argument
        // but vJass is cool and we simply assume it's an Inventory instance.
        // passes the integer ( instance ) which you setup for this UI in create.
        public method onHover takes Inventory inv, UIButton hovered returns nothing
            call board.new(2, 7)// Can create multiboards. takes rows, colums
        endmethod
        
        public method onClick takes Inventory inv, UIButton clicked returns nothing
            if isDoubleClick() then// can detect double clicks.
                call nextPage()// supports multiple pages
            endif
            if clicked.data == 678679 then
                set clicked.data = 9// buttons can store/read data
            endif
            // Read more button members
            // clicked.x
            // clicked.y
            // clicked.scale
            // clicked.z
            // clicked.typeId ( will by Backpack.typeid )
            set clicked.enabled = false// Can disable button. No Track events fired from now on.
        endmethod
        
        public method onShow takes Inventory inv, boolean flag returns nothing
        endmethod


        // Can't be a stub method :(
        static method onCreate takes thistype this returns nothing
            local string trackModel = "war3mapImported\\64x64Track.mdx"
            local string buttonModel = "UI\\Console\\Human\\human-transport-slot.blp"
            
            // Add buttons to the UI.
            call addImageButton(trackModel, 256, 128, 0, 0, buttonModel,   1.)
            
            // Can also be a destructable model
            // method addDestButton takes string model, real x, real y, real z, real face, integer objectId, real scale returns UIButton
            call addDestButton(trackModel, -256, 128, 0, 0, 'B009', 1.)
            
            // add a window border rectangle. x,y, widht, height
            call addBorder(0, 128, 300, 600) 
        endmethod
    endstruct

    // Just an example. Imagine we have a struct Inventory, which does
    // support normal inventory stuff ( addItem, equipItem, ... )
    private function Init takes nothing returns nothing
        local Inventory inv       = Inventory.create('hfoo')
        
        // Each UI has a main window. It defines the first camerasetup on show.
        // takes player, posX, posY, widht, height
        local UIMainWindow window = UIMainWindow.create(Player(0), 0, 0, 1600, 900) 
        
        // We expect Backpack UI to be a member of Inventory.
        // In the example we just use a globals instance here.
        //
        // takes player, posX, posY, widht, height, mainWindow.
        set UI = Backpack.create(Player(0), 0, 0, 1600, 900, mainWindow)
        call Backpack.onCreate(UI)
        
        // Fire this on some trigger event ( e.g on end cinemtatic, ESC )
        // Runs automatically a proper camer setup.
        call UI.show(true)
    endmethod
    
endlibrary
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Fires now a trigger event when a player opens the first or closest the last UIWindow instance.

JASS:
    function IsPlayerInAnyUI takes player p returns boolean
        return (UIWindow.openWindows[GetPlayerId(p)].size != 0)
    endfunction
    constant function GetTriggerUIPlayer takes nothing returns player
        return Player(UI_PLAYER_INDEX)
    endfunction
    function RegisterPlayerInUIEvent takes code func returns nothing
        call TriggerAddCondition(UI_PLAYER_EVENT, Condition(func))
    endfunction

I created 3 systems for my map, with this package already. The first took me around ~15 minutes.
- a settings menu to toogle on/off certain game preference
- a Inventory system
- a Skill tree system

In single player the code of all 3 work perfect so far. I will test the UIPackage in multiplayer soon.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
I'm working on it. Promise :)

Custom UI development always consumes a lot of time and try and error.

With this package, is there to speed up the non-creative parts.
-> Camera setup ( zoom out, zoom in, ... ) btw if it's different per resolution used, then we are doomed.
-> borders ( seamless, show/hide on open/close )
-> button ( image or destructable )
-> trackable handling ( keep instance count low )
-> basic UI API ( show, isDoubleClick, nextPage, prevPage ) ..
-> Sound implementation ( mouse click sound, etc. )
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
I attached a demo of a custom inventory.

It's far from beeing finished and also not perfectly documented.

You can take a look if you want.

Things I should mention:
  • You can double click items in the UI. It will equip/unequip the item.
  • Drag & drop an item to the info box will drop the item from the inventory.
  • The ghoul is just there to show that inventories can look different per unit.
  • struct ItemClass is somewhere hidden in library CustomItem.
  • You should activate your sounds when testing.
  • I didn't do any multiplayer test yet.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
I wrote a small library, which should help to generate tooltips for custom items.

It uses pointers, which allow you to modify single blocks within the tooltip,
without destroying the whole tooltip structure. Useful for items with dynamic affixes.

You can define if a free space is added between two block ( pointers ), by default a space is added.
set nospace[TOOLTIP_POINTER_PRIMARY_AFFIX] = true
You can set a headline per pointer. A headline is only written if the item has also a textblock for this pointer.
set headline[TOOLTIP_POINTER_PRIMARY_AFFIX] = "Primary:"

Example for pointers. You make up your own.
JASS:
        //*  Always start with 1! 0 will not work.
        constant integer TOOLTIP_POINTER_HEADLINE      = 1
        constant integer TOOLTIP_POINTER_PRIMARY_AFFIX = 2
        constant integer TOOLTIP_POINTER_AFFIXES       = 3
        constant integer TOOLTIP_POINTER_SOCKETS       = 4
        constant integer TOOLTIP_POINTER_ITEM_SET      = 5
        constant integer TOOLTIP_POINTER_RUNE          = 6
        constant integer TOOLTIP_POINTER_STORY         = 7
        constant integer TOOLTIP_POINTER_DURABILITY    = 8
        constant integer TOOLTIP_POINTER_REQUIREMENTS  = 9
        constant integer TOOLTIP_POINTER_GOLD          = 10
        constant integer TOOLTIP_POINTER_LUMBER        = 11
JASS:
library CustomItemTooltip/* v1.0
*************************************************************************************
*
*   CustomItemTooltip handles tooltips for the "CustomItem" type.
*   Each tooltip is stored in lines and can be loaded row by row.
*   
*   Exampe:
*       row 1 - "Short Axe"
*       row 2 - " "
*       row 3 - "+5 Damage"
*       row 4 - " "  
*       row 5 - "Sell Value 75 Gold"
*
*************************************************************************************
*
*   */ uses /*
*  
*       */ optional Ascii        /*
*       */ optional WordWrap     /*
*       */ optional StringSize   /*
*       */ optional ErrorMessage /*
*
************************************************************************************
*/ 
    
    //*  Pointers:
    //*  ==============
    //*  Pointers point to a segment of the tooltip.
    //* You can use them to change, add, remove rows within the tooltip,
    //* while obtaining the structure of the whole tooltip.
    //* You can also flush a whole pointer, if it shouldn't be in the tooltip anymore.
    //*
    //* Pointers start with the index 1 and end with x.
    //* This list from 1 to 11 is just an example. Use as many as you need.
    //* Give them any name, which fits to your logic.
    globals    
        //*  Always start with 1! 0 will not work.
        constant integer TOOLTIP_POINTER_HEADLINE      = 1
        constant integer TOOLTIP_POINTER_PRIMARY_AFFIX = 2
        constant integer TOOLTIP_POINTER_AFFIXES       = 3
        constant integer TOOLTIP_POINTER_SOCKETS       = 4
        constant integer TOOLTIP_POINTER_ITEM_SET      = 5
        constant integer TOOLTIP_POINTER_RUNE          = 6
        constant integer TOOLTIP_POINTER_STORY         = 7
        constant integer TOOLTIP_POINTER_DURABILITY    = 8
        constant integer TOOLTIP_POINTER_REQUIREMENTS  = 9
        constant integer TOOLTIP_POINTER_GOLD          = 10
        constant integer TOOLTIP_POINTER_LUMBER        = 11
    endglobals

    //*  Structure:
    //*  ==========
    //*  The system loops over all pointers, so please define,
    //*  which pointer is the first and which is the last.
    globals
        private constant integer FIRST_POINTER         = 1
        private constant integer LAST_POINTER          = 11
    endglobals
    
    //*  CustomizePointers:
    //*  ==================
    //*  Here you can define, if a healine should be added between to pointers.
    //* Furthermore you can set if no space should be added between to pointers.
    //* Only applies if the tooltip has a value for the specified pointer.
    globals
        private string  array headline //*  By default no headline text is added between to pointers.
        private boolean array nospace  //*  By default the system adds a space between to pointers.
    endglobals
    
    private function CustomizePointers takes nothing returns nothing
        set nospace[TOOLTIP_POINTER_PRIMARY_AFFIX] = true//*  In the example no space between primary affix and affixes is added.
        set headline[TOOLTIP_POINTER_PRIMARY_AFFIX] = "Primary:"
        set headline[TOOLTIP_POINTER_AFFIXES] = "Secondary:"
        //*  ....
    endfunction
    
    //*  Here starts the CustomItemTooltip code.
    //*  =======================================
    globals
        private constant hashtable TABLE   = InitHashtable()
        private constant integer   OFFSET = 0x64   //* Number of high value. Available rows per pointer.
        private constant integer   ICONS  = 0x186A0//* Number of higher value. Plus one dimension for icons.
    endglobals
    
    //*  API:
    //*  ====
    //*
    //*  Get:
    //* The total amount of lines.
    function GetItemTooltipSize takes integer itemIndex returns integer
        return LoadInteger(TABLE, 0, itemIndex)
    endfunction
    //* A single line.
    function GetItemTooltipFragment takes integer itemIndex, integer pos returns string
        return LoadStr(TABLE, itemIndex, pos)
    endfunction
    //* A Icon for the specified line.
    function GetItemTooltipFragmentIcon takes integer itemIndex, integer pos returns string
        return LoadStr(TABLE, itemIndex, pos + ICONS)
    endfunction
    //* Pointer used in the line.
    function GetItemTooltipFragmentPointer takes integer itemIndex, integer pos returns integer
        return LoadInteger(TABLE, itemIndex, pos + ICONS)
    endfunction
    function GetItemTooltipPointerSize takes integer itemIndex, integer pointer returns integer
        return LoadInteger(TABLE, itemIndex, pointer)
    endfunction
    function GetItemTooltipMaxWidth takes integer itemIndex returns real
        return LoadReal(TABLE, 0, itemIndex)
    endfunction
    //*
    //*  Set:
    //* A text in a line. Uses pointers.
    function SetItemTooltipFragment takes integer itemIndex, integer pos, string text, string icon, integer pointer returns nothing
        local integer size = IMaxBJ(LoadInteger(TABLE, itemIndex, pointer), pos + 1)
        call SaveStr(TABLE, itemIndex, pos + pointer*OFFSET, text) 
        call SaveStr(TABLE, itemIndex, pos + pointer*OFFSET + ICONS, icon) 
        call SaveInteger(TABLE, itemIndex, pointer, size)
    endfunction
    //* Word wrap a whole text block to a tooltip.
    function WordWrapStringToItemTooltip takes integer itemIndex, string text, real stringWidth, integer pointer returns nothing
        static if LIBRARY_WordWrap then
            local integer index = 0
            call WordWrapString(text, stringWidth, true)
            loop
                exitwhen index == GetWrappedStringCount()
                call SetItemTooltipFragment(itemIndex, index, GetWrappedStringFragment(index), null, pointer)
                set index = index + 1
            endloop
        endif
    endfunction
    //*
    //*  Remove:
    //* A single line of a specific pointer
    function RemoveItemTooltipFragment takes integer itemIndex, integer pos, integer pointer returns nothing 
        call RemoveSavedString(TABLE, itemIndex, pos + pointer*OFFSET)
        call RemoveSavedString(TABLE, itemIndex, pos + pointer*OFFSET + ICONS)
    endfunction
    //* All lines of a specific pointer.
    function FlushItemTooltipPointer takes integer itemIndex, integer pointer returns nothing
        local integer index = 0
        local integer size  = LoadInteger(TABLE, itemIndex, pointer)
        loop
            exitwhen (index == size)
            call RemoveSavedString(TABLE, itemIndex, index + pointer*OFFSET)
            call RemoveSavedString(TABLE, itemIndex, index + pointer*OFFSET + ICONS)
            set index = index + 1
        endloop
        call RemoveSavedInteger(TABLE, itemIndex, pointer)
    endfunction
    //*
    //*  Flush:
    //* A complete tooltip
    function FlushItemTooltip takes integer itemIndex returns nothing
        call RemoveSavedInteger(TABLE, 0, itemIndex)
        call FlushChildHashtable(TABLE, itemIndex)
    endfunction
    //*
    //* Clear: Not as radical as flush.
    private function ClearTooltip takes integer itemIndex returns nothing
        local integer size  = LoadInteger(TABLE, 0, itemIndex)
        local integer index = 0
        loop
            exitwhen index == size
            call RemoveSavedString(TABLE, itemIndex, index) 
            call RemoveSavedString(TABLE, itemIndex, index + ICONS)
            call RemoveSavedInteger(TABLE, itemIndex, index + ICONS) 
            set index = index + 1
        endloop
    endfunction
    
    //*  Average char length without library StringSize is set to 13.
    private function Concatenate takes integer itemIndex returns nothing
        local integer pointer = FIRST_POINTER
        local integer size    = 0
        local integer line    
        local integer lines   
        local boolean entry  
        local string  text 
        local real    length  = 0.
        
        //*  Let's start!
        loop
            exitwhen (pointer > LAST_POINTER)
            
            //*  Strings per pointer.
            set line  = 0
            set lines = LoadInteger(TABLE, itemIndex, pointer)
            set entry = (lines > 0)
    
            //*  Headline.
            if (entry) and (headline[pointer] != null) then
                set text = headline[pointer]
                static if LIBRARY_StringSize then
                    set length = RMaxBJ(MeasureString(text), length)
                else
                    set length = RMaxBJ(StringLength(text)*13, length)
                endif
                call SaveStr(TABLE, itemIndex, size, text)
                call SaveInteger(TABLE, itemIndex, size + ICONS, 0)
                set size = size + 1
            endif
            
            //*  Tooltip block of "pointer".
            loop
                exitwhen (line == lines)
                if HaveSavedString(TABLE, itemIndex, line + pointer*OFFSET) then
            
                    //*  Text and icon.
                    set text = LoadStr(TABLE, itemIndex, line + pointer*OFFSET)
                    static if LIBRARY_StringSize then
                        set length = RMaxBJ(MeasureString(text), length)
                    else
                        set length = RMaxBJ(StringLength(text)*13, length)
                    endif
                    call SaveStr(TABLE, itemIndex, size, text)
                    call SaveStr(TABLE, itemIndex, size + ICONS, LoadStr(TABLE, itemIndex, line + pointer*OFFSET + ICONS))
                    
                    //*  Which type of pointer.
                    call SaveInteger(TABLE, itemIndex, size + ICONS, pointer)
                    set size = size + 1
                else
                    //*  Null string is ignored.
                endif
                set line = line + 1
            endloop
            if (entry) and not (pointer == LAST_POINTER) then
                
                //*  Space?
                if not (nospace[pointer]) then
                    call SaveStr(TABLE, itemIndex, size, " ")
                    call SaveInteger(TABLE, itemIndex, size + ICONS, 0)
                    set size  = size + 1
                endif
            endif
            set pointer = pointer + 1
        endloop
        
        //*  Total string size:
        call SaveInteger(TABLE, 0, itemIndex, size)
        call SaveReal(TABLE,    0, itemIndex, length)
    endfunction
    
    function GenerateItemTooltip takes integer itemIndex returns nothing
        call ClearTooltip(itemIndex)
        call Concatenate(itemIndex)
    endfunction
    
    //*  Seriously?
    private module Inits 
        private static method onInit takes nothing returns nothing
            static if LIBRARY_ErrorMessage then
                debug call ThrowError(FIRST_POINTER <= 0,           "CustomItemTooltip", "Init", "FIRST_POINTER", FIRST_POINTER, "private constant integer FIRST_POINTER has to be bigger than 0.")
                debug call ThrowError(LAST_POINTER < FIRST_POINTER, "CustomItemTooltip", "Init", "LAST_POINTER",  LAST_POINTER,  "private constant integer LAST_POINTER has to be bigger than FIRST_POINTER.")
            endif
            call CustomizePointers()
        endmethod
    endmodule
    private struct I extends array
        implement Inits
    endstruct
    
endlibrary

Example: Code
JASS:
        //*  Start writing a tooltip where and when-ever you want.
        //*
        //*  Function arguments:
        //*  ===================
        //*  "item index" - can be item type id or any index which point to the item. handle id?
        //*  "line"       - which line within this string stack. ( one stack per pointer )
        //*  "text"       - your tooltip-
        //*  "file path"  - add an icon to the text, multiboard friendly.
        //*  "pointer"    - which part of the total tooltip
        
        //*  Example:
        //*  ========
        call SetItemTooltipFragment('srbd',     0, "affix 0", "UI\\Minimap\\MinimapIconCreepLoc2.blp",         TOOLTIP_POINTER_PRIMARY_AFFIX)
        call SetItemTooltipFragment('srbd',     1, "affix 1", "UI\\Minimap\\MinimapIconCreepLoc2.blp",         TOOLTIP_POINTER_AFFIXES)
        call SetItemTooltipFragment('srbd',     2, "affix 2", "UI\\Minimap\\MinimapIconCreepLoc2.blp",         TOOLTIP_POINTER_AFFIXES)
        call SetItemTooltipFragment('srbd',     3, "affix 3", "UI\\Minimap\\MinimapIconCreepLoc2.blp",         TOOLTIP_POINTER_AFFIXES)
        call SetItemTooltipFragment('srbd',     4, "affix 4", "UI\\Minimap\\MinimapIconCreepLoc2.blp",         TOOLTIP_POINTER_AFFIXES)
        call SetItemTooltipFragment('srbd',     5, "affix 5", "UI\\Minimap\\MinimapIconCreepLoc2.blp",         TOOLTIP_POINTER_AFFIXES)
        call SetItemTooltipFragment('srbd',     0, "Meele characters only!", null,                             TOOLTIP_POINTER_REQUIREMENTS)
        
        //*  WrapWrap helps us to transform long strings into a tooltip. ( 400. is the max string lenght per line )
        call WordWrapStringToItemTooltip('srbd', "Twohand sword made from steel. More text to see word wrap in action", 400. , TOOLTIP_POINTER_STORY)
        
        //*  Generate the tooltip
        call GenerateItemTooltip('srbd')
        
        //*  Change a line whenever you want. ( line 3 of affixes gets a new text )
        call SetItemTooltipFragment('srbd',     3, "affix 3 ( changed )", "UI\\Minimap\\MinimapIconCreepLoc2.blp", TOOLTIP_POINTER_AFFIXES)
        
        //*  Remove a single line. ( Here line 5 of the affixes )
        call RemoveItemTooltipFragment('srbd', 5, TOOLTIP_POINTER_AFFIXES)
        
        //*  Add a new line.
        call SetItemTooltipFragment('srbd',    6, "affix 6", "UI\\Minimap\\MinimapIconCreepLoc2.blp",         TOOLTIP_POINTER_AFFIXES)
        
        //*  Or remove a whole block. ( requirements completly removed from the text )
        call FlushItemTooltipPointer('srbd', TOOLTIP_POINTER_REQUIREMENTS)
        call GenerateItemTooltip('srbd')
        
        //*  2D Dimension to create a tooltip box ( multiboard or image ):
        
        //*  Get the total amount of lines.
        call BJDebugMsg(I2S(GetItemTooltipSize('srbd')))
        
        //*  Get the maximum width of all lines.
        call BJDebugMsg(R2S(GetItemTooltipMaxWidth('srbd')))

Ingame screenshot for this example:
 

Attachments

  • Tooltip test.jpg
    Tooltip test.jpg
    346 KB · Views: 114
Level 19
Joined
Mar 18, 2012
Messages
1,716
Have you checked how well the double click works as soon as multiplayer latency comes into play? I could imagine this causing problems.

Sadly, I didn't run any multiplayer tests yet.

Yes it should work in multiplayer.

Edit: I uploaded the latest version to the thread.
I will not have time in the near future to work on it.
There is still a lot to do inside the UIPackage. :/

An idea, which I didn't start to code:

A tooltip box:
- You can create the border with UIBorder.
- The tooltip text should be created via textsplats not texttags.
 
Last edited:
Level 19
Joined
Mar 18, 2012
Messages
1,716
After I saw that the map was downloaded 27 times. I picked up work again on the UIPackage.

I re-wrote a lot of things ( still testing ) and wrote a small test code to show/test the API:

JASS:
library UIDemo initializer Init uses UIPackage

    //*  Main screen. Always extend from UIScreen.
    struct DemoScreen extends UIScreen
    
        //*  stub method onShow from UIScreen.
        private method onShow takes boolean flag returns nothing
            if flag then
                call BJDebugMsg("screen visible")
            else
                call BJDebugMsg("screen hidden")
            endif
        endmethod
    
    endstruct

    //*  Window within the screen. Always extend from UIWindow.
    struct DemoWindow extends UIWindow
    
        //*  stub method onHover from UIWindow.
        private method onHover takes DemoScreen screen, UIButton hovered returns nothing
            call BJDebugMsg("Data on this button is " + I2S(hovered.data)) 
        endmethod    
    
        //*  stub method onClick from UIWindow.
        private method onClick takes DemoScreen screen, UIButton clicked returns nothing
            if (clicked.data == 1) then//*  Close button.
                call screen.show(false)
            endif
            //*  UISound API.
            call audio.bigButtonClick()
        endmethod
    
        //*  Disclaimer:
        //*  ===========
        //*  JassHelper limitation. Can't execute onCreate from static method create.
        //* We must do it by ourself. I did it in function InitDemo.
        static method onCreate takes thistype this returns nothing
            local string s          = "war3mapImported\\64x64Track.mdx" 
            local UIButton b1
            local UIButton b2
            
            //*  Add buttons to the window.
            //* Arguments:
            //* ==========
            //* s - Trackable file
            //* x, y, z, face, objectId, scale
            //*
            //* x and y are relative to the window sizes.
            //* Example: x = 128 means window.originX + 128
            //*          y = 128 means window.originY + 128
            
            //*    |
            //*    |
            //* 128|  b1    b2
            //*    |_________
            //*  0/0 128   256
            set b1 = addDestButton(s, 128, 128, 0., 270., 'cwcc', 1.)
            set b2 = addDestButton(s, 256, 128, 0., 270., 'cwdp', 1.)
            
            //*  Buttons can have data. You can read the data when they are hovered or clicked.
            set b1.data = 1
            set b2.data = 'A00A'
            
            //*  Draw a border around the window. "1." is the border scaling.
            call frame(1.)
            
            //*  Alternative:
            //*  ============
            //*  Add custom borders whereever you want.
            //*  Again relative to the windows originX/originY
            //call addBorder(0., 0., 130.,  64.,  .65) 
            //*  Or single images:
            //call addBorderImage(...)
        endmethod
    
    endstruct
    
    
    globals
        private DemoScreen demo = 0
    endglobals
    
    private function InitDemo takes nothing returns nothing
        local UIWindow window 
        //*  Create a new UIScreen. The best width, height ratio is ~ 1.8
        set demo   = DemoScreen.create(Player(0), -2000, -2000, 1632, 906)
        
        //*  Add a window to the screen.
        set window = DemoWindow.create(demo, 100, 100, 384, 256)
        
        //*  JassHelper limitation. We have to add the window by ourself.
        call demo.addWindowOfType(window.getType(), window, true)
        
        //*  Another JassHelper limitation. Can't execute onCreate....
        //* We can do that also by ourself
        call DemoWindow.onCreate(window)
        
        //*  In debug you can draw a lightings to see everything looks good.
        debug call demo.highlight()
        debug call window.highlight()
    endfunction
    
    //*  We show the UI via Esc event:
    //*  =============================
    private function OnEsc takes nothing returns nothing
        call demo.show(not demo.enabled)
    endfunction
    
    private function Init takes nothing returns nothing
        local trigger t = CreateTrigger()
        call TriggerRegisterPlayerEventEndCinematic(t, Player(0))
        call TriggerAddAction(t, function OnEsc)
        call InitDemo()
    endfunction
    
endlibrary
 

Attachments

  • Test.jpg
    Test.jpg
    431 KB · Views: 104
Level 19
Joined
Mar 18, 2012
Messages
1,716
I've run a few multiplayer emulations via JNGP 2.0 and didn't run into serious issues.

I've updated the attached map. It's packed with a lot of stuff, because I tested:
- BonusMod
- ItemPower
- ItemSet
- TextSplats
- ....

Edit: I moved it to the Spell Section,
as I think everything is working. I only need feedback, so I can make further changes and improvements
 
Last edited:
Level 19
Joined
Mar 18, 2012
Messages
1,716
I would need help with properly adjusting the camera bounds.

Currently the camera works with a dummy unit, which is very bad,
because it limits the usage of custom UI. For example you can't move
around the screen, hence can't build a nice world map or skill tree.

Better would be to adjust the camerabounds to the size of the window.

If you set camerabounds to a rect --> originX, originY originX + width, originY + height,
you gain some extra to left, right, top and bottom.

So I'm searching for an algorithm
to set the camera bounds excacly to my desired rect.

Edit: solved.
 
Last edited:
Level 19
Joined
Mar 18, 2012
Messages
1,716
Yes... At first I thought I found a good solution, but it seems to be not possible
to adjust camera bounds they way I wanted.

You can set the camera bounds to a spot in the map and play with the camera distance,
to gain more or less view over a rect.

But you can't set it to a rect, because then you'll get some extra mobility
over the bounds of that rect into all directions.

It limits custom UIs a bit, as you can't build one which is bigger than the initial screen.
In other words: you can't move your field of view by moving the mouse.
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
Isnt that amazing :D

I also started writing some camera struct with some easy implementation that would allow this to work.

But I actually dont believe you haven't tried "SetCameraBounds(x-100, y, x-100, y, x+100, y, x+100, y)"
That would allow you to move your camera sideways 100 pixels in horizontal direction from (x, y).
 
Status
Not open for further replies.
Top