Skip to content

Feature combobox button bundle#2930

Merged
jmcouffin merged 16 commits intopyrevitlabs:developfrom
dnenov:feature-combobox-button-bundle
Dec 13, 2025
Merged

Feature combobox button bundle#2930
jmcouffin merged 16 commits intopyrevitlabs:developfrom
dnenov:feature-combobox-button-bundle

Conversation

@dnenov
Copy link
Copy Markdown
Contributor

@dnenov dnenov commented Nov 25, 2025

Complete ComboBox Implementation with Full API Support

Description

This PR implements comprehensive ComboBox support for pyRevit extensions, exposing all properties and methods from the Autodesk.Revit.UI ComboBox API. The implementation enables full control over ComboBox creation, member management, and property configuration through bundle.yaml metadata.

The ComboBox implementation follows the same SmartButton pattern for deferred initialization, using the __selfinit__ function in script.py to allow scripts to access the fully constructed UI object and set up event handlers after the UI is created.

Combobox UI Element

Revit_gpZ8Xs46xi

Key Features

  • Full Property Support: All ComboBox properties are now accessible (Name, ToolTip, Image, ItemText, Enabled, Visible, Current, LongDescription, ToolTipImage)
  • Complete Method Implementation: All ComboBox methods are exposed (add_item, add_items, add_separator, get_items, etc.)
  • Rich Member Metadata: ComboBox members support icons, tooltips, groups, extended tooltips, and tooltip images
  • Event Handling: Support for CurrentChanged, DropDownOpened, and DropDownClosed events via __selfinit__ pattern (following SmartButton architecture)
  • Proper Property Setting: Fixed member property assignment to work on ComboBoxMember objects (not ComboBoxMemberData)
  • SmartButton Pattern: Uses deferred initialization via __selfinit__ function, consistent with SmartButton implementation

Technical Changes

  1. components.py: Fixed member dictionary preservation - was converting rich metadata to simple tuples, losing icon/tooltip/group properties
  2. ribbon.py: Added complete _PyRevitRibbonComboBox class with all API methods and properties
  3. uimaker.py: Enhanced ComboBox creation to set all properties from metadata and properly configure members. Implements __selfinit__ pattern (SmartButton pattern) for deferred script initialization

Bug Fixes

  • Fixed member icons/tooltips not appearing by preserving full member dictionaries
  • Fixed property setting by using ComboBoxMember objects returned from AddItem()
  • Added encoding declaration to fix non-ASCII character syntax error
  • Fixed icon path resolution for member icons

Checklist

Before submitting your pull request, ensure the following requirements are met:

  • Code follows the PEP 8 style guide.
  • Code has been formatted with Black using the command:
    pipenv run black {source_file_or_directory}
  • Changes are tested and verified to work as expected.

Related Issues

If applicable, link the issues resolved by this pull request:

  • Related to ComboBox support feature request

Additional Notes

Files Modified

  • pyrevitlib/pyrevit/extensions/components.py - Fixed member dictionary preservation
  • pyrevitlib/pyrevit/coreutils/ribbon.py - Added complete ComboBox wrapper class
  • pyrevitlib/pyrevit/loader/uimaker.py - Enhanced ComboBox creation and member configuration

Architecture Pattern

The ComboBox implementation follows the SmartButton pattern for deferred initialization:

  1. UI is created and configured from bundle.yaml metadata
  2. script.py is loaded as a module
  3. If __selfinit__ function exists, it's called with (component, ui_item, uiapp) parameters
  4. Script can access the fully constructed UI object to set up event handlers and perform custom initialization
  5. If __selfinit__ returns False, the ComboBox is deactivated

This pattern allows scripts to:

  • Access the ComboBox UI object after it's fully created
  • Set up event handlers (CurrentChanged, DropDownOpened, DropDownClosed)
  • Perform custom initialization logic
  • Access all ComboBox properties and methods

Usage Example

ComboBoxes can now be created with full metadata support in bundle.yaml:

title: Panel Selector
tooltip: Switch between panels
tooltip_ext: Extended description
icon: icon.png
media_file: tooltip.png
members:
  - id: "settings"
    text: "Settings"
    icon: icon_settings.png
    tooltip: "Open Settings panel"
    tooltip_ext: "Configure settings"
    group: "Main Panels"
  - id: "views_sheets"
    text: "Views & Sheets"
    icon: icon_views.png
    tooltip: "Open Views & Sheets panel"
    group: "Main Panels"

