ETO Form Flow Layout

Hello,

I’m working on a user tagging capability that has an Eto Forms Dialog where the user can add additional “tags” to a Rhino object.

Currently I can only get this working in a StackLayout configuration but I’m trying to get it to first fill up with rows and when the tags exceed or get close to exceeding the width of the dialogue window, a new row should be created for the additional tags, for as many times as this step is necessary.

Here’s my dialog currently:

And I’m trying to get it to layout like this:

I believe I need to use DynamicLayout for this but I can’t seem to get it working regardless of the methods I use. I think I’m fundamentally not understanding how to “nest” multiple items per row or something along those lines.

Any help is greatly appreciated!

Here is my full working Python code thus far:

import Rhino.UI
import Eto.Drawing as drawing
import Eto.Forms as forms
import rhinoscriptsyntax as rs

class RoundedPanel(forms.Panel):
    def __init__(self):
        super(RoundedPanel, self).__init__()
        self.radius = 5

    def OnPaint(self, e):
        rect = drawing.RectangleF(0, 0, self.Width, self.Height)
        brush = drawing.SolidBrush(self.BackgroundColor)
        e.Graphics.FillRoundedRectangle(brush, rect, self.radius)
        brush.Dispose()

class SimpleEtoDialog(forms.Dialog):

    def __init__(self):
        Rhino.UI.EtoExtensions.UseRhinoStyle(self)
        self.Title = "Tag Object"
        self.ClientSize = drawing.Size(300, 200)
        self.Padding = drawing.Padding(5)
        self.Resizable = False

        # Initialize tag_text as an empty string
        self.tag_text = ""

        # Label
        self.label = forms.Label()
        self.labelBox = forms.TextBox(PlaceholderText="Enter tag here")  # Provide the placeholder text
        self.labelBox.Height = 25  # Adjust the height of the text box
        self.labelBox.KeyDown += self.OnTextBoxKeyDown

        # Content panel for text labels
        self.content_panel = forms.StackLayout()  # Use StackLayout instead of FlowLayout
        self.content_panel.Spacing = 5  # Set spacing between controls

        # Layout
        layout = forms.DynamicLayout()
        layout.AddRow(self.label)
        layout.AddRow(self.labelBox)
        layout.AddRow(self.content_panel)  # Add content_panel to the layout
        self.Content = layout

        # Populate existing tags
        self.populate_existing_tags()

    def populate_existing_tags(self):
        # Get selected objects
        selected_objs = rs.SelectedObjects()
        if selected_objs:
            # Iterate over selected objects
            for obj in selected_objs:
                existing_tags = rs.GetUserText(obj, "Tags")
                if existing_tags:
                    # Split existing tags and add them to the dialog
                    tags_list = existing_tags.split(",")
                    for tag in tags_list:
                        self.add_tag_label(tag.strip())

    def add_tag_label(self, tag_text):
        # Create a new panel for the text label with rounded corners and background color
        label_panel = RoundedPanel()
        label_panel.Padding = drawing.Padding(5)
        label_panel.BackgroundColor = drawing.Color(220,220,220)  # Set background color
        label_panel.Content = forms.Label(Text=tag_text)

        # Add the panel to the content panel
        self.content_panel.Items.Add(label_panel)

    def OnTextBoxKeyDown(self, sender, e):
        if e.Key == forms.Keys.Enter:
            # Get the text entered into the text box
            new_tag_text = self.labelBox.Text.strip()
            if new_tag_text:
                # Add the new tag to the dialog
                self.add_tag_label(new_tag_text)

                # Clear the text box
                self.labelBox.Text = ""

                # Append tags to selected objects
                selected_objs = rs.SelectedObjects()
                if selected_objs:
                    tags = [new_tag_text]
                    for obj in selected_objs:
                        append_tags(obj, tags)
                else:
                    print("No objects selected.")

def append_tags(obj, additional_values):
    existing_value = rs.GetUserText(obj, "Tags")
    if existing_value:
        existing_values = existing_value.lower().split(",")  # Convert existing values to lowercase and split them
        new_values = [val.lower() for val in additional_values if val.lower() not in existing_values]
        if new_values:
            new_value = ",".join(existing_values + new_values)
            rs.SetUserText(obj, "Tags", new_value)
            print("Tags appended successfully.")
        else:
            print("No new tags added. Tags already exist.")
    else:
        lowercased_values = [val.lower() for val in additional_values]
        rs.SetUserText(obj, "Tags", ",".join(lowercased_values))
        print("Tags set successfully.")

################################################################################
# Creating a dialog instance and displaying the dialog.
################################################################################
def TestSampleEtoDialog():
    dialog = SimpleEtoDialog()
    dialog.Icon = drawing.Icon(r"C:\Users\micha\OneDrive - Michael Vollrath\TOYBLOCK\PROJECTS\PROJECT_ENDGAME\ICONS\ICO_Tag_Object.ico")
    dialog.ShowModal(Rhino.UI.RhinoEtoApp.MainWindow)

################################################################################
# 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__":
    TestSampleEtoDialog()

This is my first attempt at Eto and any Rhino UI work in general, so far the API documentation has been very helpful, I’m just completely stuck on how to implement the layout here.

Thank you!

3 Likes

EDIT:

Updated code and dialogue in “action”

-added the ability to delete tags

I still can’t figure out how to get multiple tags per row though… @dale is something you can help me with? I’m trying to have multiple “tags” exist per row of the UI, when the row width will be exceeded, a new row would be created and filled with the remaining tags.

I tried a table layout but this didn’t seem to do the trick. I think Dynamic is the way to go but I just don’t understand how to make it work and am having a hard time finding example code on the dynamic layouts.

Video:

Code:

import Rhino.UI
import Eto.Drawing as drawing
import Eto.Forms as forms
import rhinoscriptsyntax as rs

class RoundedPanel(forms.Panel):
    def __init__(self):
        super(RoundedPanel, self).__init__()
        self.radius = 5

    def OnPaint(self, e):
        rect = drawing.RectangleF(0, 0, self.Width, self.Height)
        brush = drawing.SolidBrush(self.BackgroundColor)
        e.Graphics.FillRoundedRectangle(brush, rect, self.radius)
        brush.Dispose()

