Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit e9b498d

Browse files
πŸ› Fix DeviceSelectorAction NoSuchElementException in the toolbar layout (#8515)
(originally #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.DeviceSelectorAction1ドル[...] 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 #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)

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /