Jump to content

PopupDialog and the DialogGUI classes


Recommended Posts

Since this thread seems to be serving as the de-facto crowd-sourced info on DialogGUI, I thought I'd just post about something I discovered for editing the text in TextFields.

It is possible to change the text after the window has been spawned if you keep a reference to the DialogGUITextField object via code like this:

textFieldObj.uiItem.GetComponent<TMPro.TMP_InputField>().text = "Insert new text here";

I'm guessing there's ways of editing text of labels and images shown in other objects through similar components of the gameObjects these DialogGUI classes create.

Link to comment
Share on other sites

You can also use functions, to change a text in a text field:

DialogGUITextInput latField = new DialogGUITextInput("", false, 20, (string s) => { model.Latitude = s; return s; }, model.GetLatitude, TMPro.TMP_InputField.ContentType.DecimalNumber, CommonWindowProperties.buttonHeight); 

The same aplies for labels (in this case, it's through a new delegate, because I need to pass an index to the function):

new DialogGUILabel(delegate { return GetSpeed(index); }, 60f)

 

Edited by maja
Link to comment
Share on other sites

2 hours ago, DMagic said:

Keep in mind that updating the text, or any other element, probably won't update the layout.

So if you replace some short text field with something really long it might not come out right. 

If you leave one dimension free (e.g. vertical), then it will stretch the whole layout as needed. At least for labels in a GUIVerticalLayout it's true.

Edited by maja
Link to comment
Share on other sites

I can't solve UI Texture2D rescaling problem. I have an image glyph in PNG format that i want to load and then rescale to fit a specific constraints for plugin code to work on it.

Problem is that at every time i try, KSP (or to be more precise - Unity3D under the hood) refuses or outright quietly fails to resize a texture. Debug log either shows "can't resize a compressed texture" or texture dimensions drop to 0 (!?) after resize.

I've searched and found that apparently Texture2D.Resize() method does NOT rescale a texture, it only changes the Color[] array size.

If the above is true, this is ... ridiculous.

Anyway, here is a snippet of troublesome code:
 

Texture2D sourceTexture = GameDatabase.Instance.GetTexture(imagePath, false);
sourceTexture.Resize(10, 15, TextureFormat.DXT1, false); // trying to bypass compressed texture resize failure warning
sourceTexture.Resize(10, 15); // does not work for me
sourceTexture.Apply();

Note that, resizing dimensions 10 and 15 are purely for demonstration purposes.

Failing above, i could enforce failure if loaded PNG is not within constraints. Barring the Resize() problem, my code nicely loads and manipulates the PNG for display in UI, there are no problems for now.

Edited by fatcargo
Link to comment
Share on other sites

Found solution for this, though in hindsight it is not what i'm after. I'll have to enforce PNGs to have required width and height.

If anyone still wishes to do texture resizing, source is available in this stackexchange post . Note that is quite crude for larger scaling factors, but for minor pixel-by-pixel adjustments it works ok.

Edited by fatcargo
Link to comment
Share on other sites

On 11/10/2018 at 5:36 AM, fatcargo said:

I can't solve UI Texture2D rescaling problem. I have an image glyph in PNG format that i want to load and then rescale to fit a specific constraints for plugin code to work on it.

Don't forget that Unity doesn't show the texture2d, it shows the meshrenderer or canvasrenderer or whatever is being used.

Link to comment
Share on other sites

  • 2 weeks later...

Just as i considered texture rescaling to be resolved, i stumbled upon something interesting and it just bugs me.

I have made a function that loads image, then "stretches" it by creating a new texture of desired size, copies all four corners, then copies all borders between corners and finally fills in the center. Not the most elegant (or short) function, but it does the job.

Then, i tried the same for thumb button on DialogGUIScrollList and found that, depending on given content,  thumb size changes. This in turn deforms the loaded image. Ofcourse i wanted to use OnResize event, but that d*mn thing triggers even with plain scrolling user action. Too expensive, too bulky and will most likely attract a Kraken with that many memory reallocations / garbage collections.

So, i yet again i turned to KSP built-in style HighLogic.UISkin.verticalScrollbarThumb . I assigned it to DialogGUIScrollList UIStyle and lo and behold - it rescaled button with no extra code, all rounded corners perfectly preserved, it even has a simple gradient.

So, i tried to copy the entire UIStyle and after some tests found that this specific resizing type is built inside Sprite itself. I used HighLogic.UISkin.verticalScrollbarThumb.normal.background (which is a KSP built-in Sprite) in my own UIStyle and there was the smoking gun.

So what is bugging me now, is how i can replicate same behaviour in my custom Sprite. There are parameters for Sprite.Create() as mentioned on this Unity3D help page

public static Sprite Create(Texture2D texture, Rect rect, Vector2 pivot, float pixelsPerUnit, uint extrude, SpriteMeshType meshType, Vector4 border);

I'm out of my depth here. Are there any ides how to this ?

This stackoverflow post demonstrates pretty much exactly what i'm trying to do (sans putting the  texture in 3D space).

Another example is at Unity3D docs GUI Texture (Legacy UI Component)

Ahh one more thing : it appears to be referenced as "9-slice" UI image (found at this post from answers.unity.com, reading it now and trying that solution ! Will respond tomorrow).

Link to comment
Share on other sites

Ohhh i think i solved it ! When using 9-slice UI Sprite, few things need to be done :

1. Image to be loaded as texture needs to have small enough corners (usually rounded) so that rendering engine can use it on smaller elements (like the before mentioned scrollbar thumb)

2. For proper display image needs to have alpha channel, otherwise all pixels will fill up the UI element box and it won't look good

3. Border parameter needs to reflect actual size in pixels for borders (though you can put fractionals too, its a float so tweak it your liking)

4. Pixels Per Unit parameter works ok at value of 100, if lower the displayed image will degrade

Note that if corners needs to be shown with soft(er) edges, try adding/setting anti-aliasing in image processing software (also, the below code example does it's own smoothing which really isn't sufficient on it's own).

        public int pixelsPerUnit = 100; // best to keep it at 100

        public uint extrude = 0; //don't know what this does, 0 is ok

        //these are the magic stuff needed for 9-slice to work

        public float border_left = 8f; // choose how many pixels from image edge need to be allocated for border / corner

        public float border_bottom = 8f;

        public float border_right = 8f;

        public float border_top = 8f;

        public Sprite ImageSprite(string imagePath)
        {
            Texture2D imageTexture = GameDatabase.Instance.GetTexture(imagePath, false); // load image
            imageTexture.filterMode = FilterMode.Trilinear; // try smoothing, not overly spectacular
            imageTexture.wrapMode = TextureWrapMode.Clamp; // not sure this is required
            imageTexture.Apply();
            return Sprite.Create(
                imageTexture,
                new Rect(0, 0, imageTexture.width, imageTexture.height),
                new Vector2(0.5f, 0.5f), // pivot aka center of sprite
                pixelsPerUnit, extrude, SpriteMeshType.Tight, new Vector4(border_left, border_bottom, border_right, border_top) // important stuff
                );
        }

Ahh yes the SpriteMeshType.Tight tells what to do with empty space around texture, if using entire image this has no effect.

PS: As so many times before, i was actually fighting my own stupidity - while tinkering with input values for borders i forgot to properly transfer parameters and it ended up using (1,1,1,1), so i spent couple of days staring at the screen like a statue for two hours. :)

Edited by fatcargo
Link to comment
Share on other sites

I've run into another problem : i want to use DialogGUISlider as a jog-shuttle control. Why ? I need a precise control over a potentially large range of values within a single UI element.

The problem is that available functional callbacks for setValue and actionCallback do not provide required event handling, and i need events that fire when slider thumb is clicked-and-held-down and when it is released.

I've found almost a direct example in sources of BetterManeuvering and JanitorsCloset.

For start i need to know if i can attach a additional listener(s) directly to DialogGUISlider to capture events using DialogGUISlider.uiItem.gameObject.AddComponent and also what events i need to define/use to capture inputs.

I found two approaches :

- BetterManeuvering defines events it needs and hooks into PopupDialogController (its listener is a basic MonoBehaviour)

- JanitorsCloset completely replaces default event handlers with its own (its listener inherits from MonoBehaviour, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler, IPointerEnterHandler and IPointerExitHandler)

DebugStuff could find references to thumb and container components of DialogGUISlider inside PopDialog window, so it should be possible to intercept messages for those specific event targets.

I'll try to roll my own event installer/listener ofcourse but i'd like some advice on doing this properly.

EDIT : I was wrong to try using DialogGUISlider.uiItem, its null at runtime...

Edited by fatcargo
Link to comment
Share on other sites

@fatcargo Can this piece of code help you (it's field not slider, but as an example)? If I found the right Unity API definition, then you can hook to OnMouseDown, OnMouseDrag and OnMouseMove events.

DialogGUITextInput field;
field.OnUpdate = () => {
  if (field.uiItem != null)
  {
    field.OnUpdate = () => { };
    TMP_InputField TMPField = field.uiItem.GetComponent<TMP_InputField>();
    TMPField.onSelect.AddListener(TMPFieldOnSelect);
    TMPField.onDeselect.AddListener(TMPFieldOnDeselect);
  }
};

 

Link to comment
Share on other sites

@fatcargo You could attach your own MonoBehaviour to the slider thumb (if the DialogGUISlider class doesn't provide a direct reference to the thumb object then you can just get it yourself with GetComponentInChildren) that implements IPointerDownHanlder and IPointerUpHandler. Then do what you need in that script. I'm not sure if this would intercept the events from any existing handlers on that thumb, if it does you could use your MonoBehaviour to pass the events down to the other handlers.

Link to comment
Share on other sites

@maja : Thanks for the example code ! It helped me construct a working example. uItem field is indeed the right way to obtain handle to GameObject for DialogGUI element, i just used it wrong. NOTE :  uItem field appears to be valid only after PopupDialog.SpawnPopupDialog() is called. Kudos for clever way of disabling further calls to Update by assigning an empty code block. :)  However, i noticed that when calling PopupDialog.SpawnPopupDialog() multiple times, DialogGUISlider.Update() stays disabled (so for anyone trying this - if there is a problem with code not running correctly, this may be one of the reasons).

@DMagic : Thanks for the idea to call default events from my custom handlers, though i still have to check logic and flow of code.

Example code is working i just need to make a more robust solution. Namely, there will be multiple sliders and i need to know which one fired the event. PointerEventData contains source object fields that i can use, but not all fields are valid for all types of events, some of them are null (i've got a few NREs when experimenting). Once i clean the code i'll post what i did.

Link to comment
Share on other sites

Not-so-good-news-update : i made some more tests and used different approach. I want to make a function to create a jog-shuttle type of slider.  I need test how it performs when called multiple times because it uses some local variables (it should be ok), but i also wanted to bind output value of slider to a float variable passed by reference, so the whole thing stays neatly inside a single function.

Guess what ? "out" and "ref" variables can't be used inside anonymous functions, which is how all callbacks are implemented for slider events. :/

Sigh... what now ? I'll keep picking at this but feel free to chime in. If all else fails, after i post the example code, whoever wants to use it will have to use class-level variables in callbacks to get usable values ... yuck.

Read this stackoverflow post if insterested.

Link to comment
Share on other sites

3 hours ago, fatcargo said:

I need test how it performs when called multiple times because it uses some local variables (it should be ok), but i also wanted to bind output value of slider to a float variable passed by reference, so the whole thing stays neatly inside a single function.

Guess what ? "out" and "ref" variables can't be used inside anonymous functions, which is how all callbacks are implemented for slider events. :/

Something sounds funny in your explanation. If you want everything to stay in one function, what are you outputting to? Why does the float need to be passed by reference?  Changing it won't make any difference, you'll want to call a method on the slider itself to change it anyways or you might break state stuff.

Assuming "which" slider is the problem (and you REALLY wanted to keep it all in one function [why?]), you could always do something like this:

private enum WhichSlider // or however you want to identify it, right down to calling different functions
{
    First, Second, Third
}

// embed which slider is clicked into another anon function
private void Foo()
{ 
    DialogGUISlider slider1; // = whatever
    DialogGUISlider slider2;
    DialogGUISlider slider3;

    // can make an inner func for logic...
    Action<float, WhichSlider> lambdaSliderChanged = (f, which) =>
    {
        print("Slider " + which + " is now " + f);
    };

    slider1.slider.onValueChanged.AddListener(val => { lambdaSliderChanged(val, WhichSlider.First); });
    slider2.slider.onValueChanged.AddListener(val => { lambdaSliderChanged(val, WhichSlider.Second); });
    slider3.slider.onValueChanged.AddListener(val => { lambdaSliderChanged(val, WhichSlider.Third); });
}

 

Link to comment
Share on other sites

Here is the newest iteration, i adapted code from this post on unity answers site
 

        public DialogGUISlider CreateJogShuttle()
        {
            bool isDragging = false;
            float val = 0f;
            return new DialogGUISlider(
                setValue: () => { return isDragging ? val : 5f; }, // this one. can't set any ref or out parameters here
                min: 0f, max: 10f,
                wholeNumbers: true, width: 200, height: 10,
                setCallback: (float f) => { val = f; })
            {
                OnUpdate = () =>
                 {
                     //commented out for testing purposes
                     //test_slider.OnUpdate = () => { };

                     //longwinded but reliable, good enough for my tests
                     if (test_slider.slider.GetComponent<EventTrigger>()?.triggers.Exists(e => e.eventID == EventTriggerType.BeginDrag || e.eventID == EventTriggerType.EndDrag) ?? false) return;

                     EventTrigger sliderEvent = test_slider.slider.gameObject.AddComponent<EventTrigger>();

                     EventTrigger.Entry sliderOnEndDrag = new EventTrigger.Entry();
                     sliderOnEndDrag.eventID = EventTriggerType.EndDrag;
                     sliderOnEndDrag.callback.AddListener((e) => { Debug.Log("OnEndDrag\n"); isDragging = false; });
                     sliderEvent.triggers.Add(sliderOnEndDrag);

                     EventTrigger.Entry sliderOnBegindDrag = new EventTrigger.Entry();
                     sliderOnBegindDrag.eventID = EventTriggerType.BeginDrag;
                     sliderOnBegindDrag.callback.AddListener((e) => { Debug.Log("OnBeginDrag\n"); isDragging = true; });
                     sliderEvent.triggers.Add(sliderOnBegindDrag);
                 }
            };
        }

Note that "isDragging" is updated by two event handlers, so it properly resets slider thumb. Also i used "slider" field instead of "uItem".

@xEvilReeperx thanks for the example, i'll give it a try.

Link to comment
Share on other sites

You don't need ref or out here. Just use isDragging directly in the lambda functions; C# will create a closure over it and you effectively have a reference to it (although secretly the compiler creates a private inner class which is how it pulls this off).

By the way, you probably don't want to be inserting triggers in OnUpdate(): every time the slider value changes, you add another set of triggers

Link to comment
Share on other sites

While i was reading your code i realized that i already worked with lambdas in other part of my plugin. I tried too hard to make all-in-one solution for anyone to use, in my case i actually won't have problems to using refs inside anonymous functions.

As for adding more triggers/listeners, i added a if- getcomponent-return line to prevent just that :)

Link to comment
Share on other sites

  • 1 month later...

Does anybody have examples of how to create a non-modal dialog that stays more or less where you put it?  (That is, if you swap from one vessel to another, it should stay put.)  I've hacked something up, here:

https://github.com/SteveBenz/ColonizationByNerm/blob/30b389db8fa8bac559f2bdb3bebddcf088ed54cf/src/LifeSupportStatusMonitor.cs#L146

Which sorta does it.  If you know a thing or two about this stuff, I'd love it if you'd pop over to that code and leave comments to all the "I've got no idea what this does" and "shenanigans" comments...

If anybody can point me to non-stub documentation for PopupDialog.SpawnPopupDialog's arguments, that'd be great too.

Link to comment
Share on other sites

On 1/13/2019 at 7:29 AM, NermNermNerm said:

Does anybody have examples of how to create a non-modal dialog that stays more or less where you put it?  (That is, if you swap from one vessel to another, it should stay put.)  I've hacked something up, here:

https://github.com/SteveBenz/ColonizationByNerm/blob/30b389db8fa8bac559f2bdb3bebddcf088ed54cf/src/LifeSupportStatusMonitor.cs#L146

Which sorta does it.  If you know a thing or two about this stuff, I'd love it if you'd pop over to that code and leave comments to all the "I've got no idea what this does" and "shenanigans" comments...

If anybody can point me to non-stub documentation for PopupDialog.SpawnPopupDialog's arguments, that'd be great too.

Look here (Show and Dismiss): https://github.com/jarosm/KSP-BonVoyage/blob/master/BonVoyage/gui/MainWindowView.cs

You must save a window's position on dismiss and reopen it after a scene switch. When you are switching between vessel, which are in physics distance, then a dialog window stays opened.

Link to comment
Share on other sites

  • 2 weeks later...

I've made a window that has a label with multiline text and a toggle button that controls label's visibility of that label (via OptionEnabledCondition).

EXAMPLE SNIPPET
 

public bool showDescription = false;
public DialogGUILabel lab = new DialogGUILabel("example text for\nmultiple\nlines") { OptionEnabledCondition = () => { return showDescription; } };
public DialogGUIToggleButton toggle = new DialogGUIToggleButton(false, "show/hide", (b) => { showDescription = b; }, 100f, 20f);

When toggling label visibility, my popup window changes size as it should, but it also resets back to screen coordinates set at SpawnPopupDialog().

Can this be prevented ? I can manage repositioning the window to original coordinates, but is is way too visible and kind of ugly. For anyone interested, i used same EventTrigger() / AddListener() trick as with my jog-shuttle slider.

Oh, and does anyone know how to stop ESC from closing the popup window ? I assume it can be done by intercepting ESC keypress event game-wide, though i'd like to target event stream just for my popup window.

Edited by fatcargo
Link to comment
Share on other sites

@fatcargo I had a similar issue, when a window was centered regardless of of setting. The problem was, that there was an inner box which overflowed with its dimensions a minimal space between it and the window border, so KSP resized it, but also positioned the window to original it's location. Can this be also your case? I spent literaly two days hunting this error.

ESC closing all dialog windows and opening menu dialog is a default behaviour, so I just catch it and react to it to properly reset the state of my opened windows.

Link to comment
Share on other sites

Yes it is pretty much same issue - i switch visibility of label via toggle button and window gets reset to positions defined at spawn.

I did manage to set it back to last position prior to switching visibility, but you can still see for a brief moment how it jumps to old position then moves back.

I will do more tests to see if i can hide that visual "stutter".

Edited by fatcargo
Link to comment
Share on other sites

HA! I managed to make it clean, no more stutter when doing visibility toggle on UI element ! I'll post an example tomorrow, but for now a hint : use yourDialog.dialogToDisplay.OnResize() callback to set position with yourDialog.RTrf.position Vector3 variable.

It will nicely reposition the window BEFORE it gets rendered. Oh, i also added a bool refresh flag which is set to true at point where visibility toggle runs and then clear it in OnResize(). That way, callback won't cause runaway self-triggering (hmm i'll try without that refresh flag to see if it indeed self-triggers).

The only thing left (avoidable) is when hide/show happens, the whole window changes size, so top/left position still  jumps. I'll try to cook up some math to compensate for it.

Link to comment
Share on other sites

This thread is quite old. Please consider starting a new thread rather than reviving this one.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...