class SimpleEtoDialog(forms.Dialog):

    def __init__(self):
        Rhino.UI.EtoExtensions.UseRhinoStyle(self)
        self.Title = "Tag Object"
        self.ClientSize = drawing.Size(300, 200) #Fixed Sizing, Comment Out For Automatic Sizing
        self.Padding = drawing.Padding(5)
        self.Resizable = False

        # Initialize tag_text as an empty string
        self.tag_text = ""

        # TextBox
        self.labelBox = forms.TextBox(PlaceholderText="Enter tag here")  # Provide the placeholder text
        self.labelBox.Height = 25  # Adjust the height of the text box
        self.labelBox.KeyDown += self.OnTextBoxKeyDown

        # Content panel for buttons
        self.content_panel = forms.StackLayout()  # Use StackLayout instead of FlowLayout
        self.content_panel.Spacing = 5  # Set spacing between controls

        # Layout
        layout = forms.DynamicLayout()
        layout.AddRow(self.labelBox)
        scrollable_content_panel = forms.Scrollable()
        scrollable_content_panel.Content = self.content_panel
        layout.AddRow(scrollable_content_panel)  # Wrap content_panel in a Scrollable control

        self.Content = layout

        # Check if any objects are selected, if not, prompt the user to select an object
        selected_objs = rs.SelectedObjects()
        if not selected_objs:
            selected_objs = rs.GetObjects("Select object(s) to tag", preselect=True, select=True)
            if not selected_objs:
                return  # Exit if the user cancels object selection

        # Populate existing tags
        self.populate_existing_tags()

    def populate_existing_tags(self):
        # Get selected objects
        selected_objs = rs.SelectedObjects()
        if selected_objs:
            # Iterate over selected objects
            for obj in selected_objs:
                existing_tags = rs.GetUserText(obj, "Tags")
                if existing_tags:
                    # Split existing tags and add them to the dialog
                    tags_list = existing_tags.split(",")
                    for tag in tags_list:
                        self.add_tag_label(tag.strip())

    def add_tag_label(self, tag_text):

        button_color = drawing.Color(220,220,220)
        # Create a panel to contain the label and delete button with padding
        panel = forms.Panel(BackgroundColor=button_color, Padding=drawing.Padding(5))

        # Create a new label with rounded corners
        label = forms.Label(Text=tag_text, BackgroundColor=button_color)
        
        # Create a label with "X" text
        delete_label = forms.Button(Text="X", BackgroundColor=button_color, Width=25)
        delete_label.Click += lambda s, e: self.remove_tags(tag_text, panel)

        # Create a horizontal stack layout to contain the label and delete button
        horizontal_layout = forms.StackLayout()
        horizontal_layout.Orientation = forms.Orientation.Horizontal
        horizontal_layout.Spacing = 5
        horizontal_layout.Items.Add(label)
        horizontal_layout.Items.Add(delete_label)

        # Add the horizontal layout to the panel
        panel.Content = horizontal_layout

        # Add the panel to the content panel
        self.content_panel.Items.Add(panel)

    def remove_tags(self, tag_text, panel):
        print("Removing tag:", tag_text)

        # Remove the tag from the user text list
        selected_objs = rs.SelectedObjects()
        if selected_objs:
            for obj in selected_objs:
                existing_tags = rs.GetUserText(obj, "Tags")
                if existing_tags:
                    existing_tags = [tag.strip() for tag in existing_tags.split(",")]
                    if tag_text in existing_tags:
                        existing_tags.remove(tag_text)
                        rs.SetUserText(obj, "Tags", ",".join(existing_tags))

        # Remove the controls from the panel and update the UI
        panel.Content.RemoveAll()
        self.content_panel.Items.Remove(panel)

        # Update the layout to fill the space and adjust positions
        self.update_layout()

    def update_layout(self):
        # Clear the current layout
        self.content_panel.Items.Clear()

        # Populate existing tags again to re-add them to the layout
        self.populate_existing_tags()

    def OnTextBoxKeyDown(self, sender, e):
        if e.Key == forms.Keys.Enter:
            # Get the text entered into the text box
            new_tag_text = self.labelBox.Text.strip()
            if new_tag_text:
                # Add the new tag to the dialog
                self.add_tag_label(new_tag_text)

                # Clear the text box
                self.labelBox.Text = ""

                # Append tags to selected objects
                selected_objs = rs.SelectedObjects()
                if selected_objs:
                    tags = [new_tag_text]
                    for obj in selected_objs:
                        append_tags(obj, tags)
                else:
                    print("No objects selected.")

def append_tags(obj, additional_values):
    existing_value = rs.GetUserText(obj, "Tags")
    if existing_value:
        existing_values = existing_value.lower().split(",")  # Convert existing values to lowercase and split them
        new_values = [val.lower() for val in additional_values if val.lower() not in existing_values]
        if new_values:
            new_value = ",".join(existing_values + new_values)
            rs.SetUserText(obj, "Tags", new_value)
            print("Tags appended successfully.")
        else:
            print("No new tags added. Tags already exist.")
    else:
        lowercased_values = [val.lower() for val in additional_values]
        rs.SetUserText(obj, "Tags", ",".join(lowercased_values))
        print("Tags set successfully.")

################################################################################
# Creating a dialog instance and displaying the dialog.
################################################################################
def TestSampleEtoDialog():
    dialog = SimpleEtoDialog()
    dialog.Icon = drawing.Icon(r"C:\Users\micha\OneDrive - Michael Vollrath\TOYBLOCK\PROJECTS\PROJECT_ENDGAME\ICONS\ICO_Tag_Object.ico")
    dialog.ShowModal(Rhino.UI.RhinoEtoApp.MainWindow)

################################################################################
# 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__":
    TestSampleEtoDialog()
1 Like

Hi @michaelvollrath,

i’ve tried something hacky using DynamicLayout: FakeFlowLayoutPanel.py (2.4 KB)

Unfortunately i could not find an equivalent to System.Windows.Forms.FlowLayoutPanel in Eto.

_
c.

1 Like

Thanks for looking into this @clement ! I’ll take a look at your code and report back, thanks for sharing!

Flow layout was a term I came across as well but couldn’t find anything in Eto relating to it.

I love a good hack though, haha I’ll check it out tomorrow, thanks!

1 Like

Nice script, I played with it a bit, and found it more convenient to have the crosses to the left of the words and make the size autosize:

image
tag_autosize.py (7.3 KB)

1 Like

@clement awesome! just tested and this “hacked” layout works great! I’ll try and implement with my code and post the results! I appreciate you helping out!

Thanks @Gijs ! I agree that since the main function of the tag once populated is to be able to remove it if needed, having the x more accessible makes sense. I think I’m actually going to try a version where the entire tag & X is a button so you can click anywhere for convenience to have the tag removed.

Perhaps some kind of mouse hover event to highlight the tag slightly in red or change the color of the X to red to signify to the user that you’re about to delete the tag would be helpful?

I really appreciate the explorations and help! Thanks guys!

EDIT:

Do either of you all know how to set the the border / border color of the Eto Scrollable?
I found two things in digging:
Eto.Drawing.Colors.Transparent and Scrollable.Border property BorderType=2 (None) but couldn’t figure out how to expose the scrollable border to implement either of those.

Thanks!

Hi Michael, it is again a bit of a hack but works for me under Windows:

import clr
import Eto
import System

clr.AddReference("PresentationCore")

# create Scrollable with a border
scrollable = Eto.Forms.Scrollable()
scrollable.Border = Eto.Forms.BorderType.Line

# create new brush with desired color
color = System.Windows.Media.Color.FromArgb(255, 250, 4, 8)
brush = System.Windows.Media.SolidColorBrush(color)
scrollable.ControlObject.BorderBrush = brush

