Shared tab between multiple extensions panel sorting issues

Hello all! First forum post after using pyRevit for the better part of 2 years! It is a game changer!

I have kind of taken over on the role of software manager at our MEP engineering firm. We use pyRevit a lot and recently I have migrated all of our panels that we previously had in one extension to their own extensions on github (See attached photos). I have recreated the pyRevit extension manager to only show my extensions, so see attached for installed extensions. These all share the same tab. However, the issue I am facing is that I cannot figure out how to reorder my panels in the tab between multiple extensions. It seems to just be sorting them alphabetically by extension name. I have tried naming my panel folder with numbers at the beginning and then renaming them via a bundle file, but that did not work. Also attached are screenshot of some of the folder structure. In the pyMLE - Tools extension in the screenshot below, the View and Edit panel should be second and third in my tab respectively. But because it is the last extension alphabetically, they are at the end. Any help would be appreciated. Thank you in advance!

No idea if this is what you mean, but this would be the AI answer of deepwiki, maybe that can help?

Based on the pyRevit codebase, the issue you’re experiencing is that panels from multiple extensions are being ordered alphabetically by extension name. To control the order of panels across extensions, you need to use layout directives in your panel’s bundle.yaml file.

Solution

Add layout directives to your panel’s bundle.yaml file to specify its position relative to other panels. The sort_pyrevit_ui() function processes these directives to reorder panels 1 .

Example bundle.yaml for reordering:

# In pyMLE - Tools extension, for the View panel:
name: View
layout:
  before: "PanelNameToComeAfter"  # Panel should come before this panel

Or to place a panel at the beginning:

# In pyMLE - Tools extension, for the View panel:
name: View
layout:
  beforeall: true  # Panel should come before all others

How it works

  1. When pyRevit loads extensions, it calls sort_pyrevit_ui() after creating all UI elements 2
  2. This function iterates through all pyRevit tabs and processes layout directives found in bundle files
  3. For each directive, it calls the appropriate reorder method on the tab object:
    • reorder_before() - places panel before another panel 3
    • reorder_after() - places panel after another panel 4
    • reorder_beforeall() - places panel at the beginning 5
    • reorder_afterall() - places panel at the end 6

Implementation steps

  1. Identify the exact panel names you want to reorder (use the ui_title from bundle.yaml or the directory name)
  2. Add layout directives to the bundle.yaml files of panels that need reordering
  3. Reload pyRevit for changes to take effect

The panel names used in directives should match the ui_title or directory name of the target panels 7 .

Notes

  • Layout directives only work on panels within the same tab
  • The reordering happens after all extensions are loaded, so it works across multiple extensions
  • Make sure to use the exact panel names as they appear in the UI or bundle files
  • This approach is more reliable than numbering folders since it uses pyRevit’s built-in layout system

Wiki pages you might want to explore:

Citations

File: pyrevitlib/pyrevit/loader/uimaker.py (L689-722)

def update_pyrevit_ui(ui_ext, ext_asm_info, create_beta=False):
    """Updates/Creates pyRevit ui for the extension and assembly dll address.

    Args:
        ui_ext (GenericUIContainer): UI container.
        ext_asm_info (AssemblyInfo): Assembly info.
        create_beta (bool, optional): Create beta ui. Defaults to False.
    """
    mlogger.debug('Creating/Updating ui for extension: %s', ui_ext)
    cmp_count = _recursively_produce_ui_items(
        UIMakerParams(current_ui, None, ui_ext, ext_asm_info, create_beta))
    mlogger.debug('%s components were created for: %s', cmp_count, ui_ext)


def sort_pyrevit_ui(ui_ext):
    """Sorts pyRevit UI.

    Args:
        ui_ext (GenericUIContainer): UI container.
    """
    # only works on panels so far
    # re-ordering of ui components deeper than panels have not been implemented
    for tab in current_ui.get_pyrevit_tabs():
        for litem in ui_ext.find_layout_items():
            if litem.directive:
                if litem.directive.directive_type == 'before':
                    tab.reorder_before(litem.name, litem.directive.target)
                elif litem.directive.directive_type == 'after':
                    tab.reorder_after(litem.name, litem.directive.target)
                elif litem.directive.directive_type == 'afterall':
                    tab.reorder_afterall(litem.name)
                elif litem.directive.directive_type == 'beforeall':
                    tab.reorder_beforeall(litem.name)

File: pyrevitlib/pyrevit/coreutils/ribbon.py (L397-402)

        for sub_cmp in self._sub_pyrvt_components.values():
            if child_name == sub_cmp.name:
                return sub_cmp
            elif hasattr(sub_cmp, 'ui_title') \
                    and child_name == sub_cmp.ui_title:
                return sub_cmp

File: pyrevitlib/pyrevit/coreutils/ribbon.py (L436-456)

    def reorder_before(self, item_name, ritem_name):
        """Reorder and place item_name before ritem_name.

        Args:
            item_name (str): name of component to be moved
            ritem_name (str): name of component that should be on the right
        """
        apiobj = self.get_rvtapi_object()
        litem_idx = ritem_idx = None
        if hasattr(apiobj, 'Panels'):
            for item in apiobj.Panels:
                if item.Source.AutomationName == item_name:
                    litem_idx = apiobj.Panels.IndexOf(item)
                elif item.Source.AutomationName == ritem_name:
                    ritem_idx = apiobj.Panels.IndexOf(item)
            if litem_idx and ritem_idx:
                if litem_idx < ritem_idx:
                    apiobj.Panels.Move(litem_idx, ritem_idx - 1)
                elif litem_idx > ritem_idx:
                    apiobj.Panels.Move(litem_idx, ritem_idx)

