/* * Copyright 2003-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jetbrains.mps.ide.tools; import com.intellij.ide.actions.ActivateToolWindowAction; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.KeyboardShortcut; import com.intellij.openapi.actionSystem.Shortcut; import com.intellij.openapi.keymap.Keymap; import com.intellij.openapi.keymap.KeymapManager; import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.wm.ToolWindow; import com.intellij.openapi.wm.ToolWindowAnchor; import com.intellij.openapi.wm.ToolWindowManager; import com.intellij.ui.content.Content; import com.intellij.ui.content.ContentFactoryImpl; import com.intellij.ui.content.ContentManager; import jetbrains.mps.ide.ThreadUtils; import jetbrains.mps.util.annotation.ToRemove; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.KeyStroke; import java.awt.event.InputEvent; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; public abstract class BaseTool { private final static Logger LOG = LogManager.getLogger(BaseTool.class); private final Project myProject; private final String myId; private Icon myIcon; private Map<String, KeyStroke> myShortcutsByKeymap; private final ToolWindowAnchor myAnchor; private final boolean mySideTool; private boolean myCanCloseContent; private boolean myIsRegistered; private ToolWindowManager myWindowManager; private JComponent myComponent = null; @Deprecated @ToRemove(version = 3.5) public BaseTool(Project project, String id, int number, Icon icon, ToolWindowAnchor anchor, boolean canCloseContent) { this(project, id, number, icon, anchor, false, canCloseContent); } @Deprecated @ToRemove(version = 3.5) public BaseTool(Project project, String id, int number, Icon icon, ToolWindowAnchor anchor, boolean sideTool, boolean canCloseContent) { this(project, id, shortcutsFromNumber(number), icon, anchor, sideTool, canCloseContent); } protected static Map<String, KeyStroke> shortcutsFromNumber(int number) { Map<String, KeyStroke> result = new HashMap<>(); if (number != -1) { result.put(KeymapManager.DEFAULT_IDEA_KEYMAP, KeyStroke.getKeyStroke("alt " + number)); result.put(KeymapManager.MAC_OS_X_KEYMAP, KeyStroke.getKeyStroke("meta " + number)); } return result; } public BaseTool(Project project, String id, Map<String, KeyStroke> shortcutsByKeymap, Icon icon, ToolWindowAnchor anchor, boolean sideTool, boolean canCloseContent) { myAnchor = anchor; mySideTool = sideTool; myShortcutsByKeymap = shortcutsByKeymap; myId = id; myIcon = icon; myCanCloseContent = canCloseContent; myProject = project; myIsRegistered = false; } public String getId() { return myId; } @Deprecated @ToRemove(version = 3.5) public int getNumber() { if (myShortcutsByKeymap != null) { KeyStroke defaultKeystroke = myShortcutsByKeymap.get(KeymapManager.DEFAULT_IDEA_KEYMAP); if (defaultKeystroke != null) { if (defaultKeystroke.getModifiers() == (InputEvent.ALT_MASK | InputEvent.ALT_DOWN_MASK)) { char keyChar = defaultKeystroke.getKeyChar(); if (Character.isDigit(keyChar)) { return Character.digit(keyChar, 10); } } } } return -1; } public Icon getIcon() { return myIcon; } synchronized private boolean isRegistered() { return myIsRegistered; } synchronized private void setIsRegistered(boolean isRegistered) { myIsRegistered = isRegistered; } public boolean toolIsOpened() { ThreadUtils.assertEDT(); return getToolWindow().isVisible(); } /** * Runs {@link jetbrains.mps.ide.tools.BaseTool#openTool} later in EDT event pool. * * @param setActive determine if tool window must be just opened or additionally became active and attract focus */ public void openToolLater(final boolean setActive) { ThreadUtils.runInUIThreadNoWait(() -> openTool(setActive)); } /** * Opens the tool's window, shows tool if invisible at the moment. * Need to be called in EDT. * * @param setActive determine if tool window must be just opened or additionally became active and attract focus */ public void openTool(boolean setActive) { ThreadUtils.assertEDT(); ToolWindow window = getToolWindow(); if (!isAvailable()) { makeAvailable(); } if (!toolIsOpened()) { window.show(null); } if (setActive) { window.activate(null); } } /** * Runs {@link jetbrains.mps.ide.tools.BaseTool#close} later in EDT event pool. */ // TODO: remove unused? public void closeLater() { ThreadUtils.runInUIThreadNoWait(this::close); } /** * Minimizes the window, doesn't remove tool from panel * Need to be called in EDT. */ public void close() { ThreadUtils.assertEDT(); if (isAvailable() && toolIsOpened()) { getToolWindow().hide(null); } } /** * @return whether the tool is visible by user (in the panel) */ public boolean isAvailable() { ThreadUtils.assertEDT(); return getToolWindow().isAvailable(); } public void setAvailable(boolean state) { ThreadUtils.assertEDT(); if (state) { makeAvailable(); } else { makeUnavailable(); } } /** * Runs {@link jetbrains.mps.ide.tools.BaseTool#makeAvailable} later in EDT event pool. */ public void makeAvailableLater() { ThreadUtils.runInUIThreadNoWait(this::makeAvailable); } /** * If the tool is visible, does nothing, else show the tool in panel in minimized state */ public void makeAvailable() { ThreadUtils.assertEDT(); if (!isAvailable()) { getToolWindow().setAvailable(true, null); } } /** * Runs {@link jetbrains.mps.ide.tools.BaseTool#makeUnavailable} later in EDT event pool. */ public void makeUnavailableLater() { ThreadUtils.runInUIThreadNoWait(this::makeUnavailable); } /** * Removes the tool from the panel */ public void makeUnavailable() { ThreadUtils.assertEDT(); if (isAvailable()) { getToolWindow().setAvailable(false, null); } } public ToolWindow getToolWindow() { ThreadUtils.assertEDT(); if (!isRegistered()) { register(); } // register() may fail if myProject hasn't been initialized - ToolWindowManager is a ProjectComponent return myWindowManager == null ? null : myWindowManager.getToolWindow(myId); } @NonNls @NotNull public String getComponentName() { return getClass().getName(); } public void registerLater() { ThreadUtils.runInUIThreadNoWait(() -> DumbService.getInstance(getProject()).runWhenSmart(this::register)); } public final void register() { if (myProject.isDisposed()) { return; } if (isRegistered()) { return; } ThreadUtils.assertEDT(); setIsRegistered(true); myWindowManager = ToolWindowManager.getInstance(myProject); if (myShortcutsByKeymap != null) { String actionId = ActivateToolWindowAction.getActionIdForToolWindow(myId); List<Keymap> keymaps = new ArrayList<>(myShortcutsByKeymap.size()); for (Entry<String, KeyStroke> keymapItem : myShortcutsByKeymap.entrySet()) { Keymap keymap = KeymapManager.getInstance().getKeymap(keymapItem.getKey()); if (keymap == null) { LOG.warn("Keymap " + keymapItem.getKey() + " cannot be found"); return; } keymaps.add(keymap); } // keymaps topsort here is needed because we need to remove inherited shortcuts if they are overwritten Collections.sort(keymaps, new Comparator<Keymap>() { @Override public int compare(Keymap o1, Keymap o2) { for (Keymap parent = o1.getParent(); parent != null; parent = parent.getParent()) { if (parent.equals(o2)) { return 1; } } for (Keymap parent = o2.getParent(); parent != null; parent = parent.getParent()) { if (parent.equals(o1)) { return -1; } } return 0; } }); for (Keymap keymap : keymaps) { KeyboardShortcut defShortcut = new KeyboardShortcut(myShortcutsByKeymap.get(keymap.getName()), null); keymap.removeAllActionShortcuts(actionId); keymap.addShortcut(actionId, defShortcut); } } //if we create a new project, tool windows are created for it automatically ToolWindow toolWindow = myWindowManager.getToolWindow(myId); if (toolWindow == null) { toolWindow = myWindowManager.registerToolWindow(myId, myCanCloseContent, myAnchor, getProject(), true, mySideTool); } toolWindow.setIcon(myIcon); toolWindow.setToHideOnEmptyContent(true); toolWindow.installWatcher(toolWindow.getContentManager()); setAvailable(isInitiallyAvailable()); doRegister(); if (myComponent == null) { myComponent = getComponent(); } if (myComponent != null) { addContent(myComponent, "", null, false); } toolWindow.setToHideOnEmptyContent(true); toolWindow.installWatcher(toolWindow.getContentManager()); setAvailable(isInitiallyAvailable()); } /** * Override this method to add implementation specific registration. * Called in {@link jetbrains.mps.ide.tools.BaseTool#register} before {@link jetbrains.mps.ide.tools.BaseTool#getComponent()}. */ protected void doRegister() { } public int getCurrentTabIndex() { ContentManager contentManager = getContentManager(); return contentManager.getIndexOfContent(contentManager.getSelectedContent()); } protected AnAction createCloseAction() { return new CloseAction(this); } protected boolean isInitiallyAvailable() { return false; } /** * Runs {@link jetbrains.mps.ide.tools.BaseTool#unregister instead} later in EDT event pool. */ // TODO: remove unused? public void unregisterLater() { ThreadUtils.runInUIThreadNoWait(this::unregister); } /** * Unregister Tool and removes all shortcuts in case of reload. * Need to be called in EDT. * <p> * If project is closing (== not in opened projects) { * <p> * }, but there are some other opened projects, * than shortcuts must not be removed - instance of BaseTool still exists for other projects * and shortcuts are global (registered by ActionId). * In case of BaseTool reload (unregister on opened project) we need do this, * because it will register (probably changed) shortcuts back on load. */ public final void unregister() { if (!isRegistered()) { return; } ThreadUtils.assertEDT(); doUnregister(); // see Javadoc for if condition explanation final List<Project> openedProjects = Arrays.asList(ProjectManager.getInstance().getOpenProjects()); if (myShortcutsByKeymap != null && (openedProjects.contains(getProject()) || openedProjects.isEmpty())) { for (Entry<String, KeyStroke> keymapItem : myShortcutsByKeymap.entrySet()) { Keymap keymap = KeymapManager.getInstance().getKeymap(keymapItem.getKey()); if (keymap != null) { keymap.removeAllActionShortcuts(ActivateToolWindowAction.getActionIdForToolWindow(myId)); } } } ToolWindow toolWindow = getToolWindow(); if (toolWindow != null) { ContentManager contentManager = toolWindow.getContentManager(); if (contentManager != null && !contentManager.isDisposed()) { contentManager.removeAllContents(true); } } myWindowManager.unregisterToolWindow(myId); myIsRegistered = false; } /** * Override this method to add implementation specific unregistration. * Called in {@link jetbrains.mps.ide.tools.BaseTool#unregister} before default unregister process. */ protected void doUnregister() { } // TODO: make method abstract - fix jetbrains.mps.ide.findusages.view.UsagesViewTool public JComponent getComponent() { return null; } protected Content addContent(JComponent component, @NotNull String name, Icon icon, boolean isLockable) { Content content = new ContentFactoryImpl().createContent(component, name, isLockable); if (icon != null) { content.putUserData(ToolWindow.SHOW_CONTENT_ICON, Boolean.TRUE); content.setIcon(icon); } else { content.setIcon(myIcon); } ContentManager contentManager = getContentManager(); contentManager.addContent(content); return content; } public void setSelectedComponent(JComponent component) { ContentManager manager = getContentManager(); Content content = manager.getContent(component); manager.setSelectedContent(content); } protected ContentManager getContentManager() { if (!isRegistered()) { register(); } if (getToolWindow() == null) { return null; } return getToolWindow().getContentManager(); } @Override public String toString() { return "Tool " + this.getComponentName(); } protected Project getProject() { return myProject; } public void init(Project project) { } public void dispose() { } }