# check assignment
print scrollable.ControlObject.BorderBrush.A
print scrollable.ControlObject.BorderBrush.R
print scrollable.ControlObject.BorderBrush.G
print scrollable.ControlObject.BorderBrush.B

i doubt this works on Mac though.

_
c.

Michael,

Sorry I wasn’t able to figure this one out completely. I’m completely new to ETO and Python, been .Net forever…But I spent 3 hours this morning, learning both ETO and Python.

I was able to get a Dialog with a Fixed Width that would scroll vertical and wrap horizontally, however I couldn’t finish the logic, without rewriting all of the python methods.

I’m used to being able to pass values by reference in c#, but that was my challenge in python and the code posted here.

The issue was you needed to track the number of items, and then set a value for your wrapping that matches a decent Width for the dialog, I was using 200 px. And then I was wrapping every 3 tags that were in the string collection.

To do this I was using a TableLayout() for the main outer content panel, instead of the Stack/Dynamic layout. Then I would pass in the item count to the Add/Update/Delete methods, and use the item count to determine if the new item should be in a new ROW and CELL, or just a CELL.

I would pass the item count into the methods, and return the item count back out at the end, in order to simulate a by reference passing. There is probably a better way, but like I said that was my first python script, and first time using the in app editor, which was cool.

image

image

Good luck, I wish I had more time to get this all working, but I thought I would post my partial thoughts in case it helps any.

Thank you @jstevenson, I’ll have to digest this and compare it against the dynamic layout method but it seems promising and thank you for taking the time!

I wish I could hop into Python and Eto within 3 hours haha! It takes me 3 hours to make a "
small change"

I was starting to use C# but I find the way my brain works is Visual Prototype->Replace With Python->Replace With C# for performance.

Though most of my python scripts are simple enough to not need C#.

Anyways,

Here’s the in progress update merging with @clement’s code from earlier.

-My row/button sizing is a little bugged still and the last entry in each row is overlapping the dialog window slightly so I need to figure out why that’s happening…
-Now the whole tag in the UI is a button so you can click anywhere lazily on it to remove it
-I need to embed the icon instead of linking to a file (on my computer)
-Dialog is resizable, and updates button positions accordingly, yay!

-some style changes are needed
-I need to reimplement the scrollable container

What it looks like currently:

Working Code:

import System
import Rhino
import rhinoscriptsyntax as rs
import Eto
import Eto.Drawing as drawing

from System.Collections.Generic import List

class MyDialog(Eto.Forms.Dialog[bool]):
    def __init__(self):
        
        Rhino.UI.EtoExtensions.UseRhinoStyle(self)
        self.Title = "Tag Object"
        self.Size = Eto.Drawing.Size(300, 200)
        self.Padding = Eto.Drawing.Padding(10)
        self.Resizable = True
        self.Location = Eto.Drawing.Point(220,220)
        self.Font = Eto.Drawing.SystemFonts.Default()
        
        
        self.layout = Eto.Forms.DynamicLayout()
        self.layout.DefaultSpacing = Eto.Drawing.Size(4,4) # propagates !

        # Create TextBox For Tag Entries
        self.labelBox = Eto.Forms.TextBox(PlaceholderText="Add tag here")  # Provide the placeholder text
        self.labelBox.Height = 25  # Adjust the height of the text box
        self.labelBox.KeyDown += self.OnTextBoxKeyDown
        
        
        self.UpdateDynamicLayout()
        
        self.Content = self.layout
        
        
        self.SizeChanged += self.MyOnSizeChangedEvent

        # Check if any objects are selected, if not, prompt the user to select an object
        selected_objs = rs.SelectedObjects()
        if not selected_objs:
            selected_objs = rs.GetObjects("Select object(s) to tag", preselect=True, select=True)
            if not selected_objs:
                return  # Exit if the user cancels object selection
        
        # Populate existing tags
        self.UpdateDynamicLayout()
    
    

    def MyOnSizeChangedEvent(self, sender, e):
        try:
            self.UpdateDynamicLayout()
        except Exception as ex:
            print ex
            return

    def UpdateDynamicLayout(self):
        # Get selected objects
        selected_objs = rs.SelectedObjects()
        if selected_objs:
            # Clear the layout
            self.layout.Clear()

            controls = List[Eto.Forms.Control]()
            row_width = 0
            
            # Create a new row for the text box
            self.layout.BeginHorizontal()
            self.layout.Add(self.labelBox)
            self.layout.EndHorizontal()
            
            # Initialize tags_list
            tags_list = []
            
            # Iterate over selected objects
            for obj in selected_objs:
                existing_tags = rs.GetUserText(obj, "Tags")
                if existing_tags:
                    # Split existing tags and add them to the tags_list variable
                    tags_list.extend(existing_tags.split(","))
        
            # Iterate over existing tags
            for tag in tags_list: 
                delete_button_text = (tag +  str("   X"))
                delete_button_width = Eto.Drawing.Font.MeasureString(self.Font, delete_button_text).Width + 30
                delete_button = Eto.Forms.Button(Text=delete_button_text, BackgroundColor=Eto.Drawing.Colors.DimGray, Width=delete_button_width)
                delete_button.BackgroundColor = Eto.Drawing.Colors.Transparent
                delete_button.Tag = tag  # Set the tag text as Tag for the button
                delete_button.Click += self.OnDeleteButtonClick
                
                if row_width < self.layout.Width:
                    controls.Add(delete_button)
                    row_width += delete_button_width
                else:
                    controls.Add(None)
                    self.layout.AddSeparateRow(System.Array[Eto.Forms.Control](controls))
                    controls.Clear()
                    controls.Add(delete_button)
                    row_width = delete_button_width
            
            if controls.Count != 0:
                controls.Add(None)
                self.layout.AddSeparateRow(System.Array[Eto.Forms.Control](controls))
            
            self.layout.AddSpace()
            
            self.layout.Create()

    def append_tags(self, obj, additional_values):
        existing_value = rs.GetUserText(obj, "Tags")
        if existing_value:
            existing_values = existing_value.lower().split(",")  # Convert existing values to lowercase and split them
            new_values = [val.lower() for val in additional_values if val.lower() not in existing_values]
            if new_values:
                new_value = ",".join(existing_values + new_values)
                rs.SetUserText(obj, "Tags", new_value)
                print("Tags appended successfully.")
        else:
            lowercased_values = [val.lower() for val in additional_values]
            rs.SetUserText(obj, "Tags", ",".join(lowercased_values))
            print("Tags set successfully.") 

        # Update the user text immediately
        rs.ObjectName(obj, rs.GetUserText(obj, "Tags"))


    def remove_tags(self, tag_text):
        print("Removing tag:", tag_text)

        # Remove the tag from the user text list
        selected_objs = rs.SelectedObjects()
        if selected_objs:
            for obj in selected_objs:
                existing_tags = rs.GetUserText(obj, "Tags")
                if existing_tags:
                    existing_tags = [tag.strip() for tag in existing_tags.split(",")]
                    if tag_text in existing_tags:
                        existing_tags.remove(tag_text)
                        rs.SetUserText(obj, "Tags", ",".join(existing_tags))
        
        # Update the user text immediately
        rs.ObjectName(obj, rs.GetUserText(obj, "Tags"))

    def OnDeleteButtonClick(self, sender, e):
        # Get the tag text from the button's Tag property
        tag_text = sender.Tag
        if tag_text:
            # Remove the tag from the UI
            self.remove_tags(tag_text)
            # Update the layout to reflect the changes
            self.UpdateDynamicLayout()
            

    def OnTextBoxKeyDown(self, sender, e):
        if e.Key == Eto.Forms.Keys.Enter:
            # Get the text entered into the text box
            new_tag_text = self.labelBox.Text.strip()
            if new_tag_text:
                # Append tags to selected objects
                selected_objs = rs.SelectedObjects()
                if selected_objs:
                    tags = [new_tag_text]
                    for obj in selected_objs:
                        self.append_tags(obj, tags)  # Corrected here
                else:
                    print("No objects selected.")
                
                # Add the new tag to the dialog
                self.UpdateDynamicLayout()

                # Clear the text box
                self.labelBox.Text = ""


            