And in script.py, use the __selfinit__ pattern to wire ComboBox items to commands:

def __selfinit__(component, ui_item, uiapp):
    """Deferred initializer - called after UI is created."""
    cmb = ui_item.get_rvtapi_object()
    
    # Define your command functions
    def show_settings():
        # Your logic here
        pass
    
    def show_views_sheets():
        # Your logic here
        pass
    
    # Map ComboBox member IDs to functions
    # The 'id' field from bundle.yaml members is used as the key
    FUNCTION_MAP = {
        "settings": show_settings,
        "views_sheets": show_views_sheets,
        # You can also map by display text as fallback
        "Settings": show_settings,
        "Views & Sheets": show_views_sheets,
    }
    
    def on_current_changed(sender, args):
        """Handle ComboBox selection change."""
        try:
            # Get the currently selected item
            current = sender.Current
            if not current:
                return
            
            # Access the member's ID and display text
            selected_id = current.Name      # The 'id' from bundle.yaml
            selected_text = current.ItemText  # The 'text' from bundle.yaml
            
            # Look up the function to call
            func = FUNCTION_MAP.get(selected_id) or FUNCTION_MAP.get(selected_text)
            
            if func:
                # Execute the command
                func()
            else:
                # Handle unknown selection
                logger.warning("No handler for: {} ({})".format(selected_text, selected_id))
        except Exception as e:
            logger.error("Error in ComboBox handler: {}".format(e))
    
    # Hook the event handler
    ui_item.add_current_changed_handler(on_current_changed)
    
    # Keep reference to prevent garbage collection
    ui_item._current_changed_handler = on_current_changed
    
    return True  # Return False to deactivate ComboBox

Key Points:

  • The id field from bundle.yaml members becomes the Name property of the ComboBoxMember
  • The text field becomes the ItemText property
  • Use sender.Current.Name to get the member ID for routing to commands
  • Use sender.Current.ItemText to get the display text
  • Create a mapping dictionary (FUNCTION_MAP) to connect member IDs to your command functions
  • The event handler receives the ComboBox sender and event args, allowing access to the selected member

API Methods Available

All ComboBox properties and methods from Autodesk.Revit.UI are now accessible:

  • Properties: name, current, visible, enabled, get_title(), etc.
  • Methods: set_icon(), set_tooltip(), add_item(), get_items(), etc.
  • Events: CurrentChanged, DropDownOpened, DropDownClosed (via add_*_handler() methods)

Gotchas

  • Thread Context: Do not use print() or equivalent logging inside the __selfinit__ initialization phase, as the thread context will break. Use logger from pyrevit.script instead:

    from pyrevit import script
    logger = script.get_logger()
    logger.warning("This works correctly")
  • Member Icons: Using image (or icon) for ComboBox member items can yield strange UI behavior in Revit's ComboBox dropdown. The items may appear with extra padding/margin, pushing text to the right. This is a limitation of the Revit API's ComboBox rendering and is not controllable via the Ribbon API. Consider using icons sparingly or testing thoroughly if visual alignment is critical.

Testing

Tested with Spectrum extension ComboBox implementation. All member icons, tooltips, and properties are working correctly. Event handlers work as expected using the __selfinit__ pattern.


Reviewers

FYI

Thank you for contributing to pyRevit! 🎉

- added initial combobox bundle to test case
- Add COMBOBOX_POSTFIX to extension directory hash calculation
- Implement ComboBox creation and member handling in uimaker.py
- Add _PyRevitRibbonComboBox wrapper class in ribbon.py
- Add ComboBoxGroup component class with members parsing
- the combobox loads and tirggers update
- because of event handling and initialization, we had to match the design pattern used with the smartbutton
- using delayed script loading
- Added comprehensive ComboBox property support (Name, ToolTip, Image, ItemText, Current, etc.)
- Implemented all ComboBox methods (add_item, add_items, add_separator, get_items, etc.)
- Fixed member properties by preserving full member dictionaries in components.py (was converting to tuples)
- Fixed member icon/tooltip/group properties by setting on ComboBoxMember object after AddItem
- Added encoding declaration to uimaker.py to fix non-ASCII character error
- Enhanced logging for ComboBox creation and member property setting
- Updated add_item() to return ComboBoxMember for property setting
- Full support for ComboBoxMemberData properties (icon, tooltip, group, tooltip_ext, tooltip_image)
- Added comprehensive ComboBox property support (Name, ToolTip, Image, ItemText, Current, etc.)
- Implemented all ComboBox methods (add_item, add_items, add_separator, get_items, etc.)
- Fixed member properties by preserving full member dictionaries in components.py (was converting to tuples)
- Fixed member icon/tooltip/group properties by setting on ComboBoxMember object after AddItem
- Added encoding declaration to uimaker.py to fix non-ASCII character error
- Updated add_item() to return ComboBoxMember for property setting
- Full support for ComboBoxMemberData properties (icon, tooltip, group, tooltip_ext, tooltip_image)
- Removed all debugging logging statements
- Formatted components.py, ribbon.py, and uimaker.py with Black
- Ensures code follows PEP 8 style guidelines
@devloai
Copy link
Copy Markdown
Contributor

