package net.bitpot.railways.routesView; import com.intellij.ide.PowerSaveMode; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.components.*; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.module.ModuleType; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.wm.ToolWindow; import com.intellij.openapi.wm.ToolWindowAnchor; import com.intellij.openapi.wm.ToolWindowContentUiType; import com.intellij.openapi.wm.ex.ToolWindowManagerAdapter; import com.intellij.openapi.wm.ex.ToolWindowManagerEx; import com.intellij.openapi.wm.impl.content.ToolWindowContentUi; import com.intellij.psi.util.PsiModificationTracker; import com.intellij.ui.content.Content; import com.intellij.ui.content.ContentManager; import com.intellij.ui.content.ContentManagerAdapter; import com.intellij.ui.content.ContentManagerEvent; import com.intellij.util.Alarm; import com.intellij.util.messages.MessageBusConnection; import com.intellij.util.ui.UIUtil; import net.bitpot.railways.gui.MainPanel; import net.bitpot.railways.models.RouteList; import net.bitpot.railways.navigation.ChooseByRouteRegistry; import net.bitpot.railways.utils.RailwaysUtils; import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.ruby.rails.model.RailsApp; import javax.swing.*; import java.util.ArrayList; @State( name="RoutesToolWindow", storages= { @Storage(file = StoragePathMacros.WORKSPACE_FILE) } ) /** * Implements tool window logic. Synchronizes the number of tool window panes * with the number of opened Rails modules in the project. */ public class RoutesView implements PersistentStateComponent<RoutesView.State>, Disposable { public static RoutesView getInstance(Project project) { return ServiceManager.getService(project, RoutesView.class); } private Project myProject; private ContentManager myContentManager; private MainPanel mainPanel; private ArrayList<RoutesViewPane> myPanes = new ArrayList<RoutesViewPane>(); private RoutesViewPane currentPane = null; private ToolWindow myToolWindow; private State myState = new State(); public RoutesView(Project project) { myProject = project; mainPanel = new MainPanel(project); // Subscribe on files changes to update Route list regularly. // We connect to project bus, as module bus don't work with this topic MessageBusConnection conn = project.getMessageBus().connect(); conn.subscribe(PsiModificationTracker.TOPIC, new PSIModificationListener()); } public static class State { public int selectedTabId; public boolean hideMountedRoutes; } @Nullable @Override public RoutesView.State getState() { return myState; } @Override public void loadState(RoutesView.State state) { myState = state; } /** * Initializes tool window. * * @param toolWindow Tool window to initialize. */ public synchronized void initToolWindow(final ToolWindow toolWindow) { myToolWindow = toolWindow; myContentManager = toolWindow.getContentManager(); if (!ApplicationManager.getApplication().isUnitTestMode()) { toolWindow.setContentUiType(ToolWindowContentUiType.getInstance("combo"), null); toolWindow.getComponent().putClientProperty(ToolWindowContentUi.HIDE_ID_LABEL, "true"); } // Add all modules that are already added till this moment. Module[] modules = ModuleManager.getInstance(myProject).getModules(); for (Module m : modules) addModulePane(m); // Add listener to update mainPanel when a module is selected from // tool window header. myContentManager.addContentManagerListener(new ContentManagerAdapter() { @Override public void selectionChanged(ContentManagerEvent event) { // When user selects a module from tool window combo, // selectionChanges is called twice: // 1. With 'remove' operation - for previously selected item, // 2. With 'add' operation - for newly selected item. if (event.getOperation() == ContentManagerEvent.ContentOperation.add) { viewSelectionChanged(); refreshRouteActionsStatus(); } } }); // Open tab that was active in previous IDE session Content savedContent = myContentManager.getContent(myState.selectedTabId); if (savedContent != null) myContentManager.setSelectedContent(savedContent); mainPanel.getRouteFilter().setMountedRoutesVisible(!myState.hideMountedRoutes); ToolWindowManagerEx toolManager = ToolWindowManagerEx.getInstanceEx(myProject); toolManager.addToolWindowManagerListener(new ToolWindowManagerAdapter() { /** * This method is called when ToolWindow changes its state, i.e. * expanded/collapsed, docked to another panel, etc. */ @Override public void stateChanged() { // We have to check if our tool window is still registered, as // otherwise it will raise an exception when project is closed. if (ToolWindowManagerEx.getInstanceEx(myProject).getToolWindow("Routes") == null) return; updateToolWindowOrientation(toolWindow); if (toolWindow.isVisible()) if (currentPane != null && currentPane.isRoutesInvalidated()) currentPane.updateRoutes(); refreshRouteActionsStatus(); } }); updateToolWindowOrientation(toolWindow); } public boolean isMountedRoutesVisible() { return mainPanel.getRouteFilter().isMountedRoutesVisible(); } public void setMountedRoutesVisible(boolean value) { mainPanel.getRouteFilter().setMountedRoutesVisible(value); myState.hideMountedRoutes = !value; } private void updateToolWindowOrientation(ToolWindow toolWindow) { if (toolWindow.isDisposed()) return; ToolWindowAnchor anchor = toolWindow.getAnchor(); boolean isVertical = (anchor == ToolWindowAnchor.LEFT || anchor == ToolWindowAnchor.RIGHT); mainPanel.setOrientation(isVertical); } private void viewSelectionChanged() { Content content = myContentManager.getSelectedContent(); if (content == null) return; // Find selected pane by content. RoutesViewPane pane = null; int index = 0; for(RoutesViewPane p: myPanes) { if (p.getContent() == content) { pane = p; myState.selectedTabId = index; break; } index++; } setCurrentPane(pane); } @Override public void dispose() { // Do nothing now } private JComponent getComponent() { return mainPanel.getRootPanel(); } @Nullable public RoutesManager getCurrentRoutesManager() { return (currentPane == null) ? null : currentPane.getRoutesManager(); } public void setCurrentPane(RoutesViewPane pane) { if (currentPane == pane) return; currentPane = pane; if (pane != null) { mainPanel.setDataSource(pane); if (pane.isRoutesInvalidated()) pane.updateRoutes(); syncPanelWithRoutesManager(pane.getRoutesManager()); } } public void addModulePane(Module module) { // Skip if RoutesView is not initialized or if added module is not // Rails application. RailsApp railsApp = RailsApp.fromModule(module); if ((myContentManager == null) || railsApp == null) return; // Register content, so we'll have a combo-box instead tool window // title, and each item will represent a module. String contentTitle = module.getName(); Content content = myContentManager.getFactory().createContent(getComponent(), contentTitle, false); content.setTabName(contentTitle); content.setIcon(ModuleType.get(module).getIcon()); // Set tool window icon to be the same as selected module icon content.putUserData(ToolWindow.SHOW_CONTENT_ICON, Boolean.TRUE); myContentManager.addContent(content); // Bind content with pane for further use RoutesViewPane pane = new RoutesViewPane(railsApp, myToolWindow, content); myPanes.add(pane); // Register contributor ChooseByRouteRegistry.getInstance(myProject) .addContributorFor(pane.getRoutesManager()); // Subscribe to RoutesManager events. pane.getRoutesManager().addListener(new MyRoutesManagerListener()); // And select pane if it's the first one. if (myPanes.size() == 1) setCurrentPane(pane); } public void removeModulePane(Module module) { // Find corresponding content by module... for (RoutesViewPane pane : myPanes) if (pane.getModule() == module) { // ... and remove it from panels list. myContentManager.removeContent(pane.getContent(), true); myPanes.remove(pane); // Remove contributor ChooseByRouteRegistry.getInstance(myProject) .removeContributor(pane.getRoutesManager()); Disposer.dispose(pane); break; } } /** * Updates appearance of MainPanel according to the state of RoutesManager. * * @param routesManager Routes manager which state will be used for * appearance sync. */ private void syncPanelWithRoutesManager(RoutesManager routesManager) { switch(routesManager.getRoutesState()) { case RoutesManager.UPDATING: mainPanel.showLoadingMessage(); break; case RoutesManager.UPDATED: mainPanel.setUpdatedRoutes(routesManager.getRouteList()); break; case RoutesManager.ERROR: mainPanel.showRoutesUpdateError(routesManager.getParserErrorCode()); break; } } private boolean isLiveHighlightingEnabled() { return currentPane.getRoutesManager().getState().liveActionHighlighting; } private void refreshRouteActionsStatus() { RoutesManager rm = currentPane.getRoutesManager(); RouteList routes = rm.getRouteList(); if (rm.isUpdating() || routes.size() == 0 || !isLiveHighlightingEnabled()) return; RailwaysUtils.updateActionsStatus(currentPane.getModule(), routes); mainPanel.refresh(); } private class PSIModificationListener implements PsiModificationTracker.Listener { final Alarm alarm = new Alarm(); @Override public void modificationCountChanged() { if (PowerSaveMode.isEnabled() || myToolWindow == null || !myToolWindow.isVisible() || !isLiveHighlightingEnabled()) return; alarm.cancelAllRequests(); alarm.addRequest(new Runnable() { @Override public void run() { refreshRouteActionsStatus(); } }, 1000, ModalityState.NON_MODAL); } } private class MyRoutesManagerListener implements RoutesManagerListener { @Override public void stateChanged(final RoutesManager routesManager) { // Railways can invoke this event from another thread UIUtil.invokeLaterIfNeeded(new Runnable() { public void run() { // Synchronize with routesManager only if it belongs to // currently selected pane. if (routesManager == getCurrentRoutesManager()) syncPanelWithRoutesManager(routesManager); } }); } } }