Eto realtime output?

I want to use the scroll wheel to iterate through a list of objects in Grasshopper. I’m currently doing this with a text dot in Rhino. Once it is selected, I can change the height input by clicking the text field and scrolling the mouse wheel. The textdot in the Rhino viewport updates instantly and is synchronized in Grasshopper after a short delay. The problem with this approach is that the smallest size is 3 units and I need index 0…

I also have a script to change the value of a textdot with an Eto dialog / popup.
It uses a numeric stepper and the value can be changed with the scroll wheel.

The problem with the script is that it only updates the textdot after clicking ok.

Is there a way to get the new value to a textdot in realtime without having to press ok?

################################################################################
# Source file: SampleEtoRebuildCurve.py
# MIT License - Copyright (c) 2017 Robert McNeel & Associates.
# See License.md in the root of this repository for details.
################################################################################

# Imports from
from Rhino.UI import *
from Eto.Forms import *
from Eto.Drawing import *

import Rhino
import scriptcontext as sc
import rhinoscriptsyntax as rs

rs.Command("_-Gumball Hide Enter")

################################################################################
# Class to hold arguments
################################################################################
class TextDotArgs():
    
    # Initializer
    def __init__(self):
        self.Percentage = 10

    
    # Validator
    def IsValid(self):
        if self.Percentage < 0 or self.Percentage > 30: return False

        return True
    
################################################################################
# Change TextDot value dialog class
################################################################################
class TextDotDialog(Dialog[bool]):
    
    # Initializer
    def __init__(self, args):
        self.Args = args
        # Initialize dialog box
        self.Title = 'SetTextDot'
        self.Padding = Padding(5)
        # Create layout
        layout = TableLayout()
        layout.Padding = Padding(5)
        layout.Spacing = Size(5, 5)
        layout.Rows.Add(self.CreateSteppers())
        layout.Rows.Add(None) # spacer
        layout.Rows.Add(None) # spacer
        layout.Rows.Add(self.CreateButtons())
        # Set the dialog content
        self.Content = layout
    
    # Creates numeric stepper controls
    def CreateSteppers(self):
        # Create labels
        label0 = Label(Text = 'New Value:')

        label2 = Label(Text = '({})'.format(self.Args.Percentage))

        # Create numeric steppers
        self.Percentage = NumericStepper(
            Value = self.Args.Percentage,
            MinValue = 0,
            MaxValue = 30
            )

        # Create table layout
        layout = TableLayout()
        layout.Spacing = Size(5, 5)
        
        layout.Rows.Add(TableRow(label0, self.Percentage))

        return layout
    
    # OK button click handler
    def OnOkButtonClick(self, sender, e):
        # Harvest control values before closing
        self.Args.Percentage = self.Percentage.Value
        self.Close(True)
    
    # Cancel button click handler
    def OnCancelButtonClick(self, sender, e):
        self.Close(False)
    
    # Create button controls
    def CreateButtons(self):
        # Create the default button
        self.DefaultButton = Button(Text = 'OK')
        self.DefaultButton.Click += self.OnOkButtonClick
        # Create the abort button
        self.AbortButton = Button(Text = 'Cancel')
        self.AbortButton.Click += self.OnCancelButtonClick
        # Create button layout
        button_layout = TableLayout()
        button_layout.Spacing = Size(5, 5)
        button_layout.Rows.Add(TableRow(None, self.DefaultButton, self.AbortButton, None))
        return button_layout
    
################################################################################
# Function to change the TextDot value
################################################################################

def TestTextDot():
    
    res, objref = Rhino.Input.RhinoGet.GetMultipleObjects(
        "Select at least one TextDot", 
        True, 
        Rhino.DocObjects.ObjectType.TextDot
        )
    if res != Rhino.Commands.Result.Success: return
    
    args = TextDotArgs()

    dlg = TextDotDialog(args)
    
    rc = dlg.ShowModal(RhinoEtoApp.MainWindow)
    if rc and args.IsValid():

        objects = sc.doc.Objects.GetSelectedObjects(False, False)

        for obj in objects:
            if isinstance(obj, Rhino.DocObjects.TextDotObject):
                obj.Geometry.Text = str(int(args.Percentage))
                obj.CommitChanges()
    sc.doc.Views.Redraw()
    