def EstablishDialog():
        
    dialog = MyDialog()
    icon_path = r"C:\Users\micha\OneDrive - Michael Vollrath\TOYBLOCK\PROJECTS\PROJECT_ENDGAME\ICONS\ICO_Tag_Object.ico"
    dialog.Icon = drawing.Icon(icon_path)
    dialog.ShowModal(Rhino.UI.RhinoEtoApp.MainWindow)

EstablishDialog()

2 Likes

Hi @michaelvollrath,

i think that happens if the row_width sum is not correct (too small). If you print delete_button_width to the terminal and compare the values per button with a screenshot of the dialog, you’ll measure that the button widths are correct as you’ve set them. But you need to add the space between the buttons to the sum as well. If i change it a bit like below it behaves slightly better:

if row_width < self.layout.Width:
    controls.Add(delete_button)
    row_width += delete_button_width + 30 # space
else:
    controls.Add(None)
    self.layout.AddSeparateRow(System.Array[Eto.Forms.Control](controls))
    controls.Clear()
    controls.Add(delete_button)
    row_width = delete_button_width + 30 # space

omg, it gets even more hacky…

Maybe you can convert it to a base64 string and then use it as a Eto.Drawing.Bitmap

Thats cool ! You might only update the dynamic layout and keep the textbox unaffected.

_
c.

Hmm I had trouble implementing this for some reason. Here’s and updated version that is almost there except the last item per the list is filling the whole row UI space for some reason:

Code:

import System
import Rhino
import rhinoscriptsyntax as rs
import Eto
import Eto.Drawing as drawing

from System.Collections.Generic import List

class MyDialog(Eto.Forms.Dialog[bool]):
    def __init__(self):
        
        Rhino.UI.EtoExtensions.UseRhinoStyle(self)
        self.Title = "Tag Object"
        self.Size = Eto.Drawing.Size(300, 200)
        self.Padding = Eto.Drawing.Padding(10)
        self.Resizable = True
        self.Location = Eto.Drawing.Point(220,220)
        self.Font = Eto.Drawing.SystemFonts.Default()
        
        
        self.layout = Eto.Forms.DynamicLayout()
        self.layout.DefaultSpacing = Eto.Drawing.Size(4,4) # propagates !

        # Create TextBox For Tag Entries
        self.labelBox = Eto.Forms.TextBox(PlaceholderText="Add tag here")  # Provide the placeholder text
        self.labelBox.Height = 25  # Adjust the height of the text box
        self.labelBox.KeyDown += self.OnTextBoxKeyDown
        
        
        self.UpdateDynamicLayout()
        
        self.Content = self.layout
        
        
        self.SizeChanged += self.MyOnSizeChangedEvent

        # Check if any objects are selected, if not, prompt the user to select an object
        selected_objs = rs.SelectedObjects()
        if not selected_objs:
            selected_objs = rs.GetObjects("Select object(s) to tag", preselect=True, select=True)
            if not selected_objs:
                return  # Exit if the user cancels object selection
        
        # Populate existing tags
        self.UpdateDynamicLayout()
    
    

    def MyOnSizeChangedEvent(self, sender, e):
        try:
            self.UpdateDynamicLayout()
        except Exception as ex:
            print ex
            return

    def UpdateDynamicLayout(self):
        # Get selected objects
        selected_objs = rs.SelectedObjects()
        if selected_objs:
            # Clear the layout
            self.layout.Clear()

            controls = List[Eto.Forms.Control]()
            row_controls = []
            row_width = 0
            # button_width = 120  # Fixed button width
            max_row_width = self.layout.Width  # Maximum row width based on layout width
            
            # Create a new row for the text box
            self.layout.BeginHorizontal()
            self.layout.Add(self.labelBox)
            self.layout.EndHorizontal()
            
            # Initialize tags_list
            tags_list = []
            
            # Iterate over selected objects
            for obj in selected_objs:
                existing_tags = rs.GetUserText(obj, "Tags")
                if existing_tags:
                    # Split existing tags and add them to the tags_list variable
                    tags_list.extend(existing_tags.split(","))
        
            # Iterate over existing tags
            for tag in tags_list: 
                delete_button_text = (tag +  str("   X"))
                delete_button_width = Eto.Drawing.Font.MeasureString(self.Font, delete_button_text).Width + 30
                delete_button = Eto.Forms.Button(Text=delete_button_text, BackgroundColor=Eto.Drawing.Colors.DimGray, Width=delete_button_width)
                delete_button.BackgroundColor = Eto.Drawing.Colors.Transparent
                delete_button.Tag = tag  # Set the tag text as Tag for the button
                delete_button.Click += self.OnDeleteButtonClick
                
                if row_width + delete_button_width > max_row_width:  # Check if adding the next control exceeds the width
                    row_controls.append(None)  # Add a placeholder control for spacing
                    controls.AddRange(row_controls)  # Add row controls to the main controls list
                    self.layout.AddSeparateRow(System.Array[Eto.Forms.Control](controls))  # Add controls as a separate row
                    controls.Clear()  # Clear the controls list for the next row
                    # Clear the row controls list for the next row
                    row_controls = []
                    row_width = 0  # Reset row width
                
                row_controls.append(delete_button)  # Add the button to the row controls
                row_width += delete_button_width  # Update the row width
            
            if row_controls:  # Add the remaining row controls if any
                controls.AddRange(row_controls)
                self.layout.AddSeparateRow(System.Array[Eto.Forms.Control](controls))
            
            self.layout.AddSpace()
            
            self.layout.Create()


    def append_tags(self, obj, additional_values):
        existing_value = rs.GetUserText(obj, "Tags")
        if existing_value:
            existing_values = existing_value.lower().split(",")  # Convert existing values to lowercase and split them
            new_values = [val.lower() for val in additional_values if val.lower() not in existing_values]
            if new_values:
                new_value = ",".join(existing_values + new_values)
                rs.SetUserText(obj, "Tags", new_value)
                print("Tags appended successfully.")
        else:
            lowercased_values = [val.lower() for val in additional_values]
            rs.SetUserText(obj, "Tags", ",".join(lowercased_values))
            print("Tags set successfully.") 

        # Update the user text immediately
        rs.ObjectName(obj, rs.GetUserText(obj, "Tags"))


    def remove_tags(self, tag_text):
        print("Removing tag:", tag_text)

        # Remove the tag from the user text list
        selected_objs = rs.SelectedObjects()
        if selected_objs:
            for obj in selected_objs:
                existing_tags = rs.GetUserText(obj, "Tags")
                if existing_tags:
                    existing_tags = [tag.strip() for tag in existing_tags.split(",")]
                    if tag_text in existing_tags:
                        existing_tags.remove(tag_text)
                        rs.SetUserText(obj, "Tags", ",".join(existing_tags))
        
        # Update the user text immediately
        rs.ObjectName(obj, rs.GetUserText(obj, "Tags"))

    def OnDeleteButtonClick(self, sender, e):
        # Get the tag text from the button's Tag property
        tag_text = sender.Tag
        if tag_text:
            # Remove the tag from the UI
            self.remove_tags(tag_text)
            # Update the layout to reflect the changes
            self.UpdateDynamicLayout()
            

    def OnTextBoxKeyDown(self, sender, e):
        if e.Key == Eto.Forms.Keys.Enter:
            # Get the text entered into the text box
            new_tag_text = self.labelBox.Text.strip()
            if new_tag_text:
                # Append tags to selected objects
                selected_objs = rs.SelectedObjects()
                if selected_objs:
                    tags = [new_tag_text]
                    for obj in selected_objs:
                        self.append_tags(obj, tags)  # Corrected here
                else:
                    print("No objects selected.")
                
                # Add the new tag to the dialog
                self.UpdateDynamicLayout()

                # Clear the text box
                self.labelBox.Text = ""


            

