CitiesSkylinesMods / TMPE

Cities: Skylines Traffic Manager: President Edition
https://steamcommunity.com/sharedfiles/filedetails/?id=1637663252
MIT License
571 stars 85 forks source link

UI: Replace floating toolbar with 'build bar' #51

Closed originalfoo closed 4 years ago

originalfoo commented 5 years ago

Note: The 'TMPE crown' button would still exist and be draggable to any location on screen

Currently TMPE tools are accessed by a floating toolbar which is toggled by the 'crown' button.

I propose moving the tools in to a 'build bar' (like "Find It" or "Roads" etc):

What do you think?

I've also asked Keallu, author of "Resize It!" mod, if he would consider creating reference mod that shows the ideal way to implement 'build bars' (as he did extensive investigations in to those while developing his Resize It! mod).

originalfoo commented 5 years ago

With the Overlays tab, we could remove the Overlays screen from mod options.

We could have a Policies tab too, allowing another screen to be removed from mod options.

With tool-specific buttons, we could make it easy to toggle, for example, lane highlighting (#33).

Some floating panels (I'm thinking lane arrows and speeds) could potentially be replaced by build bars. User selects the tool, build bar changes to a hidden tab containing the options for that tool, user selects speed or lane arrow and then starts clicking roads to apply it.

When setting speeds, the "set speeds for individual lanes" toggle becomes a button at the side of toolbar.

The "Straight ahead" feature of lane connections/arrows would just be an icon - select it then click any junction you want to be "straight ahead".

originalfoo commented 5 years ago

Keallu recommends referring to mods such as Surface Painter or Extra Landscaping Tools for correct (vanilla) way to do 'build panels'.

FireController1847 commented 5 years ago

Here's an idea, what about putting it in the roads menu but with its own tab? The tab icon would be the TMPE crown. Each tool could have a dedicated icon which we can click to use. What do you think?

krzychu124 commented 5 years ago

That's the 'build bar' IIRC 😉

FireController1847 commented 5 years ago

Ah, I see. I think I may have been confused, as you guys were referencing things like Surface Painter, which contains its own tab on the entire bar at the bottom (like FindIt), where as I was referencing a tab within the roads menu.

krzychu124 commented 5 years ago

I think the only difference is where you you want put that button

We will see which is better, I didn't decide yet

originalfoo commented 5 years ago

There's already loads of tabs on the road menu, and also TMPE isn't just about roads - there's features that apply to rail-based networks too. That's why I think it needs a dedicated tab.

originalfoo commented 5 years ago

Also, for TMPE button, I was thinking of something like this (note: $2 commercial image):

light

FireController1847 commented 5 years ago

@aubergine10 Can I have a link to that icon?

FireController1847 commented 5 years ago

Hey, I found it! And it's on a website that I have $5 credit in. [Previously I posted the image.] Who am I kidding? It's licensed. I can't do that. Let me know when you guys are ready for it and I'll submit a PR in the appropriate place. I own the icon now so don't buy it, don't waste your money. I already had $5 in credit for that website.

originalfoo commented 5 years ago

What formats did you get it in? I think PNG version will be enough to work with. Probably just a rescale. I was also thinking it could be used as the mod logo too? It would really stand out in the crowd of other mod logos in workshop :)

FireController1847 commented 5 years ago

@aubergine10 I'm making a Discord server with it right now. I can send you all the files available if you'd like.

https://discord.gg/faKUnST

originalfoo commented 5 years ago

I've started a repo for making a UI mockup of the build bar: https://github.com/aubergine10/TMPE-UI

Still very early stages as I have literally no idea what I'm doing.

Plan is to get a build bar up and running as separate mod, so I can quickly iterate icons and stuff like that, but also allow end-users to play with it and maybe give some feedback.

It won't actually do anything, so it might serve as a useful template for other modders keen to implement build bars in their mods. Keallu, author of Resize It mod, has said he will lend a hand to ensure it's compatible with his mods.

My first task is to get a draggable button added that will later be used to toggle build bar. If anyone wants to help with that, see this topic in simtropolis: https://community.simtropolis.com/forums/topic/757968-how-to-add-ui-button/

krzychu124 commented 5 years ago

Nice idea I like it.

With regards to draggable button... In TM:PE Main menu button is draggable 😄 You can look how it's made 😉

FireController1847 commented 5 years ago

If you do start it, maybe you could build off of the idea I started in the emergency vehicle one? Basically it's an idea of a "extension" to TM:PE. View the codebase to see how this "extension" "extends" TM:PE.

originalfoo commented 5 years ago

I want to keep it separate from TMPE for now, so I can just focus on the basic UI stuff.


I'm currently working on making the button draggable, via right-click+drag. This will allow us to ditch the Lock main menu button mod option, because the button won't be accidentally draggable any more :)

