beansoft / flutter-storm-support

🧩 a WebStorm/PhpStorm/GoLand plugin for developing Flutter applications
https://plugins.jetbrains.com/plugin/14718-flutter-storm
BSD 3-Clause "New" or "Revised" License
4 stars 1 forks source link

IDEA 2021 EAP device update issue #20

Closed beansoft closed 3 years ago

beansoft commented 3 years ago

Trying to reset custom component in a presentation

java.lang.Throwable at com.intellij.openapi.actionSystem.Presentation.putClientProperty(Presentation.java:403) at com.intellij.openapi.actionSystem.Presentation.copyFrom(Presentation.java:376) at com.intellij.openapi.actionSystem.impl.ActionUpdater.lambda$reflectSubsequentChangesInOriginalPresentation$9(ActionUpdater.java:131) at java.desktop/java.beans.PropertyChangeSupport.fire(PropertyChangeSupport.java:341) at java.desktop/java.beans.PropertyChangeSupport.firePropertyChange(PropertyChangeSupport.java:333) at java.desktop/java.beans.PropertyChangeSupport.firePropertyChange(PropertyChangeSupport.java:266) at com.intellij.openapi.actionSystem.Presentation.fireObjectPropertyChange(Presentation.java:342) at com.intellij.openapi.actionSystem.Presentation.setTextWithMnemonic(Presentation.java:190) at com.intellij.openapi.actionSystem.Presentation.setText(Presentation.java:145) at com.intellij.openapi.actionSystem.Presentation.setText(Presentation.java:200) at io.flutter.actions.DeviceSelectorAction.updateActions(DeviceSelectorAction.java:165)

beansoft commented 3 years ago

I've using similar code in mine React Native plugin to update iOS simulator list, so I encounter same issue in 2021 EAP, my temp fix is like this:

  1. move all update presentation codes out of async thread
  2. when devices thread updating finished, call // Notify the IDE system to update AnAction ActivityTracker.getInstance().inc();

Still not sure the reason why this fails since the "Trying to reset custom component in a presentation" error check in Presentation.java is not changed long time.

Below is just for demonstrate for what's i'm doing, please using your own latest code:

public class DeviceSelectorAction extends ComboBoxAction implements DumbAware {
  private final List<AnAction> actions = new ArrayList<>();
  private final List<Project> knownProjects = Collections.synchronizedList(new ArrayList<>());

  private SelectDeviceAction selectedDeviceAction;

  public DeviceSelectorAction() {
    setSmallVariant(true);
  }

  @NotNull
  @Override
  protected DefaultActionGroup createPopupActionGroup(JComponent button) {
    final DefaultActionGroup group = new DefaultActionGroup();
    group.addAll(actions);
    return group;
  }

  @Override
  protected boolean shouldShowDisabledActions() {
    return true;
  }

  @Override
  public void update(final AnActionEvent e) {
    //// Suppress device actions in all but the toolbars.
    //final String place = e.getPlace();
    //if (!Objects.equals(place, ActionPlaces.NAVIGATION_BAR_TOOLBAR) && !Objects.equals(place, ActionPlaces.MAIN_TOOLBAR)) {
    //  e.getPresentation().setVisible(false);
    //  return;
    //}

    // Only show device menu when the device daemon process is running.
    final Project project = e.getProject();
    if (!isSelectorVisible(project)) {
      e.getPresentation().setVisible(false);
      return;
    }

    super.update(e);

    if (!knownProjects.contains(project)) {
      knownProjects.add(project);
      Disposer.register(project, () -> knownProjects.remove(project));

      DeviceService.getInstance(project).addListener(() -> update(project, e.getPresentation()));

      // Listen for android device changes, and rebuild the menu if necessary.
      AndroidEmulatorManager.getInstance(project).addListener(() -> update(project, e.getPresentation()));

      update(project, e.getPresentation());
    }

    final DeviceService deviceService = DeviceService.getInstance(project);

    final FlutterDevice selectedDevice = deviceService.getSelectedDevice();
    final Collection<FlutterDevice> devices = deviceService.getConnectedDevices();

    Presentation presentation = e.getPresentation();
    final boolean visible = isSelectorVisible(project);
    presentation.setVisible(visible);

    if (devices.isEmpty()) {
      final boolean isLoading = deviceService.getStatus() == DeviceService.State.LOADING;
      if (isLoading) {
        presentation.setText(FlutterBundle.message("devicelist.loading"));
      }
      else {
        //noinspection DialogTitleCapitalization
        presentation.setText("<no devices>");
      }
    }
    else if (selectedDevice == null) {
      //noinspection DialogTitleCapitalization
      presentation.setText("<no device selected>");
    } else if(selectedDeviceAction != null) {
      final Presentation template = selectedDeviceAction.getTemplatePresentation();
      presentation.setIcon(template.getIcon());
      presentation.setText(selectedDevice.presentationName());
      presentation.setEnabled(true);
    }
  }

