I see, thank you for that!
Note, i did not keep track with the change from @Gijs which you need to add please. Btw. is there a bug when entering an uppercase tag it appears twice ?
Sure enough, good catch on the text case bug. I updated the UpdateDynamicLayout function to fix this and it shouldn’t happen anymore.
Here’s the updated code with all the most recent updates:
import System
import Rhino
import scriptcontext
import rhinoscriptsyntax as rs
import Eto
from System.Collections.Generic import List
class ObjectTaggingDialog(Eto.Forms.Dialog[bool]):
def __init__(self, selected_objs):
# the selected objects as class wide variable
self.selected_objs = selected_objs
Rhino.UI.EtoExtensions.UseRhinoStyle(self)
self.Title = "Tag Object"
self.Size = Eto.Drawing.Size(300, 220)
self.MinimumSize = Eto.Drawing.Size(320, 165)
self.MaximumSize = Eto.Drawing.Size(320, -1)
self.Padding = Eto.Drawing.Padding(10)
self.Resizable = True
self.AutoSize = False
self.Font = Eto.Drawing.SystemFonts.Default()
# searchBox
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.PlaceholderText = "Search..."
self.searchBox.TextChanged += self.OnSearchBoxTextChanged # Handle text changed event
self.searchBox.KeyDown += self.OnSearchBoxKeyDown # Handle key down event
# Create TextBox For Tag Entries
self.labelBox = Eto.Forms.TextBox(ShowBorder=False) # Provide the placeholder text
self.labelBox.PlaceholderText = "Add tag(s) here..."
self.labelBox.Height = 25 # Adjust the height of the text box
self.labelBox.KeyDown += self.OnTextBoxKeyDown
# the layout for tags
self.layout = Eto.Forms.DynamicLayout()
self.layout.DefaultSpacing = Eto.Drawing.Size(4,4) # propagates
self.layout.Padding = Eto.Drawing.Padding(0, 6, 0, 0) # controls the gap above the tags
# scrollable
self.scrollable = Eto.Forms.Scrollable()
self.scrollable.ExpandContentHeight = True
self.scrollable.ExpandContentWidth = True
self.scrollable.ScrollSize = Eto.Drawing.Size(260, -1) # 260 or smaller prevents that the horizontal scroll bar comes up
self.scrollable.Border = Eto.Forms.BorderType.None
self.scrollable.Content = self.layout
# create a stack layout which has 2 sections, top section holds the searchbox
# and text box, bottom section holds a scrollable which contains dynamic layout
self.stack_layout = Eto.Forms.StackLayout()
self.stack_layout.Orientation = Eto.Forms.Orientation.Vertical
self.stack_layout.HorizontalContentAlignment = Eto.Forms.HorizontalAlignment.Stretch
self.stack_layout.Padding = Eto.Drawing.Padding(0,0)
self.stack_layout.Spacing = 2
self.stack_layout.Items.Add(Eto.Forms.StackLayoutItem(self.searchBox, False))
self.stack_layout.Items.Add(Eto.Forms.StackLayoutItem(self.labelBox, False))
self.stack_layout.Items.Add(Eto.Forms.StackLayoutItem(self.scrollable, True)) # True means it does stretch vertically
self.Content = self.stack_layout
# add events
self.SizeChanged += self.MyOnSizeChangedEvent
self.Load += self.OnDialogLoad
self.Closing += self.OnDialogClosing
def MyOnSizeChangedEvent(self, sender, e):
try:
self.UpdateDynamicLayout()
except Exception as ex:
print ex
return
def UpdateDynamicLayout(self):
# Store the search text before clearing the layout
self.search_text = self.searchBox.Text.strip().lower()
# 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
# Initialize unique tags set
unique_tags = set()
# Collect all tags from all selected objects
for obj in self.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.lower().split(",")) # Convert tags to lowercase before adding
# 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.lower() not in unique_tags:
unique_tags.add(new_tag_text.lower()) # Convert new tag to lowercase before adding
# 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]
# 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):
# Real-Time search feedback
self.UpdateDynamicLayout()
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.")
def remove_tags(self, tag_text):
print('"' + str(tag_text) + '"' + " tag removed successfully.")
# Remove the tag from the user text list
for obj in self.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))
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
if self.selected_objs:
tags = [tag_text]
self.append_tags(self.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 OnDialogLoad(self, sender, e):
# restores dialog location from last use
key = "MC_TagObjects_DialogLocation"
if scriptcontext.sticky.has_key(key): self.Location = scriptcontext.sticky[key]
# set focus to the tag entering labelBox
self.labelBox.Focus()
def OnDialogClosing(self, sender, e):
# save dialog location for next use
scriptcontext.sticky["MC_TagObjects_DialogLocation"] = self.Location
def GetIcon():
# create bitmap from bytestring
bytes = System.Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsIAAA7CARUoSoAAAAF9SURBVDhPpdFLSwJRFAfwcy7kpiSwqE3R7JRGgxa9xkqjktpUEqHWOlrVpkXQIoq+QJC76EFgkNrLhJBAP0O10jZBtHfhxkpP9yoTzgOLWvyZ4cL/N3fOASL6V75fpDZIensw45Gr8Tox43Zg2iuDb7YfYKZPGz8/0wAbfqT8BaPCGaOPKKN8hNHrIaOVSXz0yCDN6RADsBPEPKUZ5cKsfLqGn8VzRpTkEAeXJ4yIEQhw4I5RQIFcezPsnqxiSQB0Y44YgG0B3DOKrmNhaRSes/tYpmtGpbg5YgC2FrAoALpi9M5nIJ6irKYW4YOV5gd0gGKHWDaMlf+mS23ZBHkYk2FcA9iawOK2Q1xFzIBSjCMpRslNpF4JEhqgowXA1ggNw3UQSjB646sNKpjydYPLAHTWQSrlI76lIUhNO8G6OKibgQqYIXRb/bIoT/FyiJcD+i3UAioy4oD40x7SywGjkKIt/wiItFrB4uqCYz6wiCiLa6tlA/DXmB7+PgRf7QIEb+tmZQoAAAAASUVORK5CYII=")
bitmap = Eto.Drawing.Bitmap(bytes)
# save bitmap in memory stream
stream = System.IO.MemoryStream()
bitmap.Save(stream, Eto.Drawing.ImageFormat.Png)
# create icon from it
icon = Eto.Drawing.Icon(stream)
return icon
def EstablishDialog():
# get or prompt for objects to tag, do not open dialog if no selection is made
selected_objs = rs.GetObjects("Select object(s) to tag", rs.filter.allobjects, preselect=True, select=True)
if not selected_objs: return
# open dialog and pass object ids
dialog = ObjectTaggingDialog(selected_objs)
dialog.Icon = GetIcon()
dialog.ShowModal(Rhino.UI.RhinoEtoApp.MainWindow)
EstablishDialog()