Toggle with one click

Hello, i see this old post of @DavidRutten , which give me an idea to make a toggle with 0,1 and one click instead of double click.
The first square is hidden to move the toggle because one click don’t allow to move it.
I hope someone with c# experience make it better and add option to change the text from True to False and vise versa after every click , and the design like the original toggle.

The original code available here:

A little modification

using System;
using System.Drawing;
using Grasshopper.GUI;
using Grasshopper.GUI.Canvas;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Types;

public class SpecialIntegerObject : GH_Param<GH_Integer>
{
    public SpecialIntegerObject() :
      base(new GH_InstanceDescription("Toggle", "Toggle", "True/False", "Params", "Input"))
    { }

    public override void CreateAttributes()
    {
        m_attributes = new SpecialIntegerAttributes(this);
    }

    protected override Bitmap Icon
    {
        get
        {
            return Toggle.Properties.Resources.toggle;
            //return null;
        }
    }
    public override GH_Exposure Exposure
    {
        get
        {
            return GH_Exposure.primary | GH_Exposure.obscure;
        }
    }
    public override System.Guid ComponentGuid
    {
        get { return new Guid("{4A617FB8-E57A-42fd-8C17-3FFCE014D56F}"); }
    }

    private int m_value = 0;
    public int Value
    {
        get { return m_value; }
        set { m_value = value; }
    }
    protected override void CollectVolatileData_Custom()
    {
        VolatileData.Clear();
        AddVolatileData(new Grasshopper.Kernel.Data.GH_Path(0), 0, new GH_Integer(Value));
    }

    public override bool Write(GH_IO.Serialization.GH_IWriter writer)
    {
        writer.SetInt32("SpecialInteger", m_value);
        return base.Write(writer);
    }
    public override bool Read(GH_IO.Serialization.GH_IReader reader)
    {
        m_value = 0;
        reader.TryGetInt32("SpecialInteger", ref m_value);
        return base.Read(reader);
    }
}

public class SpecialIntegerAttributes : GH_Attributes<SpecialIntegerObject>
{
    private int[][] m_square;
    public SpecialIntegerAttributes(SpecialIntegerObject owner)
      : base(owner)
    {
        m_square = new int[3][];
        m_square[0] = new int[3] {' ',0, 1};

    }

    public override bool HasInputGrip { get { return false; } }
    public override bool HasOutputGrip { get { return true; } }

    private const int ButtonSize = 35;

    //Our object is always the same size, but it needs to be anchored to the pivot.
    protected override void Layout()
    {
        //Lock this object to the pixel grid. 
        //I.e., do not allow it to be position in between pixels.
        Pivot = GH_Convert.ToPoint(Pivot);
        Bounds = new RectangleF(Pivot, new SizeF(3*ButtonSize, ButtonSize));
    }
    /// <summary>
    /// This method returns the button at the given column and row offsets.
    /// </summary>
    private Rectangle Button(int column, int row)
    {
        int x = Convert.ToInt32(Pivot.X);
        int y = Convert.ToInt32(Pivot.Y);
        return new Rectangle(x + column * ButtonSize, y + row * ButtonSize, ButtonSize, ButtonSize);
    }
    /// <summary>
    /// Gets the value for the given button.
    /// </summary>
    private int Value(int column, int row)
    {
        return m_square[row][column];
    }

    public override GH_ObjectResponse RespondToMouseDown(GH_Canvas sender, GH_CanvasMouseEvent e)
    {
        //On a double click we'll set the owner value.
        if (e.Button == System.Windows.Forms.MouseButtons.Left)
        {
            for (int col = 1; col < 3; col++)
            {
                for (int row = 0; row < 1; row++)
                {
                    RectangleF button = Button(col, row);
                    if (button.Contains(e.CanvasLocation))
                    {
                        int value = Value(col, row);
                        Owner.RecordUndoEvent("Square Change");
                        Owner.Value = value;
                        Owner.ExpireSolution(true);
                        return GH_ObjectResponse.Handled;
                    }
                }
            }
        }
        return base.RespondToMouseDown(sender, e);
        //return base.RespondToMouseDoubleClick(sender, e);
    }
    public override void SetupTooltip(PointF point, GH_TooltipDisplayEventArgs e)
    {
        base.SetupTooltip(point, e);
        e.Description = "Toggle";
    }