def EstablishDialog():
        
    dialog = MyDialog()
    icon_path = r"C:\Users\micha\OneDrive - Michael Vollrath\TOYBLOCK\PROJECTS\PROJECT_ENDGAME\ICONS\ICO_Tag_Object.ico"
    dialog.Icon = drawing.Icon(icon_path)
    dialog.ShowModal(Rhino.UI.RhinoEtoApp.MainWindow)

EstablishDialog()

Looks fine here:

Until you stretch the window:

That’s exactly the direction I was thinking with that, thanks for the suggestion!

When you see keep the text box unaffected, meaning don’t fill the whole UI width with the text box?

Yea that probably makes sense!

Just add a None at the end:

if row_controls:  # Add the remaining row controls if any
    controls.AddRange(row_controls)
    controls.Add(None) # Add an empty item which gets scaled
    self.layout.AddSeparateRow(System.Array[Eto.Forms.Control](controls))

No, i’ve meant that you put the DynamicLayout into a Scrollable. Then only update the DynamicLayout when required. Do not put the text box in that DynamicLayout.

Create a StackLayout with 2 sections, the top section remains unscalable and contains your text box, the bottom section contains the Scrollable (With the DynamicLayout) and is allowed to be stretched. If you then enable self.AutoSize=True and remove the fixed height, the dialog (it’s bottom section) should grow in size when you add enough tag buttons.

_
c.

lol that works great and I would have NEVER thought of that :upside_down_face:

I tried to nest the dynamic layout into the scrollable but I was getting errors about child controls and it got over my head trying to figure out where my nesting was off.

I’ll try and revisit with fresh eyes

1 Like

That UpdateDynamicLayout() method is much more clever than what I was hacking together.

I have a dumb question, when I try to run the latest code you guys have here in the ScriptEditor, I get tons of issues, like ETO doesn’t like the constructors you guys are using…

Seems like I have to create the ETO objects with an empty constructor and change it’s properties rather than using the syntax you guys have here.

Any clues why?

@jstevenson Are you using IronPython2 or Python3?

1 Like

I was using Python 3, maybe that is the issue, I didn’t even try it as Python 2, but that would make sense…

1 Like

@jstevenson I believe that is the issue yes, I was initially building it in Python3 and had lots of errors with Eto so I went back to IP2 for now.

1 Like

Hi all,

Just sharing some updated code on this effort.

-I added the ability to filter tags using a search box (handy with objects with LOTS of tags)
-You can enter multiple tags in one go by using commas between like “my tag,dog,purple” becomes “my tag” “dog” “purple”

I’m pretty happy with the resizing and updating of the layout.

I can’t seem to wrap the dynamic layout in a scrollable properly. I would love any pointers on this.

Updates I want to make:

  1. wrap the dynamic tag layout in a scrollable for vertical scrolling
  2. potentially combine the search box and add tag box. If a tag can’t be found in a search, entering said value will create a new tag for it (maybe?) maybe that’s a bad idea. I just don’t like the double text bar for search and Add Tag. Maybe just need to work on the UI a bit for that. I’m open to any suggestions of course!
  3. Preferably I would like the Tag buttons and the Add Tag box to have rounded corners like the Search Tags dialog though I think that requires Eto Drawable code and there’s not a default setting for the borders to get a radius is there? I couldn’t find anything in the Eto documentation about radius corners

Thank you all!

All Tags Showing:

Filtered Tags Only:

Full Code:

import System
import Rhino
import rhinoscriptsyntax as rs

import Eto
import Eto.Drawing as drawing

from System.Collections.Generic import List