File: pyrevitlib/pyrevit/coreutils/ribbon.py (L457-472)

    def reorder_beforeall(self, item_name):
        """Reorder and place item_name before all others.

        Args:
            item_name (str): name of component to be moved
        """
        # FIXME: verify docs description is correct
        apiobj = self.get_rvtapi_object()
        litem_idx = None
        if hasattr(apiobj, 'Panels'):
            for item in apiobj.Panels:
                if item.Source.AutomationName == item_name:
                    litem_idx = apiobj.Panels.IndexOf(item)
            if litem_idx:
                apiobj.Panels.Move(litem_idx, 0)

File: pyrevitlib/pyrevit/coreutils/ribbon.py (L473-493)

    def reorder_after(self, item_name, ritem_name):
        """Reorder and place item_name after ritem_name.

        Args:
            item_name (str): name of component to be moved
            ritem_name (str): name of component that should be on the left
        """
        apiobj = self.get_rvtapi_object()
        litem_idx = ritem_idx = None
        if hasattr(apiobj, 'Panels'):
            for item in apiobj.Panels:
                if item.Source.AutomationName == item_name:
                    litem_idx = apiobj.Panels.IndexOf(item)
                elif item.Source.AutomationName == ritem_name:
                    ritem_idx = apiobj.Panels.IndexOf(item)
            if litem_idx and ritem_idx:
                if litem_idx < ritem_idx:
                    apiobj.Panels.Move(litem_idx, ritem_idx)
                elif litem_idx > ritem_idx:
                    apiobj.Panels.Move(litem_idx, ritem_idx + 1)

File: pyrevitlib/pyrevit/coreutils/ribbon.py (L494-509)

    def reorder_afterall(self, item_name):
        """Reorder and place item_name after all others.

        Args:
            item_name (str): name of component to be moved
        """
        apiobj = self.get_rvtapi_object()
        litem_idx = None
        if hasattr(apiobj, 'Panels'):
            for item in apiobj.Panels:
                if item.Source.AutomationName == item_name:
                    litem_idx = apiobj.Panels.IndexOf(item)
            if litem_idx:
                max_idx = len(apiobj.Panels) - 1
                apiobj.Panels.Move(litem_idx, max_idx)

Oh I didn’t know that there was a Before method in the bundle files, that will be helpful thanks! I can foresee an issue with loading these as the intent of me separating the extensions the way that I have is so that people in my office can turn off certain panels that they do not need. If I have a before command pointing to a panel that is not loaded, what happens?

No idea, try it and report back for others to have a future reference? Skimming through the code up there, i think missing commands simply get swallowed silently.
The #FIXME doesn’t point to an error either.

After testing the bundle.yaml file in my tab folder, it seems that if the panel name in before:(panel_name) does not exist, it will just revert to extension load order. However, I have come up with another solution. I have renamed my extensions directories with a prefix number in my extensions.json file. This will allow my pyRevit to pull my github repositories to that folder and basically force a load order. This is more of a hack than a solution, but I don’t know if there will be a graceful one that will work for every use case.

What would be nice is if there was a load order parameter in your extensions.json file (or extension.json) that would force revit to load extensions in the specified order.

(Note that folder 05 is missing, but the number still ensure proper load order.)

This would be the deepwiki suggestion you could implement in a fork for your company.
If you think this is general a feature a lot of people need, its best to create an issue on github.

I understand your request. You want to implement a load order parameter for pyRevit extensions to control the order in which extensions are loaded, rather than relying on the current hack of prefixing directory names with numbers or the fragile before:(panel_name) directive that fails when the target doesn’t exist.

Current Extension Loading System

The extension loading process works as follows:

  1. Extension Discovery: Extensions are found in directories configured in user_config 1
  2. Parsing and Caching: Extensions are parsed or loaded from cache in extensionmgr.py 2
  3. UI Creation: UI elements are created recursively in uimaker.py 3
  4. Sorting: Current sorting uses layout directives like before, after, etc. 4

Implementation Plan

To add a load order parameter, you would need to modify these key areas:

1. Add Load Order to Extension Metadata

First, add a new metadata constant for load order in extensions/__init__.py:

MDATA_LOAD_ORDER = 'load_order'

2. Update Extension Component

Modify the Extension class in components.py to handle load order:

@property
def load_order(self):
    """Get the load order from extension.json metadata."""
    return self.get_bundle_data().get('load_order', 999)

3. Modify Extension Loading

Update extensionmgr.py to sort extensions by load order after parsing:

def get_installed_ui_extensions():
    # ... existing code ...
    extensions = _parse_or_cache(ext_info) for ext_info in ext_infos
    # Sort by load_order
    extensions.sort(key=lambda x: x.load_order)
    return extensions

4. Update UI Sorting

Modify sort_pyrevit_ui() in uimaker.py to respect load order as the primary sort key before applying layout directives.

Notes

The current sorting system in ribbon.py only works on panels and has limitations 5 . A load order parameter would provide a more reliable way to control extension loading order that works at the extension level rather than depending on panel names existing.

This implementation would be backward compatible since extensions without the load_order parameter would default to a high value (999), maintaining their current behavior.

The new loader in wip might behave slightly differently

WIP as in Work in Progress? When is that expected to ship out?

Yes. It does have installers for you to try.

Release? When the tests and fixes for the new ones will be over. No eta. We aren’t machine and this is not our day job.

Totally understand. You all are phenomenal are I am very thankful for the work that you put into development!