Java tutorial
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.openapi.actionSystem.ex; import com.intellij.ide.DataManager; import com.intellij.ide.actions.ActionsCollector; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ApplicationNamesInfo; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.IndexNotReadyException; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Computable; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.ThrowableComputable; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.ui.ComponentUtil; import com.intellij.util.ObjectUtils; import com.intellij.util.PausesStat; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; import java.awt.event.ActionListener; import java.awt.event.InputEvent; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.Predicate; public final class ActionUtil { private static final Logger LOG = Logger.getInstance(ActionUtil.class); @NonNls private static final String WAS_ENABLED_BEFORE_DUMB = "WAS_ENABLED_BEFORE_DUMB"; @NonNls public static final String WOULD_BE_ENABLED_IF_NOT_DUMB_MODE = "WOULD_BE_ENABLED_IF_NOT_DUMB_MODE"; @NonNls private static final String WOULD_BE_VISIBLE_IF_NOT_DUMB_MODE = "WOULD_BE_VISIBLE_IF_NOT_DUMB_MODE"; @NonNls private static final Key<ActionUpdateData> ACTION_UPDATE_DATA = Key.create("ACTION_UPDATE_DATA"); private ActionUtil() { } public static void showDumbModeWarning(AnActionEvent @NotNull... events) { Project project = null; List<String> actionNames = new ArrayList<>(); for (final AnActionEvent event : events) { final String s = event.getPresentation().getText(); if (StringUtil.isNotEmpty(s)) { actionNames.add(s); } final Project _project = event.getProject(); if (_project != null && project == null) { project = _project; } } if (project == null) { return; } if (LOG.isDebugEnabled()) { LOG.debug("Showing dumb mode warning for " + Arrays.asList(events), new Throwable()); } DumbService.getInstance(project).showDumbModeNotification(getActionUnavailableMessage(actionNames)); } @NotNull private static String getActionUnavailableMessage(@NotNull List<String> actionNames) { String message; if (actionNames.isEmpty()) { message = getUnavailableMessage("This action", false); } else if (actionNames.size() == 1) { message = getUnavailableMessage("'" + actionNames.get(0) + "'", false); } else { message = getUnavailableMessage("None of the following actions", true) + ": " + StringUtil.join(actionNames, ", "); } return message; } @NotNull public static String getUnavailableMessage(@NotNull String action, boolean plural) { return action + (plural ? " are" : " is") + " not available while " + ApplicationNamesInfo.getInstance().getProductName() + " is updating indices"; } /** * Calculates time spent for update, * remember average time (with exponential smoothing) and caches update results inside action.getTemplatePresentation().getClientProperty(ACTION_UPDATE_DATA), * if average time is quite big then skip update invocation and use cached presentation. * @param forceUseCached use cached results for slow actions if presented (relax time doesn't take into account) */ public static void performFastUpdate(boolean isInModalContext, @NotNull AnAction action, @NotNull AnActionEvent event, boolean forceUseCached) { final Presentation templatePresentation = action.getTemplatePresentation(); ActionUpdateData ud = templatePresentation.getClientProperty(ACTION_UPDATE_DATA); if (ud == null) templatePresentation.putClientProperty(ACTION_UPDATE_DATA, ud = new ActionUpdateData()); final boolean isSlow = ud.averageUpdateDurationMs > 10;// empiric val: 10 ms final long startTimeNs = System.nanoTime(); final long relaxMs = Math.min(ud.averageUpdateDurationMs * 100, 10000); // empiric vals: min 1 sec, max 10 sec if (isSlow && ud.lastUpdateEvent != null && (forceUseCached || (startTimeNs - ud.lastUpdateTimeNs) / 1000000L < relaxMs)) { // System.out.println("use cached presentation for action '" + String.valueOf(action) + "', averageUpdateDuration=" + ud.averageUpdateDurationMs + " ms, " + (startTimeNs - ud.lastUpdateTimeNs)/1000000l + " ms elapsed from last update"); event.getPresentation().copyFrom(ud.lastUpdateEvent.getPresentation()); return; } performDumbAwareUpdate(isInModalContext, action, event, false); final long finishUpdateNs = System.nanoTime(); ud.lastUpdateTimeNs = finishUpdateNs; ud.lastUpdateEvent = event; final float smoothAlpha = isSlow ? 0.8f : 0.3f; final float smoothCoAlpha = 1 - smoothAlpha; final long spentMs = (finishUpdateNs - startTimeNs) / 1000000L; ud.averageUpdateDurationMs = Math.round(spentMs * smoothAlpha + ud.averageUpdateDurationMs * smoothCoAlpha); } private static int insidePerformDumbAwareUpdate; /** * @param action action * @param e action event * @param beforeActionPerformed whether to call * {@link AnAction#beforeActionPerformedUpdate(AnActionEvent)} * or * {@link AnAction#update(AnActionEvent)} * @return true if update tried to access indices in dumb mode */ public static boolean performDumbAwareUpdate(boolean isInModalContext, @NotNull AnAction action, @NotNull AnActionEvent e, boolean beforeActionPerformed) { final Presentation presentation = e.getPresentation(); final Boolean wasEnabledBefore = (Boolean) presentation.getClientProperty(WAS_ENABLED_BEFORE_DUMB); final boolean dumbMode = isDumbMode(e.getProject()); if (wasEnabledBefore != null && !dumbMode) { presentation.putClientProperty(WAS_ENABLED_BEFORE_DUMB, null); presentation.setEnabled(wasEnabledBefore.booleanValue()); presentation.setVisible(true); } final boolean enabledBeforeUpdate = presentation.isEnabled(); boolean allowed = (!dumbMode || action.isDumbAware()) && (!Registry.is("actionSystem.honor.modal.context") || !isInModalContext || action.isEnabledInModalContext()); String presentationText = presentation.getText(); boolean edt = ApplicationManager.getApplication().isDispatchThread(); if (edt && insidePerformDumbAwareUpdate++ == 0) { ActionPauses.STAT.started(); } action.applyTextOverride(e); try { if (beforeActionPerformed) { action.beforeActionPerformedUpdate(e); } else { action.update(e); } presentation.putClientProperty(WOULD_BE_ENABLED_IF_NOT_DUMB_MODE, !allowed && presentation.isEnabled()); presentation.putClientProperty(WOULD_BE_VISIBLE_IF_NOT_DUMB_MODE, !allowed && presentation.isVisible()); } catch (IndexNotReadyException e1) { if (!allowed) { return true; } throw e1; } finally { if (edt && --insidePerformDumbAwareUpdate == 0) { ActionPauses.STAT.finished(presentationText + " action update (" + action.getClass() + ")"); } if (!allowed) { if (wasEnabledBefore == null) { presentation.putClientProperty(WAS_ENABLED_BEFORE_DUMB, enabledBeforeUpdate); } presentation.setEnabled(false); } } return false; } /** * @deprecated use {@link #performDumbAwareUpdate(boolean, AnAction, AnActionEvent, boolean)} instead */ @Deprecated public static boolean performDumbAwareUpdate(@NotNull AnAction action, @NotNull AnActionEvent e, boolean beforeActionPerformed) { return performDumbAwareUpdate(false, action, e, beforeActionPerformed); } /** * Show a cancellable modal progress running the given computation under read action with the same {@link DumbService#isAlternativeResolveEnabled()} * as the caller. To be used in actions which need to perform potentially long-running computations synchronously without freezing UI. * @throws ProcessCanceledException if the user has canceled the progress. If the action can be safely stopped at this point * without leaving inconsistent data behind, this exception doesn't need to be caught and processed. */ public static <T> T underModalProgress(@NotNull Project project, @NotNull @Nls(capitalization = Nls.Capitalization.Title) String progressTitle, @NotNull Computable<T> computable) throws ProcessCanceledException { DumbService dumbService = DumbService.getInstance(project); boolean useAlternativeResolve = dumbService.isAlternativeResolveEnabled(); return ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> { if (useAlternativeResolve) { dumbService.setAlternativeResolveEnabled(true); } try { ThrowableComputable<T, RuntimeException> inReadAction = () -> ApplicationManager.getApplication() .runReadAction(computable); return ProgressManager.getInstance().computePrioritized(inReadAction); } finally { if (useAlternativeResolve) { dumbService.setAlternativeResolveEnabled(false); } } }, progressTitle, true, project); } public static class ActionPauses { public static final PausesStat STAT = new PausesStat("AnAction.update()"); } /** * @return whether a dumb mode is in progress for the passed project or, if the argument is null, for any open project. * @see DumbService */ public static boolean isDumbMode(@Nullable Project project) { if (project != null) { return DumbService.getInstance(project).isDumb(); } for (Project openProject : ProjectManager.getInstance().getOpenProjects()) { if (DumbService.getInstance(openProject).isDumb()) { return true; } } return false; } public static boolean lastUpdateAndCheckDumb(AnAction action, AnActionEvent e, boolean visibilityMatters) { performDumbAwareUpdate(false, action, e, true); final Project project = e.getProject(); if (project != null && DumbService.getInstance(project).isDumb() && !action.isDumbAware()) { if (Boolean.FALSE.equals(e.getPresentation().getClientProperty(WOULD_BE_ENABLED_IF_NOT_DUMB_MODE))) { return false; } if (visibilityMatters && Boolean.FALSE .equals(e.getPresentation().getClientProperty(WOULD_BE_VISIBLE_IF_NOT_DUMB_MODE))) { return false; } showDumbModeWarning(e); return false; } if (!e.getPresentation().isEnabled()) { return false; } return !visibilityMatters || e.getPresentation().isVisible(); } public static void performActionDumbAwareWithCallbacks(@NotNull AnAction action, @NotNull AnActionEvent e, @NotNull DataContext context) { final ActionManagerEx manager = ActionManagerEx.getInstanceEx(); manager.fireBeforeActionPerformed(action, context, e); performActionDumbAware(action, e); manager.fireAfterActionPerformed(action, context, e); } public static void performActionDumbAware(AnAction action, AnActionEvent e) { try { action.actionPerformed(e); } catch (IndexNotReadyException ex) { LOG.info(ex); showDumbModeWarning(e); } } @NotNull public static AnActionEvent createEmptyEvent() { return AnActionEvent.createFromDataContext(ActionPlaces.UNKNOWN, null, dataId -> null); } public static void sortAlphabetically(@NotNull List<? extends AnAction> list) { list.sort((o1, o2) -> Comparing.compare(o1.getTemplateText(), o2.getTemplateText())); } /** * Tries to find an 'action' and 'target action' by text and put the 'action' just before of after the 'target action' */ public static void moveActionTo(@NotNull List<AnAction> list, @NotNull String actionText, @NotNull String targetActionText, boolean before) { if (Comparing.equal(actionText, targetActionText)) { return; } int actionIndex = -1; int targetIndex = -1; for (int i = 0; i < list.size(); i++) { AnAction action = list.get(i); if (actionIndex == -1 && Comparing.equal(actionText, action.getTemplateText())) actionIndex = i; if (targetIndex == -1 && Comparing.equal(targetActionText, action.getTemplateText())) targetIndex = i; if (actionIndex != -1 && targetIndex != -1) { if (actionIndex < targetIndex) targetIndex--; AnAction anAction = list.remove(actionIndex); list.add(before ? Math.max(0, targetIndex) : targetIndex + 1, anAction); return; } } } @NotNull public static List<AnAction> getActions(@NotNull JComponent component) { return ContainerUtil.notNullize(ComponentUtil.getClientProperty(component, AnAction.ACTIONS_KEY)); } public static void clearActions(@NotNull JComponent component) { ComponentUtil.putClientProperty(component, AnAction.ACTIONS_KEY, null); } public static void copyRegisteredShortcuts(@NotNull JComponent to, @NotNull JComponent from) { for (AnAction anAction : getActions(from)) { anAction.registerCustomShortcutSet(anAction.getShortcutSet(), to); } } public static void registerForEveryKeyboardShortcut(@NotNull JComponent component, @NotNull ActionListener action, @NotNull ShortcutSet shortcuts) { for (Shortcut shortcut : shortcuts.getShortcuts()) { if (shortcut instanceof KeyboardShortcut) { KeyboardShortcut ks = (KeyboardShortcut) shortcut; KeyStroke first = ks.getFirstKeyStroke(); KeyStroke second = ks.getSecondKeyStroke(); if (second == null) { component.registerKeyboardAction(action, first, JComponent.WHEN_IN_FOCUSED_WINDOW); } } } } public static void recursiveRegisterShortcutSet(@NotNull ActionGroup group, @NotNull JComponent component, @Nullable Disposable parentDisposable) { for (AnAction action : group.getChildren(null)) { if (action instanceof ActionGroup) { recursiveRegisterShortcutSet((ActionGroup) action, component, parentDisposable); } action.registerCustomShortcutSet(component, parentDisposable); } } public static boolean recursiveContainsAction(@NotNull ActionGroup group, @NotNull AnAction action) { return anyActionFromGroupMatches(group, true, Predicate.isEqual(action)); } public static boolean anyActionFromGroupMatches(@NotNull ActionGroup group, boolean processPopupSubGroups, @NotNull Predicate<? super AnAction> condition) { for (AnAction child : group.getChildren(null)) { if (condition.test(child)) return true; if (child instanceof ActionGroup) { ActionGroup childGroup = (ActionGroup) child; if ((processPopupSubGroups || !childGroup.isPopup()) && anyActionFromGroupMatches(childGroup, processPopupSubGroups, condition)) { return true; } } } return false; } /** * Convenience method for copying properties from a registered action * * @param actionId action id */ public static AnAction copyFrom(@NotNull AnAction action, @NotNull String actionId) { AnAction from = ActionManager.getInstance().getAction(actionId); if (from != null) { action.copyFrom(from); } ActionsCollector.getInstance().onActionConfiguredByActionId(action, actionId); return action; } /** * Convenience method for merging not null properties from a registered action * * @param action action to merge to * @param actionId action id to merge from */ public static AnAction mergeFrom(@NotNull AnAction action, @NotNull String actionId) { //noinspection UnnecessaryLocalVariable AnAction a1 = action; AnAction a2 = ActionManager.getInstance().getAction(actionId); Presentation p1 = a1.getTemplatePresentation(); Presentation p2 = a2.getTemplatePresentation(); p1.setIcon(ObjectUtils.chooseNotNull(p1.getIcon(), p2.getIcon())); p1.setDisabledIcon(ObjectUtils.chooseNotNull(p1.getDisabledIcon(), p2.getDisabledIcon())); p1.setSelectedIcon(ObjectUtils.chooseNotNull(p1.getSelectedIcon(), p2.getSelectedIcon())); p1.setHoveredIcon(ObjectUtils.chooseNotNull(p1.getHoveredIcon(), p2.getHoveredIcon())); if (StringUtil.isEmpty(p1.getText())) { p1.setTextWithMnemonic(p2.getTextWithPossibleMnemonic()); } p1.setDescription(ObjectUtils.chooseNotNull(p1.getDescription(), p2.getDescription())); ShortcutSet ss1 = a1.getShortcutSet(); if (ss1 == CustomShortcutSet.EMPTY) { a1.copyShortcutFrom(a2); } ActionsCollector.getInstance().onActionConfiguredByActionId(action, actionId); return a1; } public static void invokeAction(@NotNull AnAction action, @NotNull Component component, @NotNull String place, @Nullable InputEvent inputEvent, @Nullable Runnable onDone) { invokeAction(action, DataManager.getInstance().getDataContext(component), place, inputEvent, onDone); } public static void invokeAction(@NotNull AnAction action, @NotNull DataContext dataContext, @NotNull String place, @Nullable InputEvent inputEvent, @Nullable Runnable onDone) { Presentation presentation = action.getTemplatePresentation().clone(); AnActionEvent event = new AnActionEvent(inputEvent, dataContext, place, presentation, ActionManager.getInstance(), 0); performDumbAwareUpdate(false, action, event, true); final ActionManagerEx manager = ActionManagerEx.getInstanceEx(); if (event.getPresentation().isEnabled() && event.getPresentation().isVisible()) { manager.fireBeforeActionPerformed(action, dataContext, event); performActionDumbAware(action, event); if (onDone != null) { onDone.run(); } manager.fireAfterActionPerformed(action, dataContext, event); } } @NotNull public static ActionListener createActionListener(@NotNull String actionId, @NotNull Component component, @NotNull String place) { return e -> { AnAction action = ActionManager.getInstance().getAction(actionId); if (action == null) { LOG.warn("Can not find action by id " + actionId); return; } invokeAction(action, component, place, null, null); }; } private static class ActionUpdateData { AnActionEvent lastUpdateEvent; long lastUpdateTimeNs = 0; long averageUpdateDurationMs = 0; } }