    /// <summary>
    /// This object is rendered as a 4x4 grid of capsules.
    /// </summary>
    protected override void Render(GH_Canvas canvas, Graphics graphics, GH_CanvasChannel channel)
    {
        if (channel == GH_CanvasChannel.Objects)
        {
            //Render output grip.
            GH_CapsuleRenderEngine.RenderOutputGrip(graphics, canvas.Viewport.Zoom, OutputGrip, true);

            //Render capsules.
            for (int col = 1; col < 3; col++)
            {
                for (int row = 0; row < 1; row++)
                {
                    int value = Value(col, row);
                    Rectangle button = Button(col, row);

                    GH_Palette palette = GH_Palette.White;
                    if (value == Owner.Value)
                        palette = GH_Palette.Black;

                    GH_Capsule capsule = GH_Capsule.CreateTextCapsule(button, button, palette, value.ToString(), 0, 0);
                    capsule.Render(graphics, Selected, Owner.Locked, false);
                    capsule.Dispose();
                }
            }
        }
    }
}

Toggle.gha (10 KB)

5 Likes

I set my middle mouse button to execute a double left click

It is not stable when the middle button is the wheel itself, with click it zoom in or out

At least a Boolean Toggle in the Remote Control Panel will be single click, but yeah double clicking sucks:

1 Like

3D connexion mice have three buttons. Two thumb buttons, a scroll wheel and an additional little button up top.

You could use the Event Switch from Heteroptera, which lets you toggle with a button.

If you want to take it further you can use Metahopper to change the nickname of the button to show the state of the toggle.

button_toggle.gh (6.9 KB)

2 Likes

This is a long way, one component is enough

image

2 Likes

Nice. Where can we find that?

From a UX perspective this is not very intuitive though. Not everyone is a computer geek, like presumably a lot of us here. :wink:

In the first post under the video

1 Like

FWIW, a simple native workaround is to use a Value List set to Check List with just one value set to True, and then exploit that an unchecked value will return nothing:

But again, workarounds really shouldn’t be necessary (as the developer of Discourse pointed out way back when: Double-Click Must Die :skull_and_crossbones:).

3 Likes

I tried something like this before and got the same problem, i tried also add expression to the button in Normal state and Pressed state without success.
I think the solution is not complicated.
In android for example this simple code is enough

In Python i tried but didn’t work:

although again just a workaround, this seems to work for me


or even shorter:
1click2

1 Like

Thank you @benedict , your code help me to change the text when click but i don’t know why the output always 0

using Grasshopper;
using Grasshopper.Kernel;
using Rhino.Geometry;
using System;
using System.Collections.Generic;

using System;
using System.Drawing;
using Grasshopper.GUI;
using Grasshopper.GUI.Canvas;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Types;

public class SpecialIntegerObject : GH_Param<GH_Integer>
{
    public SpecialIntegerObject() :
      base(new GH_InstanceDescription("Toggle", "Toggle", "True/False", "Params", "Input"))
    { }

    public override void CreateAttributes()
    {
        m_attributes = new SpecialIntegerAttributes(this);
    }

    protected override Bitmap Icon
    {
        get
        {
            return toggleone.Properties.Resources.toggle;
            //return null;
        }
    }
    public override GH_Exposure Exposure
    {
        get
        {
            return GH_Exposure.primary | GH_Exposure.obscure;
        }
    }
    public override System.Guid ComponentGuid
    {
        get { return new Guid("{520F0976-35D1-4CC4-925B-2A07BFF17007}"); }
    }

    private int m_value ;
    public int Value
    {
        get { return m_value; }
        set { m_value = value; }
    }
    protected override void CollectVolatileData_Custom()
    {
        VolatileData.Clear();
        AddVolatileData(new Grasshopper.Kernel.Data.GH_Path(0), 0, new GH_Integer(Value));
    }

    public override bool Write(GH_IO.Serialization.GH_IWriter writer)
    {
        writer.SetInt32("SpecialInteger", m_value);
        return base.Write(writer);
    }
    public override bool Read(GH_IO.Serialization.GH_IReader reader)
    {
        //m_value = 0;
        reader.TryGetInt32("SpecialInteger", ref m_value);
        return base.Read(reader);
    }
}

public class SpecialIntegerAttributes : GH_Attributes<SpecialIntegerObject>
{
    private int[][] m_square;
    public SpecialIntegerAttributes(SpecialIntegerObject owner)
      : base(owner)
    {
        m_square = new int[2][];
        m_square[0] = new int[2] { ' ', 0 };

    }

