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.)