  private void update(Project project, Presentation presentation) {
    FlutterUtils.invokeAndWait(() -> {
      updateActions(project, presentation);
      updateVisibility(project, presentation);
    });
  }

  private static void updateVisibility(final Project project, final Presentation presentation) {
    final boolean visible = isSelectorVisible(project);
    //presentation.setVisible(visible);

    final JComponent component = (JComponent)presentation.getClientProperty("customComponent");
    if (component != null) {
      component.setVisible(visible);
      if (component.getParent() != null) {
        component.getParent().doLayout();
        component.getParent().repaint();
      }
    }
  }

  private static boolean isSelectorVisible(@Nullable Project project) {
    return project != null &&
           DeviceService.getInstance(project).getStatus() != DeviceService.State.INACTIVE &&
           FlutterModuleUtils.hasFlutterModule(project);
  }

  private void updateActions(@NotNull Project project, Presentation presentation) {
    actions.clear();

    final DeviceService deviceService = DeviceService.getInstance(project);

    final FlutterDevice selectedDevice = deviceService.getSelectedDevice();
    final Collection<FlutterDevice> devices = deviceService.getConnectedDevices();

    selectedDeviceAction = null;

    for (FlutterDevice device : devices) {
      final SelectDeviceAction deviceAction = new SelectDeviceAction(device, devices);
      actions.add(deviceAction);

      if (Objects.equals(device, selectedDevice)) {
        selectedDeviceAction = deviceAction;
      }
    }

    // Show the 'Open iOS Simulator' action.
    if (SystemInfo.isMac) {
      boolean simulatorOpen = false;
      for (AnAction action : actions) {
        if (action instanceof SelectDeviceAction) {
          final SelectDeviceAction deviceAction = (SelectDeviceAction)action;
          final FlutterDevice device = deviceAction.device;
          if (device.isIOS() && device.emulator()) {
            simulatorOpen = true;
          }
        }
      }

      actions.add(new Separator());
      actions.add(new OpenSimulatorAction(!simulatorOpen));
      actions.add(new RefreshDeviceAction());
    }

    // Add Open Android emulators actions.
    final List<OpenEmulatorAction> emulatorActions = OpenEmulatorAction.getEmulatorActions(project);
    if (!emulatorActions.isEmpty()) {
      actions.add(new Separator());
      actions.addAll(emulatorActions);
    }

    // Notify the IDE system to update AnAction
    ActivityTracker.getInstance().inc();
  }

  // Show the current device as selected when the combo box menu opens.
  @Override
  protected Condition<AnAction> getPreselectCondition() {
    return action -> action == selectedDeviceAction;
  }

  private static class SelectDeviceAction extends AnAction {
    @NotNull
    private final FlutterDevice device;

    SelectDeviceAction(@NotNull FlutterDevice device, @NotNull Collection<FlutterDevice> devices) {
      super(device.getUniqueName(devices), null, FlutterIcons.Phone);
      this.device = device;
    }

    public String presentationName() {
      return device.presentationName();
    }

    @Override
    public void actionPerformed(AnActionEvent e) {
      final Project project = e.getProject();
      final DeviceService service = project == null ? null : DeviceService.getInstance(project);
      if (service != null) {
        service.setSelectedDevice(device);
      }
    }
  }
}