Jump to content

How to detect a mouse event over a part in the parts list in the editor?


Recommended Posts

You can add a listener to the button itself (or add a MonoBehaviour that implements IPointerClickHandler to the button's GameObject). My preference is to edit the prefab wherever possible as it tends to make catching any edge cases a lot simpler. I kind of get the impression that you might want to prevent a part from spawning when clicking with alt held down so here's some code that goes a step further and lets you veto the click by using a small replacement click handler:

public static class EditorIconEvents
{
    public static readonly EventData<EditorPartIcon, EditorIconClickEvent> OnEditorPartIconClicked =
        new EventData<EditorPartIcon, EditorIconClickEvent>("EditorPartIconClicked");

    public class EditorIconClickEvent
    {
        public void Veto() { Vetoed = true; }
        public bool Vetoed { get; private set; }
    }

    [KSPAddon(KSPAddon.Startup.EditorAny, true)]
    private class InstallEditorIconEvents : MonoBehaviour
    {
        private IEnumerator Start()
        {
            while (EditorPartList.Instance == null) yield return null;

            var prefab = EditorPartList.Instance.partPrefab;

            InstallReplacementHandler(prefab);
                
            // some icons have already been instantiated, need to fix those too. Only needed this first time;
            // after that, the prefab will already contain the changes we want to make
            foreach (var icon in EditorPartList.Instance.gameObject.GetComponentsInChildren<EditorPartIcon>(true))
                InstallReplacementHandler(icon);

            Destroy(gameObject);
        }

        private static void InstallReplacementHandler(EditorPartIcon icon)
        {
            icon.gameObject.AddComponent<ReplacementClickHandler>();
        }
    }

    private class ReplacementClickHandler : MonoBehaviour, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler
    {
        private EditorPartIcon _icon;
        private PointerClickHandler _originalClickHandler;
        private Button _button;

        private void Start()
        {
            _button = GetComponent<Button>();
            _originalClickHandler = GetComponent<PointerClickHandler>();
            _icon = GetComponent<EditorPartIcon>();

            if (_button == null || _originalClickHandler == null || _icon == null)
            {
                Debug.LogError("Couldn't find an expected component");
                Destroy(this);
                return;
            }

            _originalClickHandler.enabled = false; // we'll be managing these events instead

            // unhook EditorPartIcon's listener from the button
            // this will allow us to veto any clicks
            _button.onClick.RemoveListener(_icon.MouseInput_SpawnPart);
        }

        public void OnPointerClick(PointerEventData eventData)
        {
            var evt = new EditorIconClickEvent();

            OnEditorPartIconClicked.Fire(_icon, evt);

            if (evt.Vetoed) return;

            _originalClickHandler.OnPointerClick(eventData);

            if (_button.interactable) _icon.MouseInput_SpawnPart();
        }

        public void OnPointerDown(PointerEventData eventData)
        {
            _originalClickHandler.OnPointerDown(eventData);
        }

        public void OnPointerUp(PointerEventData eventData)
        {
            _originalClickHandler.OnPointerUp(eventData);
        }
    }
}

And example usage:

[KSPAddon(KSPAddon.Startup.EditorAny, false)]
class TestTheEvent : MonoBehaviour
{
    private void Start()
    {
        EditorIconEvents.OnEditorPartIconClicked.Add(IconClicked);
    }

    private void OnDestroy()
    {
        EditorIconEvents.OnEditorPartIconClicked.Remove(IconClicked);
    }

    private void IconClicked(EditorPartIcon icon, EditorIconEvents.EditorIconClickEvent evt)
    {
        if (!Input.GetKey(KeyCode.LeftAlt) && !Input.GetKey(KeyCode.RightAlt))
            return;

        Debug.LogWarning("Icon was clicked for " + icon.partInfo.name + " (" + icon.partInfo.title + ")");
        evt.Veto(); // prevent part from being spawned
    }
}

 

Link to comment
Share on other sites

7 hours ago, xEvilReeperx said:

You can add a listener to the button itself (or add a MonoBehaviour that implements IPointerClickHandler to the button's GameObject). My preference is to edit the prefab wherever possible as it tends to make catching any edge cases a lot simpler. I kind of get the impression that you might want to prevent a part from spawning when clicking with alt held down so here's some code that goes a step further and lets you veto the click by using a small replacement click handler:

first off, a big thank you for writing that code.

Adding a listener to the button would be fine, if that's not too difficult.  What I haven't yet figured out is how to do this, for the buttons which are added to the parts window.

And, you mention "edit the prefab", which is total greek to me (I don't speak greek :-)  ).  What do you mean, and how is it done?  Or is adding the listenr to the button what you are referring to?

Thanks

Edit:  I just read the code, I assume that the code IS adding a listener to the button?

Edited by linuxgurugamer
Link to comment
Share on other sites

1 hour ago, linuxgurugamer said:

Adding a listener to the button would be fine, if that's not too difficult.  What I haven't yet figured out is how to do this, for the buttons which are added to the parts window.

Well there isn't much to it. All you need to know is how to find the items you're interested. Dump their contents, see if there's anything potentially useful and try it out.

_UIMaster has components:
...c: UnityEngine.Transform
...c: KSP.UI.UIMasterController
...c: DoNotDestroy
->MainCanvas has components:
....c: UnityEngine.RectTransform
....c: UnityEngine.Canvas
....c: UnityEngine.UI.CanvasScaler
....c: UnityEngine.UI.GraphicRaycaster
....c: UnityEngine.CanvasGroup
....c: KSP.UI.CanvasGroupInputLock
-->Editor has components:
.....c: UnityEngine.RectTransform
.....c: UnityEngine.Canvas
.....c: UnityEngine.UI.GraphicRaycaster
--->Panel Parts List has components:
......c: UnityEngine.RectTransform
......c: KSP.UI.UIPanelTransition
......c: KSP.UI.Screens.EditorPartList
......c: UnityEngine.CanvasGroup
---->Mode Transition has components:
.......c: UnityEngine.RectTransform
.......c: KSP.UI.UIPanelTransition
.......c: UnityEngine.CanvasGroup
----->PartList Area has components:
........c: UnityEngine.RectTransform
........c: UnityEngine.UI.VerticalLayoutGroup
------>PartList and sorting has components:
.........c: UnityEngine.RectTransform
.........c: UnityEngine.UI.LayoutElement
------->ListAndScrollbar has components:
..........c: UnityEngine.RectTransform
..........c: UnityEngine.CanvasRenderer
..........c: UnityEngine.EventSystems.EventTrigger
-------->bg has components:
...........c: UnityEngine.RectTransform
...........c: UnityEngine.CanvasRenderer
...........c: UnityEngine.UI.Image
--------->ScrollRect has components:
............c: UnityEngine.RectTransform
............c: UnityEngine.UI.ScrollRect
............c: UnityEngine.CanvasRenderer
............c: UnityEngine.UI.Image
............c: UnityEngine.UI.Mask
............c: KSP.UI.Screens.UIScrollRectState
---------->PartGrid has components:
.............c: UnityEngine.RectTransform
.............c: UnityEngine.UI.GridLayoutGroup
.............c: UnityEngine.UI.ContentSizeFitter
----------->PartItemPrefab(Clone) has components:
..............c: UnityEngine.RectTransform
..............c: UnityEngine.CanvasRenderer
..............c: UnityEngine.UI.Image
..............c: UnityEngine.UI.Button
..............c: KSP.UI.Screens.EditorPartIcon
..............c: KSP.UI.PointerEnterExitHandler
..............c: UnityEngine.UI.LayoutElement
..............c: KSP.UI.PointerClickHandler
..............c: KSP.UI.Screens.Editor.PartListTooltipController

The icon has two interesting bits in there, a UnityEngine.UI.Button and KSP.UI.PointerClickHandler. Now find all part icons, add a listener to their button and see if it does what you expect:

[KSPAddon(KSPAddon.Startup.EditorAny, false)]
class EditorIconButtonClickTester : MonoBehaviour
{
    private IEnumerator Start()
    {
        while (EditorPartList.Instance == null)
            yield return null;

        foreach (
            var icon in
                EditorPartList.Instance.partListScrollRect.gameObject.GetComponentsInChildren<EditorPartIcon>(
                    true))
        {
            var whichIcon = icon;
            icon.gameObject.GetComponent<Button>().onClick.AddListener(() => IconClicked(whichIcon));
        }
    }

    private static void IconClicked(EditorPartIcon icon)
    {
        if (!Input.GetKey(KeyCode.LeftAlt) && !Input.GetKey(KeyCode.RightAlt)) return;

        Debug.LogWarning("You clicked on " + icon.partInfo.name);
    }
}

As for prefabs: if the game clones them when it creates something, any changes you make will be automatically applied to the new object as well. Sometimes this is useful when you don't actually know how the game manages stuff ... for example, does the editor create all of the part icons straight off the bat or are they destroyed and a new set created on every category change? If it's the latter, the code above is going to break. If I tweak the prefab instead, my changes (specifically: the component I added in the first post which tweaks the icon behaviour) will just automatically be copied into the new ones and I don't have to be concerned about it at all

Edited by xEvilReeperx
Link to comment
Share on other sites

There are a few ways to find the prefabs. 

Sometimes the related classes simply have public references to them. In this case EditorPartList.Instance.partPrefab is the relevant reference.

For other cases, where it's not so easy there some other things to try.

Use Debug Stuff to find out how the editor part icons are setup. Sometimes this can give you a hint to what the prefab may be called.

If there isn't an easily visible prefab available you can always use AssetBase. It has a public array of all prefabs loaded, AssetBase.prefabs, and a static method to return any prefab by name, AssetBase.GetPrefab. Of course, the instance property for AssetBase isn't available, so to check through all of the prefabs you'll have to do something silly, like run GetComponentsOfType<AssetBase>, then print out the name of each prefab to see if one sounds likely (which is fine since it's not something you'll be doing in the release version).

To "edit the prefab" you can just attach your own MonoBehaviour to it. Once you have the prefab (which is always a Unity GameObject) just use AddComponent<YourMono> on the prefab object.

This part of the code is the "adding a listener" bit:

private static void InstallReplacementHandler(EditorPartIcon icon)
        {
            icon.gameObject.AddComponent<ReplacementClickHandler>();
        }

 

Edit: Just to be clear, the prefab is the base component which is used to instantiate all of the part icon buttons. It is something that was created in the Unity editor and exported as a prefab so that KSP can grab that component and use it as basically a template to make multiple copies for every part. Each part then has a different preview icon and tooltip which are added to the button.

 

Edited by DMagic
Link to comment
Share on other sites

  • 4 years later...
On 9/4/2016 at 12:42 AM, xEvilReeperx said:

You can add a listener to the button itself (or add a MonoBehaviour that implements IPointerClickHandler to the button's GameObject). My preference is to edit the prefab wherever possible as it tends to make catching any edge cases a lot simpler. I kind of get the impression that you might want to prevent a part from spawning when clicking with alt held down so here's some code that goes a step further and lets you veto the click by using a small replacement click handler:


public static class EditorIconEvents
{
    public static readonly EventData<EditorPartIcon, EditorIconClickEvent> OnEditorPartIconClicked =
        new EventData<EditorPartIcon, EditorIconClickEvent>("EditorPartIconClicked");

    public class EditorIconClickEvent
    {
        public void Veto() { Vetoed = true; }
        public bool Vetoed { get; private set; }
    }

    [KSPAddon(KSPAddon.Startup.EditorAny, true)]
    private class InstallEditorIconEvents : MonoBehaviour
    {
        private IEnumerator Start()
        {
            while (EditorPartList.Instance == null) yield return null;

            var prefab = EditorPartList.Instance.partPrefab;

            InstallReplacementHandler(prefab);
                
            // some icons have already been instantiated, need to fix those too. Only needed this first time;
            // after that, the prefab will already contain the changes we want to make
            foreach (var icon in EditorPartList.Instance.gameObject.GetComponentsInChildren<EditorPartIcon>(true))
                InstallReplacementHandler(icon);

            Destroy(gameObject);
        }

        private static void InstallReplacementHandler(EditorPartIcon icon)
        {
            icon.gameObject.AddComponent<ReplacementClickHandler>();
        }
    }

    private class ReplacementClickHandler : MonoBehaviour, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler
    {
        private EditorPartIcon _icon;
        private PointerClickHandler _originalClickHandler;
        private Button _button;

        private void Start()
        {
            _button = GetComponent<Button>();
            _originalClickHandler = GetComponent<PointerClickHandler>();
            _icon = GetComponent<EditorPartIcon>();

            if (_button == null || _originalClickHandler == null || _icon == null)
            {
                Debug.LogError("Couldn't find an expected component");
                Destroy(this);
                return;
            }

            _originalClickHandler.enabled = false; // we'll be managing these events instead

            // unhook EditorPartIcon's listener from the button
            // this will allow us to veto any clicks
            _button.onClick.RemoveListener(_icon.MouseInput_SpawnPart);
        }

        public void OnPointerClick(PointerEventData eventData)
        {
            var evt = new EditorIconClickEvent();

            OnEditorPartIconClicked.Fire(_icon, evt);

            if (evt.Vetoed) return;

            _originalClickHandler.OnPointerClick(eventData);

            if (_button.interactable) _icon.MouseInput_SpawnPart();
        }

        public void OnPointerDown(PointerEventData eventData)
        {
            _originalClickHandler.OnPointerDown(eventData);
        }

        public void OnPointerUp(PointerEventData eventData)
        {
            _originalClickHandler.OnPointerUp(eventData);
        }
    }
}

And example usage:


[KSPAddon(KSPAddon.Startup.EditorAny, false)]
class TestTheEvent : MonoBehaviour
{
    private void Start()
    {
        EditorIconEvents.OnEditorPartIconClicked.Add(IconClicked);
    }

    private void OnDestroy()
    {
        EditorIconEvents.OnEditorPartIconClicked.Remove(IconClicked);
    }

    private void IconClicked(EditorPartIcon icon, EditorIconEvents.EditorIconClickEvent evt)
    {
        if (!Input.GetKey(KeyCode.LeftAlt) && !Input.GetKey(KeyCode.RightAlt))
            return;

        Debug.LogWarning("Icon was clicked for " + icon.partInfo.name + " (" + icon.partInfo.title + ")");
        evt.Veto(); // prevent part from being spawned
    }
}

 

Sorry if this is a crazy necro post, but I'm a bit hung up on a mod I'm working on and nearing completion, and I really think this code is close to what I'm looking for in order to finish. However, my understanding of editing prefabs is very limited. Is there a way to change this code slightly to prevent the "Research" button in the TechTree screen from researching a given tech node unless other conditions have been met?

Link to comment
Share on other sites

16 hours ago, Codapop said:

Is there a way to change this code slightly

You can use the same general idea.  In this case, it's easier to straight hijack the live button once it exists. Most of the effort of this is figuring out how to get a reference to the thing you want (conveniently, RDController.Instance.actionButton) and how to manipulate it in a way that doesn't break stock code. Here is some prototype code to get you started:

/*
// RDController.Instance.nodePanel
Panel_Right has components:
...c: UnityEngine.RectTransform
...c: KSP.UI.UIStatePanel
->Panel node has components: <-- this is the RnD panel (there are two panels in this GUI: RnD and archives)
....c: UnityEngine.RectTransform
....c: UnityEngine.CanvasRenderer
....c: UnityEngine.UI.Image
-->Button has components:
.....c: UnityEngine.RectTransform
.....c: UnityEngine.CanvasRenderer
.....c: UnityEngine.UI.Image
.....c: UnityEngine.UI.Button <-- this fires clicks
.....c: KSP.UI.UIStateButton <-- this listens to Button and is involved in the visual state of the button
--->Text has components:
......c: UnityEngine.RectTransform
......c: UnityEngine.CanvasRenderer
......c: TMPro.TextMeshProUGUI <-- you can use this to manipulate the button text
---->TMP UI SubObject [TextMeshPro/Sprite] has components: <-- I assume this has something to do with the science graphic in the button text
.......c: UnityEngine.RectTransform
.......c: UnityEngine.CanvasRenderer
.......c: TMPro.TMP_SubMeshUI
*/

    [KSPAddon(KSPAddon.Startup.SpaceCentre, true /* run once */)]
    class ConditionalResearchButtonSetup : MonoBehaviour {
        private void Start() {
            RDController.OnRDTreeSpawn.Add(i => AddButton(i)); // EventData doesn't seem to like non-static methods
            Destroy(gameObject);
        }

        // inserts conditional button into RnD instance
        private static void AddButton(RDController instance) {
            instance.actionButton.gameObject.AddComponent<ConditionalResearchButton>();
        }
    }

 
    // Conditional research button. When player clicks a technology, fires a message
    // allowing recipients to deny player access to the button
    //
    // Also adds confirmation dialog as an example of how you can intercept an event entirely
    class ConditionalResearchButton : MonoBehaviour {
        public class Request {
            public RDNode node;
            public bool allowed;

            public void Deny() { allowed = false; }
        }

        public static EventData<Request> OnResearchButtonPresented = new EventData<ConditionalResearchButton.Request>("OnResearchButtonPresented");

        private Image _buttonImage;
        private Button _button;
        private Button.ButtonClickedEvent _originalEvent;

        private void Awake() {
            // Start is too slow to grab these references if we want to use OnEnable
            _buttonImage = GetComponent<Image>();
            _button = GetComponent<Button>();

            // hijack button so we can decide whether it's been "clicked"
            _originalEvent = _button.onClick;
            _button.onClick = new Button.ButtonClickedEvent();
            _button.onClick.AddListener(OnButtonClicked);

            // the button we have is the "raw" button, but a lot of state-driven buttons
            // have another class controlling visual states. RDController sets the
            // state each time the panel changes for some reason, which is a good enough
            // way for us to stay informed about selection changes
            GetComponent<UIStateButton>().onValueChanged.AddListener(_ => UpdateButtonStatus());
        }

        private void OnEnable() {
            UpdateButtonStatus();
        }

        // if the button is used for research, decide if the player can click it
        private void UpdateButtonStatus() {
            // don't mess with the button if it's not a research button (seems to serve dual purposes?)
            // otherwise, if it's a research button, decide if it should be enabled
            ToggleButton(!IsNodeResearchable() || AllowResearch()); 
        }

        private void ToggleButton(bool enable) {
            _button.enabled = enable;

            // color difference to clarify button status
            _buttonImage.color = enable ? Color.white : Color.red; 
        }

        private void OnButtonClicked() {
            // here you could decide not to call original listeners
            // as an example, I'll add a confirmation window
            if (IsNodeResearchable()) {
                PopupDialog.SpawnPopupDialog(
                    new Vector2(0.5f, 0.5f),
                    new Vector2(0.5f, 0.5f),
                    new MultiOptionDialog(
                        "ConfirmResearchDialog", $"Are you sure you want to purchase {RDController.Instance.node_selected.tech.title}?", 
                        "Confirm?", 
                        UISkinManager.GetSkin("KSP window 7"),
                        new DialogGUIButton("Yes", UserConfirmedPurchase, true),
                        new DialogGUIButton("No", () => { }, true)), 
                    false, 
                    UISkinManager.GetSkin("KSP window 7"));
            } else {
                _originalEvent.Invoke(); // not a research event
            }
        }

        // user confirmed they want to research whatever it is that's active
        private void UserConfirmedPurchase() {
            _originalEvent.Invoke(); // just as though user clicked the button themselves
        }

        // user can select already-researched nodes. We don't care about those
        private bool IsNodeResearchable() {
            var node = RDController.Instance.node_selected;

            return node != null && node.state == RDNode.State.RESEARCHABLE;
        }

        // fires event to decide if the button is enabled
        private bool AllowResearch() {
            var request = new Request { node = RDController.Instance.node_selected, allowed = true };
            OnResearchButtonPresented.Fire(request);
            return request.allowed;
        }
    }

    // An addon that wants to control whether or not the player can research something
    [KSPAddon(KSPAddon.Startup.SpaceCentre, false)]
    class LetsStayLowTech : MonoBehaviour {
        private void Start() {
            ConditionalResearchButton.OnResearchButtonPresented.Add(ShouldWeAllowIt);
        }

        private void OnDestroy() {
            ConditionalResearchButton.OnResearchButtonPresented.Remove(ShouldWeAllowIt);
        }

        private void ShouldWeAllowIt(ConditionalResearchButton.Request request) {
            // nobody gets to use techs that cost more than 50 science
            // note: might need to apply game modifiers here
            if (request.node.tech.scienceCost > 50f)
                request.Deny();
        }
    }

 

Link to comment
Share on other sites

8 hours ago, xEvilReeperx said:

You can use the same general idea.  In this case, it's easier to straight hijack the live button once it exists. Most of the effort of this is figuring out how to get a reference to the thing you want (conveniently, RDController.Instance.actionButton) and how to manipulate it in a way that doesn't break stock code. Here is some prototype code to get you started:

 

Wow, thank you very very much!! I've literally spent several tens of hours trying to get this to work prior to your explanation and code. Thank you thank you!!

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...