    public override bool HasInputGrip { get { return false; } }
    public override bool HasOutputGrip { get { return true; } }

    private const int ButtonSize = 36;

    public bool bo;

    //Our object is always the same size, but it needs to be anchored to the pivot.
    protected override void Layout()
    {
        //Lock this object to the pixel grid. 
        //I.e., do not allow it to be position in between pixels.
        Pivot = GH_Convert.ToPoint(Pivot);
        Bounds = new RectangleF(Pivot, new SizeF(2 * ButtonSize, ButtonSize));
    }
    /// <summary>
    /// This method returns the button at the given column and row offsets.
    /// </summary>
    private Rectangle Button(int column, int row)
    {
        int x = Convert.ToInt32(Pivot.X);
        int y = Convert.ToInt32(Pivot.Y);
        return new Rectangle(x + column * ButtonSize, y + row * ButtonSize, ButtonSize, ButtonSize);
    }
    /// <summary>
    /// Gets the value for the given button.
    /// </summary>
    private int Value(int column, int row)
    {
        return m_square[row][column];
    }

    public override GH_ObjectResponse RespondToMouseDown(GH_Canvas sender, GH_CanvasMouseEvent e)
    {
        //On one click we'll set the owner value.
        if (e.Button == System.Windows.Forms.MouseButtons.Left)
        {

            for (int col = 1; col < 2; col++)
            {
                for (int row = 0; row < 1; row++)
                {
                    RectangleF button = Button(2, 1);
                    if (button.Contains(e.CanvasLocation))
                    {
                        int value = Value(col, row);
                        Owner.RecordUndoEvent("Square Change");
                        Owner.Value = value;
                        Owner.ExpireSolution(true);
                        return GH_ObjectResponse.Handled;
                    }
                }
            }
            //if (bo) { bo = false;} else { bo = true;}
            { bo = bo ? false : true; }
 
        }
        return base.RespondToMouseDown(sender, e);

    }
    public override void SetupTooltip(PointF point, GH_TooltipDisplayEventArgs e)
    {
        base.SetupTooltip(point, e);
        e.Description = "Toggle";
    }

    /// <summary>
    /// This object is rendered as a 1x3 grid of capsules.
    /// </summary>

    protected override void Render(GH_Canvas canvas, Graphics graphics, GH_CanvasChannel channel)
    {
        if (channel == GH_CanvasChannel.Objects)
        {
            //Render output grip.
            GH_CapsuleRenderEngine.RenderOutputGrip(graphics, canvas.Viewport.Zoom, OutputGrip, true);

            //Render capsules.
            for (int col = 1; col < 2; col++)
            {
                for (int row = 0; row < 1; row++)
                {
                    int value = Value(col, row);
                    Rectangle button = Button(col, row);

                    GH_Palette palette = GH_Palette.White;
                    if (bo == true)
                    { value = 1; palette = GH_Palette.Blue; } 
                    else if (bo == false)
                    { value = 0; palette = GH_Palette.White; }

                    //if (value == Owner.Value)
                        //palette = GH_Palette.Blue;

                    GH_Capsule capsule = GH_Capsule.CreateTextCapsule(button, button, palette, value.ToString(), 4, 4);
                    capsule.Render(graphics, Selected, Owner.Locked, false);
                    capsule.Dispose();
                }
            }
        }
    }
}

Set to Value Cycle you can click either arrow left or right doesn’t matter…

1 Like

I’m not at the office, but I think you need to declare bo outside the scope to keep its value.
oups, you did. my bad.

I am sure the code could be better and shorter but i don’t have experience with c# to improve it or to know where is the problem exactly

I’m not an expert neither, but I will for sure have a look at this

Thank you and i still try

Yes that’s same workaround @dannyboyesiom posted back in the old thread I linked to above. The Value List really is a versatile interface.

1 Like

@anon39580149

sorry the late reply

this seems to work, although I’m not sure to have cleaned up all code propoerly:
singleClick.gha (8.5 KB)

oneClick


using System;
using System.Collections.Generic;
using Grasshopper.Kernel;
using Rhino.Geometry;
using System.Drawing;
using Grasshopper.GUI;
using Grasshopper.GUI.Canvas;
using Grasshopper.Kernel.Types;

namespace singleClick  //adapt to your project
{

    public class SpecialIntegerObject : GH_Param<GH_Integer>
    {
        public SpecialIntegerObject() :
          base(new GH_InstanceDescription("Toggle", "Toggle", "True/False", "Params", "Input"))
        { }