################################################################################
# Check to see if this file is being executed as the "main" python
# script instead of being used as a module by some other python script
# This allows us to use the module which ever way we want.
################################################################################
if __name__ == "__main__":
    TestTextDot()

I added this but it doesn’t seem to do anything:

    def PercentageChanged(self, sender, e):
        self.Args.Percentage = self.Percentage.Value

Hi @martinsiegrist,

You need to use the NumericStepper.ValueChanged event to apply any changes the user has made when it happens.

1 Like

Sorry, where does this need to go in my script above?

After you’ve created your stepper component and with the PercentageChanged method in the same class:

self.Percentage.ValueChanged += self.PercentageChanged

Maybe rename PerccentageChanged to OnPercentageValueChanged to keep in line with the other event handler method names.

I’m doing something wrong.

    def CreateSteppers(self):
        # Create labels
        label0 = Label(Text = 'New Value:')

        label2 = Label(Text = '({})'.format(self.Args.Percentage))

        # Create numeric steppers
        self.Percentage = NumericStepper(
            Value = self.Args.Percentage,
            MinValue = 0,
            MaxValue = 30
            )
        self.Percentage.Value = self.Args.Percentage
        self.Percentage.ValueChanged += self.OnPercentageChanged
        
        self.Percentage.Value = self.Args.Percentage
        NumericStepper.ValueChanged += self.OnPercentageChanged

        # Create table layout
        layout = TableLayout()
        layout.Spacing = Size(5, 5)
        
        layout.Rows.Add(TableRow(label0, self.Percentage))

        return layout

    def OnPercentageChanged(self, sender, e):
        self.Args.Percentage = NumericStepper.ValueChanged


    # OK button click handler
    def OnOkButtonClick(self, sender, e):
        # Harvest control values before closing
        self.Args.Percentage = self.Percentage.Value
        self.Close(True)

Yeah, you are assigning the event to the value field instead of the value. Try:

def OnPercentageChanged(self, sender, e):
        self.Args.Percentage = self.Percentage.Value

Also, I suggest renaming self.Percentage to self.Stepper or similar for readability.

1 Like

or PercentageStepper.

This is what I have and I’m lost:

    def CreateSteppers(self):
        # Create labels
        label0 = Label(Text = 'New Value:')

        label2 = Label(Text = '({})'.format(self.Args.Percentage))

        # Create numeric steppers
        self.Percentage = NumericStepper(
            Value = self.Args.Percentage,
            MinValue = 0,
            MaxValue = 30
            )

        self.Percentage.Value = self.Args.Percentage
        self.Percentage.ValueChanged += self.OnPercentageStepperChanged


        # Create table layout
        layout = TableLayout()
        layout.Spacing = Size(5, 5)
        
        layout.Rows.Add(TableRow(label0, self.Percentage))

        return layout

    def OnPercentageStepperChanged(self, sender, e):
        self.Args.Percentage = self.Percentage.Value

The following two lines are for the event handler, right?

self.Percentage.Value = self.Args.Percentage
self.Percentage.ValueChanged += self.OnPercentageStepperChanged

Only the second line is for the event handler. The first line initializes the stepper value to whatever the TextDotArgs instance passed in has.

If you are expecting the label text to change, just updating the self.Args.Percentage isn’t enough, since the label itself doesn’t have any reference to that variable.

You need to update the Text field of the Label object, and possibly call OnTextChanged() (not sure if this is necessary, don’t use Eto much).

def OnPercentageStepperChanged(self, sender, e):
        self.Args.Percentage = self.Percentage.Value
        self.label2.Text = '({})'.format(self.Args.Percentage))
        self.label2.OnTextChanged() # maybe not necessary?

Sorry I still don’t understand where the NumericStepper.ValueChanged needs to be?

You already have it with the Percentage.ValueChanged. You don’t need to add a line that says NumericStepper.ValueChanged += self.OnPercentageChanged. Your self.Percentage is the NumericStepper.

Ok but still it only changes the text dot after clicking ok.

I want the change to happen instantly after scrolling or using the up down arrows on the text field.

That is because you update the text dot only after closing the dialog.

The part that says:

        for obj in objects:
            if isinstance(obj, Rhino.DocObjects.TextDotObject):
                obj.Geometry.Text = str(int(args.Percentage))
                obj.CommitChanges()