I then plan on adding the following features:

originalfoo commented 5 years ago

Anyone know how to detect if a UI component (like UIButton) is over a certain panel (like "TSBar")?

originalfoo commented 5 years ago

Progress update...

Work continues on splitting the sprite atlas for custom button class. CO made several methods private / internal which has somewhat thrown a spanner in the works, but think I can work round it (just means a bunch of stuff from UIButton has to be cloned in to the new class).

There's some issues with sprite sizes, in that the foreground sprite will often be smaller (36x36) than the background sprites. I thought simply having two atlases would work round this, but on digging through UIButton code with ILSpy it seems everything pulls from the size property.

I'm considering an alternative of just adding a new child sprite to the button. It shouldn't add to the overhead of rendering the button much (as the fg sprites of the main button will all be null and thus won't get rendered; I could even re-route those properties to the child sprite).

Still no idea how to detect which panel a button is over, but that can wait for now.

The right-click to drag thing works great, and I've tweaked the interaction model to avoid spammy updates while the button is dragged. I'll be adding a few additional bits to facilitate easier connection to config updates (so when button is dropped, the outer code gets notified and can store relevant info in xml or whatever).

krzychu124 commented 5 years ago

With regards to detecting button over panel you can try comparing button absolutePosition + size (Rect) and check if it intersects with any UIComponent absolutePosition + size (Rect).

I have no idea why you need this but here is my quick ugly code to check which component is hovered by mouse (definitely not usable in Update()). (cut from ModTools (Ctrl + R - debug rendering)). Maybe you will find something useful here.

Attach to something... AnyGuiComponent.gameObject.AddComponent<Test>();// button or something different (buttonInstance.GetComponent<Test>().enable = true/false) -> calls OnEnable()/OnDisable() MonoBehaviour methods

class Test: MonoBehaviour{
// put this to any method ---------- ... A lot of to choose from MonoBehaviour class -----------
UIView uiView = Object.FindObjectOfType<UIView>(); // retrieves UIView
if (uiView != null) {
    components = base.GetComponentsInChildren<UIComponent>();
    Array.Sort<UIComponent>(components, ((component1, component2) => component1.renderOrder.CompareTo(component2.renderOrder) ));
    Vector3 mousePosition = Input.mousePosition;
    mousePosition.y = Screen.height - mousePosition.y;
    for (int i = 0; i < components.Length; i++) {
        if (!components[i].isVisible || components[i].name == "FullScreenContainer" || components[i].name == "PauseOutline") {
          continue; 
        }
        Vector3 absolutePosition = components[i].absolutePosition;
        Vector2 size = components[i].size;
        if (!CalcComponentRect(absolutePosition, size).Contains(mousePosition)) continue;
        LogHover(components[i]); // Simple method for getting name -> void LogHover(UIComponent c) { Log.Info("[Hover above]: " + c.name); }
        break;
    }
}
// ---------------------------------------
private Rect CalcComponentRect(Vector3 absolutePosition, Vector2 size)
{
    float num = (float)Screen.width / 1920f; // don't know why those numbers but works on low screen res
    float num2 = (float)Screen.height / 1080f;
    absolutePosition.x *= num;
    absolutePosition.y *= num2;
    size.x *= num;
    size.y *= num2;
    return new Rect(absolutePosition.x, absolutePosition.y, size.x, size.y);
}

Log:

[Hover above]: Roads
[Hover above]: MainToolstrip
[Hover above]: TSBar
[Hover above]: Roads

Look inside MonoBehaviour class, it's native Unity class for creating Unity scripts attached to objects. There are tons of events to subscribe and even more useful methods to override.

originalfoo commented 5 years ago

After some digging, I think best approach might be to use Rect and RectTransform, for example:

public static class RectTransformExtensions
{

    public static bool Overlaps(this RectTransform a, RectTransform b) {
        return a.WorldRect().Overlaps(b.WorldRect());
    }
    public static bool Overlaps(this RectTransform a, RectTransform b, bool allowInverse) {
        return a.WorldRect().Overlaps(b.WorldRect(), allowInverse);
    }

    public static Rect WorldRect(this RectTransform rectTransform) {
        Vector2 sizeDelta = rectTransform.sizeDelta;
        float rectTransformWidth = sizeDelta.x * rectTransform.lossyScale.x;
        float rectTransformHeight = sizeDelta.y * rectTransform.lossyScale.y;

        Vector3 position = rectTransform.position;
        return new Rect(position.x - rectTransformWidth / 2f, position.y - rectTransformHeight / 2f, rectTransformWidth, rectTransformHeight);
    }
}

I can take advantage of the fact that I know in advance which panels I'm interested in and they should always be there. I can cache the worldrects of the panels when button starts to be dragged, then check .y to quickly determine which panel (if any) I'm likely over, then do a proper overlap check to make sure (eg. to accommodate people with multiple monitors that use mods to make panels only appear on one screen). If I'm in TSBar I can do additional check to see if I'm in MainToolStrip.

In other news, I think I might also have a solution for different sprite sizes - seems a UITextureAtlas can contain sprites of different sizes, although I've yet to determine how that works. I can set the foreground sprite to scale to fit the background sprite if my comprehension of UIInteractiveComponent.foregroundSpriteMode is correct.

originalfoo commented 5 years ago

Is there a way to wait until the entire game UI has rendered before doing something?

In order to place button in relevant place (eg. anchored to a vanilla game UI element) that element has to exist. Yet when the Start() method gets called on my element it seems it's not there yet.

I've seen other mods using Update() method for that stuff but it seems super crufty way of doing things just for initialisation.

krzychu124 commented 5 years ago

You can use Awake(), OnEnabled() - one time methods from MonoBehaviour or OnLevelLoaded() from LoadingExtensionBase

You can even try SimulationStep() (MonoBehaviour) first frame hack like we have inside TM:PE :wink:

originalfoo commented 5 years ago

I'm adding the button to the scene during OnLevelLoaded(), then shortly after I see Start() being called. I'll give Awake() and OnEnabled() a try.

I assume the SimulationStep() is equivalent to using Update()? Do you have link to the code in TMPE that uses that first frame hack? Specifically, is it possible to make it only run on first frame and not all subsequent frames (like, completely remove the method/listener)?

EDIT: I tried looking at https://github.com/krzychu124/Cities-Skylines-Traffic-Manager-President-Edition/blob/master/TLM/TLM/LoadingExtension.cs and omg wtf is that mess? Can't see the wood for the trees! +1 for Harmony

krzychu124 commented 5 years ago

https://github.com/krzychu124/Cities-Skylines-Traffic-Manager-President-Edition/blob/0e25ec053b3831f76a705117234935db39fec97b/TLM/TLM/ThreadingExtension.cs#L45-L50

krzychu124 commented 5 years ago

Check vanilla LoadingWrapper LoadingManager too. It has some public events to subscribe.

You can add your script to UIView too. - global container for all UiComponents

[Edit3] Haha, there is OnGui() too... Oh man so many possibilities

originalfoo commented 5 years ago

Thanks, I'll try those.

I've been doing some testing with triggering the code on button click just to sort out some other issues and I'm getting some real strange stuff happening....

    public class UIChameleonButton : UIButton
    {
        // supported screen regions
        public enum UIScreenRegion { None, Floating, InfoPanel, TSBar, MainToolStrip, ThumbnailBar };

        // internal store of current screen region
        protected UIScreenRegion m_ScreenRegion = UIScreenRegion.None;

On click....

        protected override void OnClick(UIMouseEventParameter p)
        {
            if (p.buttons.IsFlagSet(UIMouseButton.Left))
            {
                Debug.Log("TMPEBB - Chameleon button - left click");
                ScreenRegion = UIScreenRegion.InfoPanel;
                base.OnClick(p);
            }
        }

getter/setter:

        // get/set screen region
        public UIScreenRegion ScreenRegion
        {
            get => m_ScreenRegion;
            set
            {
                Debug.Log("TMPEBB - Chameleon button - screen region setter");
                if (value != m_ScreenRegion)
                {
                    Debug.Log("TMPEBB - Chameleon button - screen region has changed");
                    m_ScreenRegion = value;
                    OnScreenRegionChange();
                }
            }
        }

On screen region change...

        // triggered when screen region changes, assimilages button design to region
        protected void OnScreenRegionChange()
        {
            Debug.Log("TMPEBB - Chameleon button on region change");
            switch (m_ScreenRegion)
            {
                case UIScreenRegion.None:
                case UIScreenRegion.Floating:
                    Debug.Log("TMPEBB - Chameleon button - floating");
                    AssimilateButton("Info"); // info views toggle button
                    break;
                case UIScreenRegion.ThumbnailBar:
                case UIScreenRegion.InfoPanel:
                    Debug.Log("TMPEBB - Chameleon button - thumbnail");
                    // no background
                    size = new Vector2(36, 36); // icon.size
                    break;
                case UIScreenRegion.TSBar:
                case UIScreenRegion.MainToolStrip:
                    Debug.Log("TMPEBB - Chameleon button - tsbar");
                    AssimilateButton("Roads"); // roads menu toggle button
                    break;
            }
            Invalidate();
        }

Log file....

TMPEBB - Chameleon button - left click

TMPEBB - Chameleon button - screen region setter

TMPEBB - Chameleon button - screen region has changed

TMPEBB - Chameleon button on region change

TMPEBB - Chameleon button - thumbnail

wtf is going on there? That last log should be ... - tsbar.

krzychu124 commented 5 years ago

You've set ScreenRegion = UIScreenRegion.InfoPanel so switch case working perfectly fine ;) Don't forget about breaks because if e.g. set to None it will fall through to Floating.

To get TsBar you have to set to UIScreenRegion.TsBar or UIScreenRegion.MainToolStrip because you don't have break in between :)

originalfoo commented 5 years ago

*hangs head in shame*

How on earth did I not spot that :(

originalfoo commented 5 years ago

omg I'm so embarrassed right now

originalfoo commented 5 years ago

PEBKAC *sigh*

originalfoo commented 5 years ago

I have failed this button.

originalfoo commented 5 years ago

It lives! Hopefully this will illustrate what all the faff has been about - user drags button to where they want, and it adapts to the button style of that location :)

floating

tsbar

There's some minor scaling/sizing/aspect issues that I need to deal with, but it's getting there :)

krzychu124 commented 5 years ago

Now I see what you wanted to do 😄

Can you snap it where help button is placed?

originalfoo commented 5 years ago

Yes, that's ThumbnailBar.

What do you think button background should be in that region? Same as help (adviser) button, or no background? I can do either.

Also, I'm working on "sticky drag" - when being dragged over one of those panels at bottom of screen, and also a 'virtual panel' at top of screen, the button will be 'magnetically' constrained to the panel (you have to drag further away to detach it). So it will make placement easier as it will auto-align with other things on that panel.

The way CO (possibly derived from Unity) have implemented the UI stuff is an utter nightmare. It's like throwback to the 80's, eerily reminiscent of IE5 and 6 html layout quirks.

originalfoo commented 5 years ago

Turns out the buttons on the ThumbnailBar are a freaking mess. They use a class UIMultiStateButton which is a complete and utter mess, full of circular references and all kinds of other cruft. Normal UIButton class is crufty, but this UIMultiStateButton takes things to a whole new level of unnecessary complexity.

Despite getting names of the sprites, and checking they are in the same atlas as all the others, I've so far been unable to assimilate them in the chameleon button. So, IMO, for that particular bar we either drop the background sprites or use something else. Closest design I see to the help button is the buttons at top of screen (eg. toggle info views button) - while they are much bigger, simply setting the size of the chameleon button will cause them to resize so they'd look same size and very closely match the design of the help button. What do you think?

krzychu124 commented 5 years ago

Good idea with that toggle info views button.

originalfoo commented 5 years ago

:)

tthumb

originalfoo commented 5 years ago

Managed to get exact style match :D better

krzychu124 commented 5 years ago

Do you know how to create scrollable container? I have everything besides that for incompatible mods checking... 😭

originalfoo commented 5 years ago

This is by far the nicest scroll panel I've seen so far in any mod: https://github.com/keallu/CSL-WatchIt/blob/master/WatchIt/LimitsPanel.cs

EDIT: On looking at code, it's not a scroll panel, but it should be sufficient to list our mods.

1643902284_preview_cap3

You could have columns for mod id and name.

originalfoo commented 4 years ago

Closing this as we decided to stay with existing TMPE toolbar (which also avoids myriad of issues associated with "toolbar extender/resizer" mods that alter vanilla toolbar in weird ways.