        public override void CreateAttributes()
        {
            m_attributes = new SpecialIntegerAttributes(this);
        }

        protected override Bitmap Icon
        {
            get
            {
                //return toggleone.Properties.Resources.toggle;
                return null;
            }
        } 
        public override GH_Exposure Exposure
        {
            get
            {
                return GH_Exposure.primary | GH_Exposure.obscure;
            }
        }
        public override System.Guid ComponentGuid
        {
            get { return new Guid("{520F0976-35D1-4CC4-925B-2A07BFF17007}"); }
        }

        private int m_value;
        public int Value
        {
            get { return m_value; }
            set { m_value = value; }
        }
        protected override void CollectVolatileData_Custom()
        {
            VolatileData.Clear();
            AddVolatileData(new Grasshopper.Kernel.Data.GH_Path(0), 0, new GH_Integer(Value));
        }

        public override bool Write(GH_IO.Serialization.GH_IWriter writer)
        {
            writer.SetInt32("SpecialInteger", m_value);
            return base.Write(writer);
        }
        public override bool Read(GH_IO.Serialization.GH_IReader reader)
        {
            //m_value = 0;
            reader.TryGetInt32("SpecialInteger", ref m_value);
            return base.Read(reader);
        }
    }

    public class SpecialIntegerAttributes : GH_Attributes<SpecialIntegerObject>
    {
        
        public SpecialIntegerAttributes(SpecialIntegerObject owner)
          : base(owner)
        {

        }

        public override bool HasInputGrip { get { return false; } }
        public override bool HasOutputGrip { get { return true; } }

        private const int ButtonSize = 36;

        public bool bo;

        //Our object is always the same size, but it needs to be anchored to the pivot.
        protected override void Layout()
        {
            //Lock this object to the pixel grid. 
            //I.e., do not allow it to be position in between pixels.
            Pivot = GH_Convert.ToPoint(Pivot);
            Bounds = new RectangleF(Pivot, new SizeF(ButtonSize, ButtonSize));
        }
        /// <summary>
        /// This method returns the button at the given column and row offsets.
        /// </summary>
        private Rectangle Button()
        {
            int x = Convert.ToInt32(Pivot.X);
            int y = Convert.ToInt32(Pivot.Y);
            return new Rectangle(x, y, ButtonSize, ButtonSize);
        }
        /// <summary>
        /// Gets the value for the given button.
        /// </summary>
        private int Value(int column, int row)
        {
            return Convert.ToInt32(bo);
        }

        public override GH_ObjectResponse RespondToMouseDown(GH_Canvas sender, GH_CanvasMouseEvent e)
        {
            //On one click we'll set the owner value.
            if (e.Button == System.Windows.Forms.MouseButtons.Left)
            {
                bo = bo ? false : true; 
                RectangleF button = Button();
                if (button.Contains(e.CanvasLocation))
                {
                    int value = Convert.ToInt32(bo);
                    Owner.RecordUndoEvent("Square Change");
                    Owner.Value = value;
                    Owner.ExpireSolution(true);
                    return GH_ObjectResponse.Handled;
                }
                
                
                
            }
            return base.RespondToMouseDown(sender, e);

        }
        public override void SetupTooltip(PointF point, GH_TooltipDisplayEventArgs e)
        {
            base.SetupTooltip(point, e);
            e.Description = "Toggle";
        }

        /// <summary>
        /// This object is rendered as a 1x3 grid of capsules.
        /// </summary>

        protected override void Render(GH_Canvas canvas, Graphics graphics, GH_CanvasChannel channel)
        {
            if (channel == GH_CanvasChannel.Objects)
            {
                //Render output grip.
                GH_CapsuleRenderEngine.RenderOutputGrip(graphics, canvas.Viewport.Zoom, OutputGrip, true);

                //Render capsules.

                        Rectangle button = Button();
                        GH_Palette palette = GH_Palette.White;
                        if (bo)
                        { 
                           palette = GH_Palette.Blue;
                        }
                        else
                        { 
                            palette = GH_Palette.White;
                        }

                        GH_Capsule capsule = GH_Capsule.CreateTextCapsule(button, button, palette, bo.ToString(), 4, 4);
                        capsule.Render(graphics, Selected, Owner.Locked, false);
                        capsule.Dispose();
            }
        }
    }
}

Thanks for this task, I’ve learned a ton of things by doing this :slight_smile:
hope it helps others as well

Ben

2 Likes