class MyDialog(Eto.Forms.Dialog[bool]):
    def __init__(self):
        
        Rhino.UI.EtoExtensions.UseRhinoStyle(self)
        self.Title = "Tag Object"
        self.Size = Eto.Drawing.Size(300, 200)
        self.Padding = Eto.Drawing.Padding(10)
        self.Resizable = True
        self.Font = Eto.Drawing.SystemFonts.Default()
        
        self.search_text = ""  # Store the search text
        self.is_updating_layout = False  # Flag to indicate whether layout update is in progress

        self.searchBox = Eto.Forms.SearchBox()  # Define searchBox here
        self.searchBox.TextChanged += self.OnSearchBoxTextChanged  # Handle text changed event
        self.searchBox.KeyDown += self.OnSearchBoxKeyDown  # Handle key down event

        self.layout = Eto.Forms.DynamicLayout()
        self.layout.DefaultSpacing = Eto.Drawing.Size(4,4) # propagates !

        # Create TextBox For Tag Entries
        self.labelBox = Eto.Forms.TextBox(PlaceholderText="Add tag here...",ShowBorder=False)  # Provide the placeholder text
        self.labelBox.Height = 25  # Adjust the height of the text box
        self.labelBox.KeyDown += self.OnTextBoxKeyDown
        
        # Timer for delayed UI update
        self.update_timer = None
        self.update_delay = 0.5  # Adjust the delay time as needed
        
        self.Content = self.layout
        
        self.SizeChanged += self.MyOnSizeChangedEvent

        # Check if any objects are selected, if not, prompt the user to select an object
        selected_objs = rs.SelectedObjects()
        if not selected_objs:
            selected_objs = rs.GetObjects("Select object(s) to tag", preselect=True, select=True)
            if not selected_objs:
                return  # Exit if the user cancels object selection
        

    def MyOnSizeChangedEvent(self, sender, e):
        try:
            self.UpdateDynamicLayout()
        except Exception as ex:
            print ex
            return

    def UpdateDynamicLayout(self):
        # print("Updating dynamic layout...")

        # Store the search text before clearing the layout
        self.search_text = self.searchBox.Text.strip().lower()

        # Clear the layout
        self.layout.Clear()

        # print("Layout cleared.")

        controls = List[Eto.Forms.Control]()
        row_controls = []
        row_width = 0
        # button_width = 120  # Fixed button width
        max_row_width = self.layout.Width  # Maximum row width based on layout width
        
        # Create a new row for the search box
        self.layout.BeginHorizontal()
        self.searchBox.PlaceholderText = "Search tags..."
        self.layout.Add(self.searchBox, True)  # Add search box with horizontal expansion
        self.layout.EndHorizontal()

        # Create a new row for the text box
        self.layout.BeginHorizontal()
        self.labelBox.PlaceholderText = "Add tag here..."
        self.labelBox.ShowBorder = False
        self.labelBox.Height = 25  # Adjust the height of the text box
        self.layout.Add(self.labelBox, True)  # Add text box with horizontal expansion
        self.layout.EndHorizontal()
        
        # Get selected objects
        selected_objs = rs.SelectedObjects()
        # print("Selected objects:", selected_objs)
        
        if selected_objs:
            # Initialize unique tags set
            unique_tags = set()
            
            # Collect all tags from all selected objects
            for obj in selected_objs:
                existing_tags = rs.GetUserText(obj, "Tags")
                if existing_tags:
                    # Split existing tags and add them to the unique_tags set
                    unique_tags.update(existing_tags.split(","))
        
           # Add newly added or appended tags from the text box if they are not already in unique_tags
            new_tag_text = self.labelBox.Text.strip()
            # Check if the new tag contains commas
            if new_tag_text and ',' not in new_tag_text:
                # Only add the new tag if it's not already in unique_tags
                if new_tag_text not in unique_tags:
                    unique_tags.add(new_tag_text)
            # print("Unique tags:", unique_tags)
            
            # Filter tags based on the search text if it's not empty
            search_text = self.search_text
            # print("Search text:", search_text)
            if search_text:
                filtered_tags = [tag for tag in unique_tags if search_text in tag.lower()]
                # print("Filtered tags:", filtered_tags)
            else:
                filtered_tags = list(unique_tags)
                # print("No search text. Showing all tags.")
            
            # Iterate over filtered tags and populate the UI
            for tag in filtered_tags: 
                delete_button_text = (tag +  str("   X"))
                delete_button_width = Eto.Drawing.Font.MeasureString(self.Font, delete_button_text).Width + 30
                delete_button = Eto.Forms.Button(Text=delete_button_text, BackgroundColor=Eto.Drawing.Colors.DimGray, Width=delete_button_width)
                delete_button.BackgroundColor = Eto.Drawing.Colors.Transparent
                delete_button.Tag = tag  # Set the tag text as Tag for the button
                delete_button.Click += self.OnDeleteButtonClick
                
                if row_width + delete_button_width > max_row_width:  # Check if adding the next control exceeds the width
                    row_controls.append(None)  # Add a placeholder control for spacing
                    controls.AddRange(row_controls)  # Add row controls to the main controls list
                    self.layout.AddSeparateRow(System.Array[Eto.Forms.Control](controls))  # Add controls as a separate row
                    controls.Clear()  # Clear the controls list for the next row
                    # Clear the row controls list for the next row
                    row_controls = []
                    row_width = 0  # Reset row width
                
                row_controls.append(delete_button)  # Add the button to the row controls
                row_width += delete_button_width  # Update the row width
            
            if row_controls:  # Add the remaining row controls if any
                controls.AddRange(row_controls)
                controls.Add(None) # Add an empty item which gets scaled
                self.layout.AddSeparateRow(System.Array[Eto.Forms.Control](controls))
        
        self.layout.AddSpace()
        
        self.layout.Create()

    def OnSearchBoxKeyDown(self, sender, e):
        # Check if Enter key is pressed
        if e.Key == Eto.Forms.Keys.Enter:
            # Call the layout update function
            self.UpdateDynamicLayout()

    def OnSearchBoxTextChanged(self, sender, e):
        # Check if the search box text is empty and it's not already in the process of updating
        if not sender.Text.strip() and not self.is_updating_layout:
            # Set the flag to indicate that the layout is being updated
            self.is_updating_layout = True
            # If the search box is empty, update the layout to show all tags
            self.UpdateDynamicLayout()
            # Reset the flag after the layout update is complete
            self.is_updating_layout = False

    def append_tags(self, objs, additional_values):
        appended_count = 0
        existing_count = 0

        # Ensure objs is a list, even if it contains only one object
        if not isinstance(objs, list):
            objs = [objs]

        for obj in objs:
            existing_value = rs.GetUserText(obj, "Tags")
            if existing_value:
                existing_values = existing_value.lower().split(",")  # Convert existing values to lowercase and split them
                new_values = [val.lower() for val in additional_values if val.lower() not in existing_values]
                if new_values:
                    new_value = ",".join(existing_values + new_values)
                    rs.SetUserText(obj, "Tags", new_value)
                    appended_count += 1
                else:
                    existing_count += 1  # Increment existing count if tag already exists for this object
            else:
                lowercased_values = [val.lower() for val in additional_values]
                rs.SetUserText(obj, "Tags", ",".join(lowercased_values))
                appended_count += 1
        
        # Print concise statement
        if appended_count > 0 or existing_count > 0:
            message = ""
            if appended_count > 0:
                message += "Tag added to {} objects".format(appended_count)
            if existing_count > 0:
                if message:
                    message += ", "
                message += "Tag already exists in {} objects".format(existing_count)
            print(message)


        else:
            lowercased_values = [val.lower() for val in additional_values]
            rs.SetUserText(obj, "Tags", ",".join(lowercased_values))
            print("Tags set successfully.") 

        # Update the user text immediately
        rs.ObjectName(obj, rs.GetUserText(obj, "Tags"))


    def remove_tags(self, tag_text):
        print('"' + str(tag_text) + '"' + " tag removed successfully.")

        # Remove the tag from the user text list
        selected_objs = rs.SelectedObjects()
        if selected_objs:
            for obj in selected_objs:
                existing_tags = rs.GetUserText(obj, "Tags")
                if existing_tags:
                    existing_tags = [tag.strip() for tag in existing_tags.split(",")]
                    if tag_text in existing_tags:
                        existing_tags.remove(tag_text)
                        rs.SetUserText(obj, "Tags", ",".join(existing_tags))
        
        # Update the user text immediately
        rs.ObjectName(obj, rs.GetUserText(obj, "Tags"))

    def OnDeleteButtonClick(self, sender, e):
        # Get the tag text from the button's Tag property
        tag_text = sender.Tag
        if tag_text:
            # Remove the tag from the UI
            self.remove_tags(tag_text)
            # Update the layout to reflect the changes
            self.UpdateDynamicLayout()
            
    def OnTextBoxKeyDown(self, sender, e):
        if e.Key == Eto.Forms.Keys.Enter:
            # Get the text entered into the text box and split it by commas
            tag_texts = self.labelBox.Text.strip().split(",")
            for tag_text in tag_texts:
                tag_text = tag_text.strip()  # Remove leading and trailing spaces
                if tag_text:
                    # Append tags to selected objects
                    selected_objs = rs.SelectedObjects()
                    if selected_objs:
                        tags = [tag_text]
                        self.append_tags(selected_objs, tags)  # Pass selected_objs as a list
                        # Update the UI tag list
                        self.UpdateDynamicLayout()
                    else:
                        print("No objects selected.")
            # Clear the text box
            self.labelBox.Text = ""


