Skip to content

Fixed: ClassCastException in ToolWindowBadgeUpdater$BadgeIcon due to missing ScalableIcon implementation on IDEA and Android Studio #8440

@ArdaKoksall

Description

@ArdaKoksall
  • Flutter IntelliJ Plugin Version: 87.0
  • IDE: Android Studio 2025.1.2 Narwhal Feature Drop
  • Operating System: Windows 11

Problem Description

When running a Flutter application, the "Run" tool window icon on the side stripe breaks. It may appear empty, multiple, unclickable, or fail to update its state. This is caused by an unhandled java.lang.ClassCastException that occurs when the IDE attempts to apply a status badge (e.g., a green overlay for a successful run) to the tool window icon.

The core issue is that the IDE's UI framework expects the icon to implement com.intellij.openapi.util.ScalableIcon for proper handling on HiDPI displays, but the ToolWindowBadgeUpdater$BadgeIcon class provided by the Flutter plugin does not.

Full Stack Trace

The following exception is thrown when the tool window icon attempts to update:

java.lang.ClassCastException: class io.flutter.toolwindow.ToolWindowBadgeUpdater$BadgeIcon cannot be cast to class com.intellij.openapi.util.ScalableIcon (io.flutter.toolwindow.ToolWindowBadgeUpdater$BadgeIcon is in unnamed module of loader com.intellij.ide.plugins.cl.PluginClassLoader @4f08d22; com.intellij.openapi.util.ScalableIcon is in unnamed module of loader com.intellij.util.lang.PathClassLoader @4cb2c100)
	at com.intellij.openapi.wm.impl.SquareStripeButtonKt.createPresentation$lambda$0(SquareStripeButton.kt:304)
	at com.intellij.util.concurrency.SynchronizedClearableLazy._get_value_$lambda$1$lambda$0(SynchronizedClearableLazy.kt:41)
	at java.base/java.util.concurrent.atomic.AtomicReference.updateAndGet(Unknown Source)
	at com.intellij.util.concurrency.SynchronizedClearableLazy.getValue(SynchronizedClearableLazy.kt:40)
	at com.intellij.util.concurrency.SynchronizedClearableLazy.get(SynchronizedClearableLazy.kt:28)
	at com.intellij.openapi.actionSystem.Presentation.setIcon(Presentation.java:319)
	at com.intellij.openapi.wm.impl.SquareAnActionButton.isSelected(SquareStripeButton.kt:345)
	at com.intellij.openapi.actionSystem.ToggleAction.update(ToggleAction.java:85)
	at com.intellij.openapi.actionSystem.ex.ActionUtil.performDumbAwareUpdate$lambda$2(ActionUtil.kt:236)
	at com.intellij.openapi.actionSystem.ex.ActionUtil.performDumbAwareUpdate(ActionUtil.kt:254)
	at com.intellij.openapi.actionSystem.impl.ActionButton.update(ActionButton.java:339)

Proposed Solution and Patch

The issue can be resolved by modifying ToolWindowBadgeUpdater$BadgeIcon to correctly implement the ScalableIcon interface, including all methods required by modern platform versions.

Here is the patched version of the class that resolves the problem:

package io.flutter.toolwindow;

import com.intellij.openapi.util.ScalableIcon;
import javax.swing.*;
import java.awt.*;

class ToolWindowBadgeUpdater$BadgeIcon implements Icon, ScalableIcon {
    private final Icon baseIcon;
    private final Color overlayColor;

    public ToolWindowBadgeUpdater$BadgeIcon(Icon baseIcon, Color overlayColor) {
        this.baseIcon = baseIcon;
        this.overlayColor = overlayColor;
    }

    @Override
    public void paintIcon(Component c, Graphics g, int x, int y) {
        baseIcon.paintIcon(c, g, x, y);
        Graphics2D g2d = (Graphics2D) g.create();
        try {
            g2d.translate(x, y);
            g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
            g2d.setColor(overlayColor);
            g2d.fillRect(0, 0, getIconWidth(), getIconHeight());
        } finally {
            g2d.dispose();
        }
    }

    @Override
    public int getIconWidth() {
        return baseIcon.getIconWidth();
    }

    @Override
    public int getIconHeight() {
        return baseIcon.getIconHeight();
    }

    // FIX #1: Implement the scale() method from ScalableIcon.
    // This implementation is robust: it attempts to delegate scaling to the base
    // icon and returns a new BadgeIcon with the result.
    @Override
    public Icon scale(float scaleFactor) {
        if (baseIcon instanceof ScalableIcon) {
            Icon scaledBaseIcon = ((ScalableIcon) baseIcon).scale(scaleFactor);
            return new ToolWindowBadgeUpdater$BadgeIcon(scaledBaseIcon, this.overlayColor);
        }
        return this;
    }

    // FIX #2: Implement the getScale() method, required by modern platform versions.
    // This delegates to the base icon if possible, providing a sensible default.
    @Override
    public float getScale() {
        if (baseIcon instanceof ScalableIcon) {
            return ((ScalableIcon) baseIcon).getScale();
        }
        return 1.0f;
    }
}

Verification and Testing

I have successfully patched the plugin and verified the fix. The process was as follows:

  1. Set up a minimal Gradle project using the org.jetbrains.intellij plugin.
  2. Configured the project to compile against the exact platform version of my IDE (2025.1.2) to avoid ClassLoader conflicts.
  3. Compiled the patched ToolWindowBadgeUpdater$BadgeIcon.class with the fixes above. It was compiled with JDK 21 to match the original plugin's bytecode version (65.0).
  4. Manually updated the flutter-intellij-87.0.jar by injecting the new .class file using the jar command-line tool.

Result:
With the patched class, the ClassCastException is resolved. The Run tool window icon now correctly updates and displays its green badge overlay without crashing, restoring the intended functionality.

Hope this detailed report is helpful for incorporating a permanent fix into an upcoming release.

Image Image Image Image Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions