Skip to content

Fix/reload smartbutton#3476

Open
ChrisCrosley wants to merge 2 commits into
pyrevitlabs:developfrom
ChrisCrosley:fix/reload-smartbutton
Open

Fix/reload smartbutton#3476
ChrisCrosley wants to merge 2 commits into
pyrevitlabs:developfrom
ChrisCrosley:fix/reload-smartbutton

Conversation

@ChrisCrosley

@ChrisCrosley ChrisCrosley commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Description

SmartButtons nested inside pulldown and split buttons lost their __selfinit__ icon/state changes on pyRevit reload under the C# loader. This PR re-runs __selfinit__ on the reload update path for those two containers, hardens Update.smartbutton against transient update-check failures, and adds a DevTools test button for verifying the behavior.


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:


Root cause

On reload, UpdateExistingChildren in PulldownButtonBuilder and SplitButtonBuilder re-applied the bundle icon via
ButtonPostProcessor.Process() but never re-ran __selfinit__. Any icon or enabled/disabled state set by the init script was lost, and an icon set in a previous session could linger out of sync with current state (e.g. the Update button staying orange after updating). The fix mirrors the pattern already used by PushButtonBuilder.UpdateExistingButton and StackBuilder, which were unaffected.

Notes

Stale IconManager BitmapCache (the original report's theory): not the cause - IconManager and its cache are recreated on every LoadSession, so no bitmaps survive a reload, and ClearCache() in the reload sequence would be a no-op.

Reviewer's can consider the updates to Update.SmartButton optional. I was not able to reproduce the stale icon issue with this button directly. I did create a test button similar to Test Reload Icon Toggle in a slideout - to reproduce the reload conditions for this button type - and the icon state changes correctly. If the Update.smartbutton issue is reproduceable, it is probably something deeper in the updater.

Changes

  • PulldownButtonBuilder.UpdateExistingChildren / SplitButtonBuilder.UpdateExistingChildren: after re-applying
    post-processing to an existing child, run ExecuteSelfInit for SmartButtons and honor a False return by disabling the button - matching the create path and PushButtonBuilder's update path.
  • Update.smartbutton: a failed update-availability check no longer returns False from __selfinit__ (which deactivated the Update button for the whole session on any transient git/network error); it now logs the error and keeps the tool usable. Only the user_can_update permission gate deactivates the button.
  • New Test Reload Icon Toggle SmartButton (pyRevitDevTools → Debug → Bundle Tests): clicking toggles a state persisted in the user config ini ([devtools_reload_tests]); __selfinit__ re-applies the matching icon on every load. After a reload the icon must match the stored state, which distinguishes "__selfinit__ not re-run" (default icon shown while state is on) from "bundle icon not restored" (orange shown while state is off).

Testing

  1. Build the loader (pyRevitLoader.sln) and reload pyRevit.
  2. In pyRevitDev → Debug → Bundle Tests, click Test Reload Icon Toggle → icon turns orange.
  3. Reload pyRevit → icon stays orange (before this fix it reverted to the default icon because __selfinit__ never re-ran inside pulldowns).
  4. Click again (icon reverts to default), reload → stays default, confirming the loader correctly restores bundle icons when
    __selfinit__ declines to set one — the scenario from the original report ("Update button stays orange after updating").

On reload, UpdateExistingChildren in PulldownButtonBuilder and
SplitButtonBuilder re-applied bundle icons via ButtonPostProcessor.Process
but never re-ran __selfinit__. SmartButtons nested in pulldowns and split
buttons therefore lost any icon/state set by their init scripts, and icons
set in a previous session could linger out of sync with current state
(e.g. the Update button staying orange after updating). Top-level,
slide-out, and stacked SmartButtons were unaffected — PushButtonBuilder
and StackBuilder already re-ran __selfinit__ on their update paths.

Note: this was originally reported as a stale IconManager BitmapCache, but
the cache is not the cause — IconManager (and its cache) is recreated on
every LoadSession, so no bitmaps survive a reload.

Also:
- Update.smartbutton: a failed update-availability check no longer
  deactivates the button (it only skips the icon swap and logs at debug);
  only the user_can_update gate returns False. Previously any transient
  git/network error during load disabled the one tool that performs the
  update.
- Add "Test Reload Icon Toggle" SmartButton to pyRevitDevTools Bundle
  Tests pulldown: clicking toggles a state persisted in the user config
  ini; __selfinit__ re-applies the matching icon on load. After a reload
  the icon must match the stored state, distinguishing "__selfinit__ not
  re-run" from "bundle icon not restored".

@devloai devloai Bot left a comment

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.

PR Summary:

  • Fixes SmartButtons nested in pulldown/split buttons losing __selfinit__-driven icon/enabled state after a reload, by re-running ExecuteSelfInit in PulldownButtonBuilder.UpdateExistingChildren and SplitButtonBuilder.UpdateExistingChildren (mirrors the existing PushButtonBuilder.UpdateExistingButton pattern).
  • Hardens Update.smartbutton's __selfinit__ so a transient update-check failure no longer deactivates the button for the whole session — only the user_can_update gate can now disable it.
  • Adds a new Test Reload Icon Toggle DevTools SmartButton to manually verify reload behavior for nested SmartButtons.

Review Summary:

Traced the fix against the sibling implementations (PushButtonBuilder, StackBuilder, SmartButtonScriptInitializer.ExecuteSelfInit) and confirmed the new code faithfully mirrors the established create-path pattern (reset Enabled/Visible, re-run self-init, honor a false return) with no null-ref or ordering issues. The Update.smartbutton control-flow change was verified line-by-line against the diff and correctly always returns True after the try/except unless the permission gate short-circuits. The new DevTools bundle's config key naming is consistent between __selfinit__/__main__, and its icon fallback (missing on.dark.png) degrades safely via SmartButtonContext.get_bundle_file. Checked repo-wide guidelines (IPY2712 compatibility, PEP8, Black/style consistency) — no violations, and the unused off.png/off.dark.png assets match an existing convention already present elsewhere in the same extension, so not flagged as a new deviation. One code-smell comment was recorded: the "run self-init / disable on false" block is now duplicated across 6 call sites in 3 files, which is worth consolidating into a shared ButtonBuilderBase helper to avoid future drift (this duplication is arguably what caused the original bug).

Suggestions

  • Extract the duplicated SmartButton self-init logic into a shared ButtonBuilderBase helper. Apply
  • Add automated tests covering SmartButton reload behavior in Pulldown/SplitButtonBuilder. Apply


// Handle SmartButton-specific initialization on update;
// __selfinit__ must re-run after post-processing since it may override the bundle icon
if (sub.Type == CommandComponentType.SmartButton && _smartButtonScriptInitializer != null)

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 "run ExecuteSelfInit, disable on false, log" block is now duplicated almost verbatim in 6 places: PushButtonBuilder.Build and UpdateExistingButton, PulldownButtonBuilder.AddSingleChildToPulldown (existing) and here, and SplitButtonBuilder.AddSingleChildToSplitButton (existing) and its UpdateExistingChildren counterpart. Since ButtonBuilderBase already centralizes similar cross-cutting helpers (UpdatePushButtonCommandBinding, DeactivateRibbonItem), consider factoring this into a shared helper there, e.g.:

protected void ExecuteSmartButtonSelfInitIfNeeded(
    ParsedComponent sub,
    PushButton btn,
    SmartButtonScriptInitializer? initializer,
    string context)
{
    if (sub.Type != CommandComponentType.SmartButton || initializer == null)
        return;

    if (!initializer.ExecuteSelfInit(sub, btn))
    {
        btn.Enabled = false;
        Logger.Debug($"SmartButton '{sub.DisplayName}' in {context} deactivated by __selfinit__.");
    }
}

This would prevent the two behaviors (create-path vs. update-path re-init) from drifting apart again in the future, which is effectively what caused the bug this PR fixes.

actions

Feedback: Rate this comment to help me improve future code reviews:

  • 👍 Good - Helpful and accurate
  • 👎 Poor - Wrong, unclear, or unhelpful
  • Skip if you don't have any strong opinions either way.

Copilot AI left a comment

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.

Pull request overview

This PR fixes a C# loader reload behavior where SmartButtons nested inside pulldown and split buttons were not re-running __selfinit__ during the “update existing UI” reload path, causing icon and enabled/disabled state to drift after reloads. It also hardens the Update SmartButton so transient update-check failures don’t deactivate the button, and adds a DevTools SmartButton to validate the behavior.

Changes:

  • Re-run SmartButton __selfinit__ (via SmartButtonScriptInitializer.ExecuteSelfInit) after post-processing when updating existing children in pulldown/split containers, disabling the button if __selfinit__ returns False.
  • Make Update.smartbutton log update-check failures but remain active (only user_can_update can deactivate it).
  • Add a DevTools “Test Reload Icon Toggle” SmartButton (plus menu entry) to verify icon persistence across reloads inside a pulldown.

Reviewed changes

Copilot reviewed 6 out of 11 changed files in this pull request and generated no comments.

Show a summary per file
File Description
extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Bundle Tests.pulldown/Test Reload Icon Toggle.smartbutton/script.py New SmartButton that persists a toggle state in config and applies an on-icon in __selfinit__ to validate reload behavior.
extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Bundle Tests.pulldown/Test Reload Icon Toggle.smartbutton/bundle.yaml Adds title/tooltip metadata describing the reload test and expected icon behavior.
extensions/pyRevitDevTools.extension/pyRevitDev.tab/Debug.panel/Bundle Tests.pulldown/bundle.yaml Adds the new test SmartButton to the pulldown layout.
extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Update.smartbutton/script.py Prevents transient update-check failures from disabling the Update tool; keeps the default icon unless an update is detected.
dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/Buttons/SplitButtonBuilder.cs Re-runs SmartButton __selfinit__ during reload updates for existing split-button children, after post-processing.
dev/pyRevitLoader/pyRevitAssemblyBuilder/UIManager/Buttons/PulldownButtonBuilder.cs Re-runs SmartButton __selfinit__ during reload updates for existing pulldown children, after post-processing.

@ChrisCrosley
ChrisCrosley marked this pull request as ready for review July 7, 2026 13:40
@Wurschdhaud

Copy link
Copy Markdown
Contributor

I was not able to reproduce the stale icon issue with this button directly.

That one is a pita to test.

  • enable check for updates in settings
  • create a github repo for an extension, with idk a hello world button
  • add ext to pyrevit
  • close revit
  • open revit → the update.smartbutton will be greyish
  • close revit
  • add a new commit to the hello world ext
  • open revit → the update.smartbutton will be orange
  • click the update
  • ext will update
  • old launcher update icon will now be greyish (as no new updates are available)
  • new launcher update icon will stay orange, as selfinit isn't rerun properly, you have to reopen revit for it to go greyish

@Wurschdhaud

Copy link
Copy Markdown
Contributor

i have not tried this PR because it's so annyoing to test

@ChrisCrosley

Copy link
Copy Markdown
Contributor Author

Hmm. So maybe this PR does not address the update button at all... I think the changes I made to the __selfinit__ drop the gray icon when when no updates are available. I may need a small follow-up commit here to fix that.

However, I feel pretty confident that if the bug does exist, it is something within the Update script, not the UI builder. Other SmartButtons are working correctly.

@ChrisCrosley

Copy link
Copy Markdown
Contributor Author

Ok, Claude to the rescue here. I was able to have it automate all the PITA parts of the test with a local repo 'helloworld' extension. The button state starts orange when an update is available and resets to the default state after a successful run without a Revit restart. I think this is as tested as we can get for this one.

  1. Scaffolded the HelloWorldTest extension (tab/panel/pushbutton + script + icon). (Claude)
  2. Created the bare "remote", committed, set upstream tracking (baseline = up-to-date). (Claude)
  3. Registered the extensions search path; set checkupdates=on, autoupdate=off; verified new_loader/usercanupdate already on. (Claude)
  4. Verified clean baseline — pyRevit clone + hello-world ext both 0 behind. (Claude)
  5. Launch 1 — opened Revit, observed Update button = default (no updates), confirmed HelloWorld tab loaded, closed Revit. (Me)
  6. Verified Launch 1 log: check_for_updates ran, all 3 repos "up-to-date". (Claude)
  7. Staged a pending update — pushed a new commit to the ext's remote; confirmed ext clone now behind by 1. (Claude)
  8. Launch 2 — opened Revit → Update button orange; clicked Update → ext pulled + session reloaded; observed icon return to default without restart; reported success. (Me)
  9. Verified Launch 2 log end-to-end: orange trigger → pull to 6d64fcc → reload → post-reload selfinit re-ran → up-to-date → icon reset; concluded the fix works. (Claude)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: SmartButton icons set via __selfinit__ do not update during reload in C# launcher

3 participants