This shows the eto form.

rc = dlg.ShowModal(RhinoEtoApp.MainWindow)

I really don’t understand what I have to change?

Yes, that is where you show the modal form, but only after it returns do you do the update to the text dot geometry to set the new text.

You could pass in the ObjRef list you get from Rhino.Input.RhinoGet.GetMultipleObjects to your TextDotArgs instance.

Then whenever you handle the stepper ValueChanged event you can loop over the list of ObjRef, get the TextDot from it, duplicate the TextDot instance, set the new Text for the copy and use Replace(ObjRefInstance, TextDotInstance) on the document Objects table.

I would pass on the document to your TextDotArgs as well, so you don’t have to use sc.doc in the implementation of the update function.

1 Like

Ok, one thing is done :slight_smile:

I was expecting that when I reference the textdot in Grasshopper, the value I get in Grasshopper would also update in realtime but that’s not the case unfortunately. It works when I’m using a trigger component on the annotation dot reference.

It sort of works with an event handler which triggers when something is selected:

from Rhino.UI import *
from Eto.Forms import *
from Eto.Drawing import *

import Rhino
import scriptcontext as sc
import rhinoscriptsyntax as rs

obj = rs.GetObject("select TextDot", filter = 8192, preselect = True, )

rs.Command("_-Gumball Hide Enter")

current_value = int(rs.TextDotText(obj))

print(current_value)

################################################################################
# Class to hold arguments
################################################################################
class TextDotArgs():

    # Initializer
    def __init__(self):
        self.Percentage = current_value

    # Validator
    def IsValid(self):
        if self.Percentage < 0 or self.Percentage > 30: return False

        return True

################################################################################
# Change TextDot value dialog class
################################################################################
class TextDotDialog(Dialog[bool]):

    # Initializer
    def __init__(self, args):
        self.Args = args
        # Initialize dialog box
        self.Title = 'SetTextDot'
        self.Padding = Padding(5)
        # Create layout
        layout = TableLayout()
        layout.Padding = Padding(5)
        layout.Spacing = Size(5, 5)
        layout.Rows.Add(self.CreateSteppers())
        layout.Rows.Add(None) # spacer
        layout.Rows.Add(None) # spacer
        layout.Rows.Add(self.CreateButtons())
        # Set the dialog content
        self.Content = layout


    # Creates numeric stepper controls
    def CreateSteppers(self):
        # Create labels
        label0 = Label(Text = 'New Value:')

        label2 = Label(Text = '({})'.format(self.Args.Percentage))

        # Create numeric steppers
        self.Percentage = NumericStepper(
            Value = self.Args.Percentage,
            MinValue = 0,
            MaxValue = 30
            )

        self.Percentage.ValueChanged += self.OnNumericStepperChanged

        # Create table layout
        layout = TableLayout()
        layout.Spacing = Size(5, 5)

        layout.Rows.Add(TableRow(label0, self.Percentage))

        return layout


    def OnNumericStepperChanged(self, sender, e):
        self.Args.Percentage = sender.Value
        #print(int(self.Args.Percentage))
        rs.TextDotText(obj, str(int(args.Percentage)))


    # OK button click handler
    def OnOkButtonClick(self, sender, e):
        # Harvest control values before closing
        self.Args.Percentage = self.Percentage.Value
        self.Close(True)


    # Cancel button click handler
    def OnCancelButtonClick(self, sender, e):
        self.Close(False)


    # Create button controls
    def CreateButtons(self):
        # Create the default button
        self.DefaultButton = Button(Text = 'OK')
        self.DefaultButton.Click += self.OnOkButtonClick
        # Create the abort button
        self.AbortButton = Button(Text = 'Cancel')
        self.AbortButton.Click += self.OnCancelButtonClick
        # Create button layout
        button_layout = TableLayout()
        button_layout.Spacing = Size(5, 5)
        button_layout.Rows.Add(TableRow(None, self.DefaultButton, self.AbortButton, None))
        return button_layout


args = TextDotArgs()

dlg = TextDotDialog(args)

rc = dlg.ShowModal(RhinoEtoApp.MainWindow)
2 Likes

This is cool! In the example video above are you using the trigger or the event handler method?

Hey Michael, I’m using the event handler.

1 Like