def EstablishDialog():
        
    dialog = MyDialog()
    icon_path = r"C:\Users\micha\OneDrive - Michael Vollrath\TOYBLOCK\PROJECTS\PROJECT_ENDGAME\ICONS\ICO_Tag_Object.ico"
    dialog.Icon = drawing.Icon(icon_path)
    dialog.ShowModal(Rhino.UI.RhinoEtoApp.MainWindow)

EstablishDialog()

6 Likes

Michael,

Looks like you are well on your way, looking good. So I thought I would give you a couple tweaks that I worked through with my morning coffee.

  • I created a base64 string to hold the icon image, so users won’t need to have that icon on disk. See the GetIconImagePath() method. You can use this website to upload your image, and it will convert it to a base64 string you can replace the value in that method to use your image. NOTE: The string currently in the code there is only a partial because of limitations to this forum post size. You will need to replace it to see the image.
    https://www.base64-image.de/

  • I wrapped everything in a scrollable, and modified the UpdateDynamicLayout() logic to use the visible scrollable width to determine if a new row is needed.

#! python 2

import math
import System.Collections.Generic
import System
import Rhino
import rhinoscriptsyntax as rs
import Eto
import Eto.Drawing as drawing

import base64
import tempfile

from System.Collections.Generic import List

class MyDialog(Eto.Forms.Dialog[bool]):
    def __init__(self):
        
        Rhino.UI.EtoExtensions.UseRhinoStyle(self)
        self.Title = "Tag Object"
        self.Size = Eto.Drawing.Size(300, 200)
        self.Padding = Eto.Drawing.Padding(10)
        self.Resizable = True
        self.Font = Eto.Drawing.SystemFonts.Default()
        
        self.search_text = ""  # Store the search text
        self.is_updating_layout = False  # Flag to indicate whether layout update is in progress

        self.searchBox = Eto.Forms.SearchBox()  # Define searchBox here
        self.searchBox.TextChanged += self.OnSearchBoxTextChanged  # Handle text changed event
        self.searchBox.KeyDown += self.OnSearchBoxKeyDown  # Handle key down event

        self.layout = Eto.Forms.DynamicLayout()
        self.scrollcontent = Eto.Forms.DynamicLayout()
        self.layout.DefaultSpacing = Eto.Drawing.Size(4,4) # propagates !

        # Create TextBox For Tag Entries
        self.labelBox = Eto.Forms.TextBox(PlaceholderText="Add tag here...",ShowBorder=False)  # Provide the placeholder text
        self.labelBox.Height = 25  # Adjust the height of the text box
        self.labelBox.KeyDown += self.OnTextBoxKeyDown
        
        # Timer for delayed UI update
        self.update_timer = None
        self.update_delay = 0.5  # Adjust the delay time as needed
        
        # Scrollable Content Area
        scrollable_content_panel = Eto.Forms.Scrollable()
        scrollable_content_panel.ExpandContentHeight = True
        scrollable_content_panel.ExpandContentWidth = True       
        scrollable_content_panel.Border = Eto.Forms.BorderType.None
        scrollable_content_panel.Content = self.layout

        self.Content = scrollable_content_panel
        
        self.SizeChanged += self.MyOnSizeChangedEvent

        # Check if any objects are selected, if not, prompt the user to select an object
        selected_objs = rs.SelectedObjects()
        if not selected_objs:
            selected_objs = rs.GetObjects("Select object(s) to tag", preselect=True, select=True)
            if not selected_objs:
                return  # Exit if the user cancels object selection
        

    def MyOnSizeChangedEvent(self, sender, e):
        try:
            self.UpdateDynamicLayout()
        except Exception as ex:
            print ex
            return

    def UpdateDynamicLayout(self):
        # print("Updating dynamic layout...")

        # Store the search text before clearing the layout
        self.search_text = self.searchBox.Text.strip().lower()

        # Clear the layout
        self.layout.Clear()
        #print("Layout cleared.")

        controls = List[Eto.Forms.Control]()
        row_controls = []
        row_width = 0
        max_row_width = (self.Content.VisibleRect.Width - 4) # Maximum row width based on Visible Scrollable Width - Fudge Factor (4px)
        
        # Create a new row for the search box
        self.layout.BeginHorizontal()
        self.searchBox.PlaceholderText = "Search tags..."
        self.layout.Add(self.searchBox, True)  # Add search box with horizontal expansion
        self.layout.EndHorizontal()

        # Create a new row for the text box
        self.layout.BeginHorizontal()
        self.labelBox.PlaceholderText = "Add tag here..."
        self.labelBox.ShowBorder = False
        self.labelBox.Height = 25  # Adjust the height of the text box
        self.layout.Add(self.labelBox, True)  # Add text box with horizontal expansion
        self.layout.EndHorizontal()
        
        # Get selected objects
        selected_objs = rs.SelectedObjects()
        # print("Selected objects:", selected_objs)
        
        if selected_objs:
            # Initialize unique tags set
            unique_tags = set()
            
            # Collect all tags from all selected objects
            for obj in selected_objs:
                existing_tags = rs.GetUserText(obj, "Tags")
                if existing_tags:
                    # Split existing tags and add them to the unique_tags set
                    unique_tags.update(existing_tags.split(","))
        
           # Add newly added or appended tags from the text box if they are not already in unique_tags
            new_tag_text = self.labelBox.Text.strip()
            # Check if the new tag contains commas
            if new_tag_text and ',' not in new_tag_text:
                # Only add the new tag if it's not already in unique_tags
                if new_tag_text not in unique_tags:
                    unique_tags.add(new_tag_text)
            # print("Unique tags:", unique_tags)
            
            # Filter tags based on the search text if it's not empty
            search_text = self.search_text
            # print("Search text:", search_text)
            if search_text:
                filtered_tags = [tag for tag in unique_tags if search_text in tag.lower()]
                # print("Filtered tags:", filtered_tags)
            else:
                filtered_tags = list(unique_tags)
                # print("No search text. Showing all tags.")
            
            # Iterate over filtered tags and populate the UI
            for tag in filtered_tags: 
                delete_button_text = (tag +  str("   X"))
                delete_button_width = Eto.Drawing.Font.MeasureString(self.Font, delete_button_text).Width + 15
                delete_button = Eto.Forms.Button(Text=delete_button_text, BackgroundColor=Eto.Drawing.Colors.DimGray, Width=delete_button_width)
                delete_button.BackgroundColor = Eto.Drawing.Colors.Transparent
                delete_button.Tag = tag  # Set the tag text as Tag for the button
                delete_button.Click += self.OnDeleteButtonClick
                
                if row_width + delete_button_width >= max_row_width:  # Check if adding the next control exceeds the width
                    row_controls.append(None)  # Add a placeholder control for spacing
                    controls.AddRange(row_controls)  # Add row controls to the main controls list
                    self.layout.AddSeparateRow(System.Array[Eto.Forms.Control](controls))  # Add controls as a separate row
                    controls.Clear()  # Clear the controls list for the next row
                    # Clear the row controls list for the next row
                    row_controls = []
                    row_width = 0  # Reset row width
                
                row_controls.append(delete_button)  # Add the button to the row controls
                row_width += delete_button_width  # Update the row width
            
            if row_controls:  # Add the remaining row controls if any
                controls.AddRange(row_controls)
                controls.Add(None) # Add an empty item which gets scaled
                self.layout.AddSeparateRow(System.Array[Eto.Forms.Control](controls))

        self.layout.AddSpace()
        self.layout.Create()        

    def OnSearchBoxKeyDown(self, sender, e):
        # Check if Enter key is pressed
        if e.Key == Eto.Forms.Keys.Enter:
            # Call the layout update function
            self.UpdateDynamicLayout()

    def OnSearchBoxTextChanged(self, sender, e):
        # Check if the search box text is empty and it's not already in the process of updating
        if not sender.Text.strip() and not self.is_updating_layout:
            # Set the flag to indicate that the layout is being updated
            self.is_updating_layout = True
            # If the search box is empty, update the layout to show all tags
            self.UpdateDynamicLayout()
            # Reset the flag after the layout update is complete
            self.is_updating_layout = False

    def append_tags(self, objs, additional_values):
        appended_count = 0
        existing_count = 0

        # Ensure objs is a list, even if it contains only one object
        if not isinstance(objs, list):
            objs = [objs]

        for obj in objs:
            existing_value = rs.GetUserText(obj, "Tags")
            if existing_value:
                existing_values = existing_value.lower().split(",")  # Convert existing values to lowercase and split them
                new_values = [val.lower() for val in additional_values if val.lower() not in existing_values]
                if new_values:
                    new_value = ",".join(existing_values + new_values)
                    rs.SetUserText(obj, "Tags", new_value)
                    appended_count += 1
                else:
                    existing_count += 1  # Increment existing count if tag already exists for this object
            else:
                lowercased_values = [val.lower() for val in additional_values]
                rs.SetUserText(obj, "Tags", ",".join(lowercased_values))
                appended_count += 1
        
        # Print concise statement
        if appended_count > 0 or existing_count > 0:
            message = ""
            if appended_count > 0:
                message += "Tag added to {} objects".format(appended_count)
            if existing_count > 0:
                if message:
                    message += ", "
                message += "Tag already exists in {} objects".format(existing_count)
            print(message)


        else:
            lowercased_values = [val.lower() for val in additional_values]
            rs.SetUserText(obj, "Tags", ",".join(lowercased_values))
            print("Tags set successfully.") 

        # Update the user text immediately
        rs.ObjectName(obj, rs.GetUserText(obj, "Tags"))


    def remove_tags(self, tag_text):
        print('"' + str(tag_text) + '"' + " tag removed successfully.")

        # Remove the tag from the user text list
        selected_objs = rs.SelectedObjects()
        if selected_objs:
            for obj in selected_objs:
                existing_tags = rs.GetUserText(obj, "Tags")
                if existing_tags:
                    existing_tags = [tag.strip() for tag in existing_tags.split(",")]
                    if tag_text in existing_tags:
                        existing_tags.remove(tag_text)
                        rs.SetUserText(obj, "Tags", ",".join(existing_tags))
        
        # Update the user text immediately
        rs.ObjectName(obj, rs.GetUserText(obj, "Tags"))

    def OnDeleteButtonClick(self, sender, e):
        # Get the tag text from the button's Tag property
        tag_text = sender.Tag
        if tag_text:
            # Remove the tag from the UI
            self.remove_tags(tag_text)
            # Update the layout to reflect the changes
            self.UpdateDynamicLayout()
            
    def OnTextBoxKeyDown(self, sender, e):
        if e.Key == Eto.Forms.Keys.Enter:
            # Get the text entered into the text box and split it by commas
            tag_texts = self.labelBox.Text.strip().split(",")
            for tag_text in tag_texts:
                tag_text = tag_text.strip()  # Remove leading and trailing spaces
                if tag_text:
                    # Append tags to selected objects
                    selected_objs = rs.SelectedObjects()
                    if selected_objs:
                        tags = [tag_text]
                        self.append_tags(selected_objs, tags)  # Pass selected_objs as a list
                        # Update the UI tag list
                        self.UpdateDynamicLayout()
                    else:
                        print("No objects selected.")
            # Clear the text box
            self.labelBox.Text = ""

def EstablishDialog():       
    dialog = MyDialog()
    #icon_path = r"C:\Users\micha\OneDrive - Michael Vollrath\TOYBLOCK\PROJECTS\PROJECT_ENDGAME\ICONS\ICO_Tag_Object.ico"
    dialog.Icon = drawing.Icon(GetIconImagePath())
    dialog.ShowModal(Rhino.UI.RhinoEtoApp.MainWindow)

def GetIconImagePath():

    # Decode Image from Base64 String
    imageAsBase64String = r'iVBORw0KGgoAAAANSUhEUgAAAUIAAADiCAIAAAC0pFmRAAAAAXNSR0IArs4c6QAAAARnQU1uCX/FnkI4NCByIzgnqds1...'
    image_64_decode = base64.b64decode(imageAsBase64String)

     # Create temporary file for Image
    with tempfile.NamedTemporaryFile(delete=False) as f:
        image_result = open(f.name, 'wb')
        image_result.write(image_64_decode)
        image_result.close()
        return f.name;

EstablishDialog()
1 Like

Hi @michaelvollrath :slight_smile:

below version autoscales the dialog depending on content of the DynamicLayout, has a minimum size, remembers dialog position and does not open if no objects are selected at start.

I’ve put searchBox and labelBox out from the UpdateDynamicLayout() function as there is nothing to update. All is placed in a StackLayout and the labelBox gets focus once opened so you can start typing directly…

MC_TagObjects.py (13.0 KB)

_
c.

1 Like