Skip to content

Commit e9b498d

Browse files
authored
🐛 Fix DeviceSelectorAction NoSuchElementException in the toolbar layout (flutter#8515)
(originally flutter#8496) This PR fixes a `NoSuchElementException` that occurs in the `DeviceSelectorAction` when IntelliJ's toolbar layout system tries to calculate component widths. ## Problem The error manifested as: ``` java.util.NoSuchElementException: Key io.flutter.actions.DeviceSelectorAction$1[...] is missing in the map. at kotlin.collections.MapsKt__MapWithDefaultKt.getOrImplicitDefaultNullable(MapWithDefault.kt:24) at kotlin.collections.MapsKt__MapsKt.getValue(Maps.kt:369) at com.intellij.openapi.actionSystem.toolbarLayout.CompressingLayoutStrategyKt.calculateComponentWidths(CompressingLayoutStrategy.kt:200) ``` ## Root Cause The `getPreferredSize()` method in the anonymous JButton class was being called by IntelliJ's layout system **before** the client properties (`ICON_LABEL_KEY`, `TEXT_LABEL_KEY`, `ARROW_LABEL_KEY`) were set during component initialization. This caused the layout system to fail when trying to register the component in its internal maps because: 1. The method accessed null client properties without proper fallback handling 2. Used unsafe `Objects.requireNonNull(fm)` calls that could throw exceptions 3. The layout system couldn't determine proper component dimensions during initialization ## Solution Enhanced the `getPreferredSize()` method with defensive programming: - **Added fallback logic**: When client properties are null (during initialization), use the same default icons and text that would normally be used - **Safe null checking**: Replaced `Objects.requireNonNull(fm)` with proper null checks - **Reasonable defaults**: Provide sensible sizing estimates using `FlutterIcons.Mobile`, chevron down icon, and "No device selected" text width ```java // Before: Unsafe access width += Objects.requireNonNull(fm).stringWidth(text); // After: Defensive with fallback if (fm != null) { width += fm.stringWidth(text); height = Math.max(height, fm.getHeight()); } ``` ## Impact - Eliminates `NoSuchElementException` during toolbar initialization - Maintains exact same functionality once component is fully initialized - No performance impact - fallback logic only runs during the brief initialization phase - More robust component that gracefully handles IntelliJ's layout timing Fixes flutter#8494.
1 parent 559f9b1 commit e9b498d

File tree

3 files changed

+34
-7
lines changed

3 files changed

+34
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- Set the device selector component to opaque during its creation to avoid an unexpected background color (#8471)
1515
- Refactored `DeviceSelectorAction` and add rich icons to different platform devices (#8475)
1616
- Fix DTD freezes when opening projects, and EDT freezes when the theme is changed and opening embedded DevTools (#8477)
17+
- Fix `DeviceSelectorAction` `NoSuchElementException` in the toolbar layout (#8515)
1718

1819
## 87.1.0
1920

src/io/flutter/FlutterBundle.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ flutter.io.gettingStarted.IDE.url=https://docs.flutter.dev/tools/android-studio
7474
flutter.io.runAndDebug.url=https://docs.flutter.dev/tools/android-studio#running-and-debugging
7575

7676
devicelist.loading=Loading...
77+
devicelist.noDevices=<no devices>
78+
devicelist.noDeviceSelected=<no device selected>
7779

7880
flutter.pop.frame.action.text=Drop Frame (Flutter)
7981
flutter.pop.frame.action.description=Pop the current frame off the stack

src/io/flutter/actions/DeviceSelectorAction.java

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ public class DeviceSelectorAction extends AnAction implements CustomComponentAct
4747
private static final Key<JBLabel> ICON_LABEL_KEY = Key.create("iconLabel");
4848
private static final Key<JBLabel> TEXT_LABEL_KEY = Key.create("textLabel");
4949
private static final Key<JBLabel> ARROW_LABEL_KEY = Key.create("arrowLabel");
50+
private static final @NotNull Icon DEFAULT_DEVICE_ICON = FlutterIcons.Mobile;
51+
private static final @NotNull Icon DEFAULT_ARROW_ICON = IconUtil.scale(AllIcons.General.ChevronDown, null, 1.2f);
5052

5153
private final List<AnAction> actions = new ArrayList<>();
5254
private final List<Project> knownProjects = Collections.synchronizedList(new ArrayList<>());
@@ -87,9 +89,9 @@ public void actionPerformed(@NotNull AnActionEvent e) {
8789

8890
@Override
8991
public @NotNull JComponent createCustomComponent(@NotNull Presentation presentation, @NotNull String place) {
90-
final JBLabel iconLabel = new JBLabel(FlutterIcons.Mobile);
92+
final JBLabel iconLabel = new JBLabel(DEFAULT_DEVICE_ICON);
9193
final JBLabel textLabel = new JBLabel();
92-
final JBLabel arrowLabel = new JBLabel(IconUtil.scale(AllIcons.General.ChevronDown, null, 1.2f));
94+
final JBLabel arrowLabel = new JBLabel(DEFAULT_ARROW_ICON);
9395

9496
// Create a wrapper button for hover effects
9597
final JButton button = new JButton() {
@@ -119,17 +121,39 @@ public Dimension getPreferredSize() {
119121
width += icon.getIconWidth();
120122
height = Math.max(height, icon.getIconHeight());
121123
}
124+
else {
125+
// Fallback: use the default mobile icon size when the component is not fully initialized
126+
final Icon defaultIcon = DEFAULT_DEVICE_ICON;
127+
width += defaultIcon.getIconWidth();
128+
height = Math.max(height, defaultIcon.getIconHeight());
129+
}
122130

131+
final @Nullable FontMetrics fm;
132+
final @NotNull String textLabelText;
123133
if (textLabel instanceof JBLabel label && label.getText() instanceof String text && !text.isEmpty()) {
124-
final FontMetrics fm = label.getFontMetrics(label.getFont());
125-
width += Objects.requireNonNull(fm).stringWidth(text);
134+
fm = label.getFontMetrics(label.getFont());
135+
textLabelText = text;
136+
}
137+
else {
138+
// Fallback: estimate width for typical device name length
139+
fm = getFontMetrics(getFont());
140+
textLabelText = FlutterBundle.message("devicelist.noDevices");
141+
}
142+
if (fm != null) {
143+
width += fm.stringWidth(textLabelText);
126144
height = Math.max(height, fm.getHeight());
127145
}
128146

129147
if (arrowLabel instanceof JBLabel label && label.getIcon() instanceof Icon icon) {
130148
width += icon.getIconWidth();
131149
height = Math.max(height, icon.getIconHeight());
132150
}
151+
else {
152+
// Fallback: use the default arrow icon size
153+
final Icon defaultArrow = DEFAULT_ARROW_ICON;
154+
width += defaultArrow.getIconWidth();
155+
height = Math.max(height, defaultArrow.getIconHeight());
156+
}
133157

134158
width += JBUI.scale(24);
135159
height += JBUI.scale(8);
@@ -278,19 +302,19 @@ public void projectClosing(@NotNull Project project) {
278302
final Collection<FlutterDevice> devices = deviceService.getConnectedDevices();
279303

280304
final String text;
281-
Icon icon = FlutterIcons.Mobile;
305+
Icon icon = DEFAULT_DEVICE_ICON;
282306

283307
if (devices.isEmpty()) {
284308
final boolean isLoading = deviceService.getStatus() == DeviceService.State.LOADING;
285309
if (isLoading) {
286310
text = FlutterBundle.message("devicelist.loading");
287311
}
288312
else {
289-
text = "<no devices>";
313+
text = FlutterBundle.message("devicelist.noDevices");
290314
}
291315
}
292316
else if (selectedDevice == null) {
293-
text = "<no device selected>";
317+
text = FlutterBundle.message("devicelist.noDeviceSelected");
294318
}
295319
else {
296320
text = selectedDevice.presentationName();

0 commit comments

Comments
 (0)