devloai bot commented Nov 25, 2025

Unable to perform a code review. You have run out of credits 😔
Please upgrade your plan or buy additional credits from the subscription page.

@jmcouffin jmcouffin added New Feature New feature request [class->Implemented #{number}: {title}] Highlight Highlight these issues on Release notes labels Nov 25, 2025
@jmcouffin
Copy link
Copy Markdown
Contributor

@romangolev this may touch your actual refactoring work

@jmcouffin
Copy link
Copy Markdown
Contributor

This is great stuff @dnenov

I'll take a look as soon as I can.

In the meantime, if it is not too much to ask, could make a sample/test in the pyrevit dev extensions in a new PR. That would be helpful.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements comprehensive ComboBox support for pyRevit extensions, exposing all properties and methods from the Autodesk.Revit.UI ComboBox API. The implementation follows the SmartButton pattern for deferred initialization using the __selfinit__ function.

Key Changes:

  • Complete ComboBox API wrapper with all properties and methods (name, tooltip, image, current, events, etc.)
  • Rich member metadata support (icons, tooltips, groups, extended tooltips, tooltip images)
  • SmartButton-style deferred initialization pattern for event handler setup
  • Bug fixes for member property setting and icon path resolution

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.

File Description
pyrevitlib/pyrevit/loader/uimaker.py Added _produce_ui_combobox function implementing full ComboBox creation with member configuration and __selfinit__ pattern support
pyrevitlib/pyrevit/extensions/components.py Added ComboBoxGroup class to handle ComboBox metadata and member preservation
pyrevitlib/pyrevit/extensions/__init__.py Added COMBOBOX_POSTFIX constant for ComboBox component identification
pyrevitlib/pyrevit/coreutils/ribbon.py Added _PyRevitRibbonComboBox wrapper class with complete API methods, properties, and event handler support
Comments suppressed due to low confidence (1)

pyrevitlib/pyrevit/loader/uimaker.py:1023

  • Except block directly handles BaseException.
                except:

@jmcouffin
Copy link
Copy Markdown
Contributor

@sanzoghenzo I'd like your opinionated review 😸

@romangolev
Copy link
Copy Markdown
Member

@romangolev this may touch your actual refactoring work

Cool stuff! @jmcouffin would be nice to implement this functionality in new loader as well. Do you think we could confirm that that it works, confirm that my PR works as well and then proceed with this?
Splitting it into chunks might be more controllable

@jmcouffin
Copy link
Copy Markdown
Contributor

@romangolev this may touch your actual refactoring work

Cool stuff! @jmcouffin would be nice to implement this functionality in new loader as well. Do you think we could confirm that that it works, confirm that my PR works as well and then proceed with this? Splitting it into chunks might be more controllable

100%
I'll get both validated as soon as I can and as soon as I can the brilliant reviews from my brother in arm: Sanzo. While having dinner with Deyan tonight IRL, yeah, that actually happened, I asked him to had a test/example in the pyrevitdev extension.

Both PR are quite extensive, so expect turbulences.

@dnenov
Copy link
Copy Markdown
Contributor Author

dnenov commented Nov 27, 2025

I will do my best to get a test/example in the dev extension - I wasn't aware of it! As you guys can see, the actual, in-code implementation of the combobox is a bit complicated, but that's to be expected. It will be of great value to have that tested the way you want, so I'll jump on it asap!

Re-splitting this one up - it's a singular new feature :/ I struggle to find a way to have it broken down, as each piece works together with the rest. If you guys, @romangolev @jmcouffin have a better idea how to get it split up, ... yes, up for it too.

Essentially, it's a wrapper around the native Revit combobox ui element, much like the other ui elements we have.

Copy link
Copy Markdown
Contributor

@sanzoghenzo sanzoghenzo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HI, here's my review.
I didn't test any of this, just looked at the code style 😉

return None


def _produce_ui_combobox(ui_maker_params):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is really long, is there a way to split it into logical chunks?
Also, is this code that error prone that you need to catch an exception on each line? (I didn't try it so this is just by reading the code).
If this is necessary, can you reduce the scope of the exceptions to only catch those expected?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright! I did my best to logically break that down to a few sub-routines to make it easier to read and review now. Still, the complexity is there - mainly because we don't have a single button this time around, but we deal with all the sub-menu (combobox) items as well.

@jmcouffin
Copy link
Copy Markdown
Contributor

@devloai please review

Copy link
Copy Markdown
Contributor

@devloai devloai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Summary:

This PR implements comprehensive ComboBox support for pyRevit extensions, exposing all properties and methods from the Autodesk.Revit.UI ComboBox API. Key features include:

  • Full property support (Name, ToolTip, Image, ItemText, Enabled, Visible, Current, etc.)
  • Complete method implementation (add_item, add_items, add_separator, get_items, etc.)
  • Rich member metadata support (icons, tooltips, groups, extended tooltips)
  • Event handling via __selfinit__ pattern (following SmartButton architecture)
  • Proper member property assignment on ComboBoxMember objects

Review Summary:

The implementation is well-structured and follows existing pyRevit patterns correctly. The code properly:

  • Uses the SmartButton pattern for deferred initialization via __selfinit__
  • Handles member property extraction and assignment correctly (initial concern about variable shadowing was unfounded - the code cleverly extracts properties before reassigning the variable)
  • Follows the existing pattern where create_* methods don't return values (consistent with create_pulldown_button, create_split_button, etc.)
  • Implements comprehensive error handling with appropriate logging levels
  • Preserves full member dictionaries to maintain rich metadata (icons, tooltips, groups)

The architecture aligns with the SmartButton pattern documented in the knowledge base, allowing scripts to access fully constructed UI objects and set up event handlers after UI creation.

No critical issues found. The implementation is solid and ready to merge. 🎉

Follow-up suggestions:

  • @devloai create example/test bundle in pyrevitdev extension showing ComboBox usage
  • @devloai add documentation for ComboBox bundle.yaml metadata format

- resolved auto generated Copilot checks
- Fix debug logging levels and exception handling
- Improve code style: reduce nesting, extract helpers
- Add missing docstring documentation
- Remove redundant code and clarify comments

Addresses Copilot AI and human review suggestions.
- Extract _setup_combobox_objects() to handle validation and object creation
- Extract _add_combobox_members() to handle member addition logic
- Refactor _produce_ui_combobox() for better readability and maintainability
- Fix critical indentation bug: unindent code after icon check so tooltips,
  members, and activation run regardless of icon file presence

This refactoring improves code organization by separating concerns:
- Setup/validation logic is isolated and testable
- Member addition logic is self-contained
- Main function focuses on high-level configuration flow
@dnenov dnenov requested a review from sanzoghenzo December 10, 2025 14:18
- Add _sanitize_script_file() function to replace common non-ASCII characters with ASCII equivalents
- Call sanitization before loading combobox scripts to prevent SyntaxError
- Handles em/en dashes, smart quotes, ellipsis, and non-breaking spaces
@dnenov dnenov mentioned this pull request Dec 10, 2025
3 tasks
@dnenov
Copy link
Copy Markdown
Contributor Author

dnenov commented Dec 10, 2025

Thank you for the wonderful reviews, @sanzoghenzo and @jmcouffin ! Very keen eye, and I hope I was able to accommodate all the requests at this point.

If you could cast another eye on it, that would be wonderful! Meanwhile, I created this draft PR to follow up with, with implementation under the pyRevitDev toolbar. I am not entirely sure if that was the right way to do it, I got some prompts for the pythonnet repository but did not look into details to be honest :/ If you let me know what's the best way to contribute, I will amend accordingly!

@jmcouffin jmcouffin merged commit 1325ebc into pyrevitlabs:develop Dec 13, 2025
@jmcouffin
Copy link
Copy Markdown
Contributor

@dnenov Thank you so much for this one.
This is a great new addition.

@romangolev just tested and approved. I'll see to get to your PR and then put the combobox in your task list :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Highlight Highlight these issues on Release notes New Feature New feature request [class->Implemented #{number}: {title}]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants