package com.intellij.lang.javascript.flex.projectStructure; import com.intellij.flex.model.bc.BuildConfigurationNature; import com.intellij.flex.model.bc.OutputType; import com.intellij.lang.javascript.flex.FlexBundle; import com.intellij.lang.javascript.flex.FlexModuleType; import com.intellij.lang.javascript.flex.projectStructure.model.ModifiableFlexBuildConfiguration; import com.intellij.lang.javascript.flex.projectStructure.model.SdkEntry; import com.intellij.lang.javascript.flex.projectStructure.model.impl.Factory; import com.intellij.lang.javascript.flex.projectStructure.model.impl.FlexProjectConfigurationEditor; import com.intellij.lang.javascript.flex.projectStructure.ui.AddBuildConfigurationDialog; import com.intellij.lang.javascript.flex.projectStructure.ui.CompositeConfigurable; import com.intellij.lang.javascript.flex.projectStructure.ui.FlexBCConfigurable; import com.intellij.lang.javascript.flex.sdk.FlexSdkUtils; import com.intellij.openapi.Disposable; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleType; import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.roots.ModifiableRootModel; import com.intellij.openapi.roots.libraries.Library; import com.intellij.openapi.roots.libraries.LibraryTable; import com.intellij.openapi.roots.ui.configuration.ModulesConfigurator; import com.intellij.openapi.roots.ui.configuration.ProjectStructureConfigurable; import com.intellij.openapi.roots.ui.configuration.projectRoot.LibrariesModifiableModel; import com.intellij.openapi.roots.ui.configuration.projectRoot.ModuleStructureConfigurable; import com.intellij.openapi.roots.ui.configuration.projectRoot.daemon.ProjectStructureDaemonAnalyzer; import com.intellij.openapi.ui.MasterDetailsComponent; import com.intellij.openapi.util.Disposer; import com.intellij.ui.navigation.Place; import com.intellij.util.EventDispatcher; import com.intellij.util.PathUtil; import com.intellij.util.containers.BidirectionalMap; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.HashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; public class FlexBCConfigurator { private static final Logger LOG = Logger.getInstance(FlexBCConfigurator.class.getName()); public interface Listener extends EventListener { void moduleRemoved(Module module); void buildConfigurationRemoved(FlexBCConfigurable configurable); void natureChanged(FlexBCConfigurable configurable); void buildConfigurationRenamed(FlexBCConfigurable configurable); } private final BidirectionalMap<ModifiableFlexBuildConfiguration, CompositeConfigurable> myConfigurablesMap = new BidirectionalMap<>(); private final BidirectionalMap<ModifiableFlexBuildConfiguration, String> myBCToOutputPathMap = new BidirectionalMap<>(); private final EventDispatcher<Listener> myEventDispatcher = EventDispatcher.create(Listener.class); // we can have only one Project Structure configuration dialog at a time, so it's OK to hold state for one project private final LazyInitializer<Project> myModifiableModelInitializer = new LazyInitializer<Project>() { @Override protected void initialize(final Project project) { LOG.assertTrue(myConfigEditor == null); final ModulesConfigurator configurator = ModuleStructureConfigurable.getInstance(project).getContext().getModulesConfigurator(); myConfigEditor = new FlexProjectConfigurationEditor(project, new FlexProjectConfigurationEditor.ProjectModifiableModelProvider() { @Override public Module[] getModules() { return configurator.getModuleModel().getModules(); } @Override public ModifiableRootModel getModuleModifiableModel(Module module) { return configurator.getOrCreateModuleEditor(module).getModifiableRootModelProxy(); } @Override public void addListener(Listener listener, Disposable parentDisposable) { FlexBCConfigurator.this.addListener(listener, parentDisposable); } @Override public void commitModifiableModels() throws ConfigurationException { configurator.apply(); } public LibraryTable.ModifiableModel getLibrariesModifiableModel(final String level) { return ProjectStructureConfigurable.getInstance(project).getContext().createModifiableModelProvider(level).getModifiableModel(); } public Library findSourceLibraryForLiveName(final String name, final String level) { final LibrariesModifiableModel model = ProjectStructureConfigurable.getInstance(project).getContext().createModifiableModelProvider(level).getModifiableModel(); return ContainerUtil.find(model.getLibraries(), library -> name.equals(model.getLibraryEditor(library).getModel().getName())); } public Library findSourceLibrary(final String name, final String level) { return getLibrariesModifiableModel(level).getLibraryByName(name); } }); } @Override public void doDispose() { Disposer.dispose(myConfigEditor); myConfigEditor = null; } }; private FlexProjectConfigurationEditor myConfigEditor; /** * @return current editor if Project Structure dialog is open */ @Nullable public FlexProjectConfigurationEditor getConfigEditor() { return myConfigEditor; } public void addListener(Listener listener, Disposable parentDisposable) { myEventDispatcher.addListener(listener, parentDisposable); } public void reset(Project project) { myModifiableModelInitializer.ensureInitialized(project); ModuleStructureConfigurable moduleStructureConfigurable = ModuleStructureConfigurable.getInstance(project); for (final CompositeConfigurable configurable : myConfigurablesMap.values()) { moduleStructureConfigurable.ensureInitialized(configurable); } } public List<CompositeConfigurable> getOrCreateConfigurables(final Module module, final Runnable treeNodeNameUpdater) { myModifiableModelInitializer.ensureInitialized(module.getProject()); final ModifiableFlexBuildConfiguration[] configurations = myConfigEditor.getConfigurations(module); List<CompositeConfigurable> configurables = new ArrayList<>(configurations.length); for (final ModifiableFlexBuildConfiguration bc : configurations) { CompositeConfigurable configurable = myConfigurablesMap.get(bc); if (configurable == null) { myConfigurablesMap.put(bc, configurable = createBcConfigurable(module, bc, treeNodeNameUpdater)); } configurables.add(configurable); } return configurables; } private CompositeConfigurable createBcConfigurable(final Module module, final ModifiableFlexBuildConfiguration bc, final Runnable treeNodeNameUpdater) { final ProjectStructureConfigurable c = ProjectStructureConfigurable.getInstance(module.getProject()); final Runnable bcNatureModifier = createBCNatureModifier(bc); return new FlexBCConfigurable(module, bc, bcNatureModifier, myConfigEditor, c.getProjectJdksModel(), c.getContext()) { public void apply() throws ConfigurationException { super.apply(); myBCToOutputPathMap.put(bc, bc.getActualOutputFilePath()); } @Override public void setDisplayName(final String name) { super.setDisplayName(name); treeNodeNameUpdater.run(); myEventDispatcher.getMulticaster().buildConfigurationRenamed(this); } }.wrapInTabs(); } private Runnable createBCNatureModifier(final ModifiableFlexBuildConfiguration bc) { return () -> { final CompositeConfigurable compositeConfigurable = myConfigurablesMap.get(bc); final BuildConfigurationNature oldNature = bc.getNature(); final AddBuildConfigurationDialog dialog = new AddBuildConfigurationDialog(myConfigEditor.getProject(), FlexBundle.message("change.bc.type.title"), Collections.emptyList(), oldNature, false); dialog.reset(bc.getName(), bc.getAndroidPackagingOptions().isEnabled(), bc.getIosPackagingOptions().isEnabled()); if (!dialog.showAndGet()) { return; } final BuildConfigurationNature newNature = dialog.getNature(); if (newNature.equals(oldNature)) { if (newNature.isApp() && newNature.isMobilePlatform()) { bc.getAndroidPackagingOptions().setEnabled(dialog.isAndroidEnabled()); bc.getIosPackagingOptions().setEnabled(dialog.isIOSEnabled()); compositeConfigurable.reset(); } return; } bc.setNature(newNature); fixOutputFileExtension(bc); if (newNature.targetPlatform != oldNature.targetPlatform || newNature.outputType != oldNature.outputType) { // set package names only if corresponding tabs were not applicable before updatePackageFileName(bc, PathUtil.suggestFileName(bc.getName())); } if (newNature.isApp() && newNature.isMobilePlatform()) { bc.getAndroidPackagingOptions().setEnabled(dialog.isAndroidEnabled()); bc.getIosPackagingOptions().setEnabled(dialog.isIOSEnabled()); } FlexProjectConfigurationEditor.resetNonApplicableValuesToDefaults(bc); final FlexBCConfigurable bcConfigurable = FlexBCConfigurable.unwrap(compositeConfigurable); bcConfigurable.createChildConfigurables(); bcConfigurable.updateTabs(compositeConfigurable); compositeConfigurable.reset(); myEventDispatcher.getMulticaster().natureChanged(bcConfigurable); }; } private static void fixOutputFileExtension(final ModifiableFlexBuildConfiguration bc) { final String outputFileName = bc.getOutputFileName(); final String lowercase = outputFileName.toLowerCase(); final String extension = bc.getOutputType() == OutputType.Library ? ".swc" : ".swf"; if (lowercase.endsWith(".swf") || lowercase.endsWith(".swc")) { bc.setOutputFileName(outputFileName.substring(0, outputFileName.length() - ".sw_".length()) + extension); } else { bc.setOutputFileName(outputFileName + extension); } } public void moduleRemoved(final Module module) { if (ModuleType.get(module) != FlexModuleType.getInstance()) { return; } // config editor will handle event and update modifiable model on its own, we just need to update configurables Collection<ModifiableFlexBuildConfiguration> configsToRemove = ContainerUtil.findAll(myConfigurablesMap.keySet(), bc -> myConfigEditor.getModule(bc) == module); final ProjectStructureDaemonAnalyzer daemonAnalyzer = ProjectStructureConfigurable.getInstance(myConfigEditor.getProject()).getContext().getDaemonAnalyzer(); for (ModifiableFlexBuildConfiguration bc : configsToRemove) { CompositeConfigurable configurable = myConfigurablesMap.remove(bc); myBCToOutputPathMap.remove(bc); daemonAnalyzer.removeElement(configurable.getProjectStructureElement()); daemonAnalyzer.queueUpdateForAllElementsWithErrors(); configurable.disposeUIResources(); } myEventDispatcher.getMulticaster().moduleRemoved(module); } public boolean isModified() { if (myConfigEditor.isModified()) return true; for (final CompositeConfigurable configurable : myConfigurablesMap.values()) { if (configurable.isModified()) { return true; } } return false; } public void apply() throws ConfigurationException { final ModuleStructureConfigurable c = ProjectStructureConfigurable.getInstance(myConfigEditor.getProject()).getModulesConfig(); for (final CompositeConfigurable configurable : myConfigurablesMap.values()) { c.ensureInitialized(configurable); if (configurable.isModified()) { configurable.apply(); } } if (myConfigEditor.isModified()) { myConfigEditor.checkCanCommit(); myConfigEditor.commit(); } } public void afterModelCommit() { for (final CompositeConfigurable configurable : myConfigurablesMap.values()) { configurable.reset(); } } public void dispose() { // configurables are disposed by MasterDetailsComponent myModifiableModelInitializer.dispose(); myConfigurablesMap.clear(); myBCToOutputPathMap.clear(); } public void removeConfiguration(final ModifiableFlexBuildConfiguration bc) { CompositeConfigurable configurable = myConfigurablesMap.remove(bc); myBCToOutputPathMap.remove(bc); final ProjectStructureDaemonAnalyzer daemonAnalyzer = ProjectStructureConfigurable.getInstance(myConfigEditor.getProject()).getContext().getDaemonAnalyzer(); daemonAnalyzer.removeElement(configurable.getProjectStructureElement()); daemonAnalyzer.queueUpdateForAllElementsWithErrors(); myEventDispatcher.getMulticaster().buildConfigurationRemoved(FlexBCConfigurable.unwrap(configurable)); } public void addConfiguration(final Module module, final Runnable treeNodeNameUpdater) { if (module == null) { return; } final String title = FlexBundle.message("add.build.configuration.title", module.getName()); final AddBuildConfigurationDialog dialog = new AddBuildConfigurationDialog(module.getProject(), title, getUsedNames(module), BuildConfigurationNature.DEFAULT, true); if (!dialog.showAndGet()) { return; } final ModifiableFlexBuildConfiguration bc = myConfigEditor.createConfiguration(module); final String bcName = dialog.getBCName(); final String fileName = PathUtil.suggestFileName(bcName); final BuildConfigurationNature nature = dialog.getNature(); bc.setName(bcName); bc.setNature(nature); final ModifiableFlexBuildConfiguration someExistingConfig = myConfigEditor.getConfigurations(module)[0]; bc.setOutputFileName(fileName + (bc.getOutputType() == OutputType.Library ? ".swc" : ".swf")); bc.setOutputFolder(someExistingConfig.getOutputFolder()); updatePackageFileName(bc, fileName); if (nature.isApp() && nature.isMobilePlatform()) { bc.getAndroidPackagingOptions().setEnabled(dialog.isAndroidEnabled()); bc.getIosPackagingOptions().setEnabled(dialog.isIOSEnabled()); } final SdkEntry sdkEntry = someExistingConfig.getDependencies().getSdkEntry(); final SdkEntry newSdkEntry; if (sdkEntry != null && FlexSdkUtils.findFlexOrFlexmojosSdk(sdkEntry.getName()) != null) { newSdkEntry = Factory.createSdkEntry(sdkEntry.getName()); } else { newSdkEntry = findAnySdk(); } bc.getDependencies().setSdkEntry(newSdkEntry); createConfigurableNode(bc, module, treeNodeNameUpdater); } @Nullable private static SdkEntry findAnySdk() { final List<Sdk> sdks = FlexSdkUtils.getFlexSdks(); return sdks.isEmpty() ? null : Factory.createSdkEntry(sdks.get(0).getName()); } public void copy(final CompositeConfigurable configurable, final Runnable treeNodeNameUpdater) { try { configurable.apply(); } catch (ConfigurationException ignored) {/**/} ModifiableFlexBuildConfiguration existingBC = myConfigurablesMap.getKeysByValue(configurable).get(0); FlexBCConfigurable unwrapped = FlexBCConfigurable.unwrap(configurable); final String title = FlexBundle.message("copy.build.configuration", existingBC.getName(), unwrapped.getModule().getName()); Module module = unwrapped.getModule(); AddBuildConfigurationDialog dialog = new AddBuildConfigurationDialog(module.getProject(), title, getUsedNames(module), existingBC.getNature(), true); dialog.reset("", existingBC.getAndroidPackagingOptions().isEnabled(), existingBC.getIosPackagingOptions().isEnabled()); if (!dialog.showAndGet()) { return; } final String newBCName = dialog.getBCName(); final String fileName = PathUtil.suggestFileName(newBCName); final BuildConfigurationNature newNature = dialog.getNature(); ModifiableFlexBuildConfiguration newBC = myConfigEditor.copyConfiguration(existingBC, newNature); newBC.setName(newBCName); newBC.setOutputFileName(fileName + (newBC.getOutputType() == OutputType.Library ? ".swc" : ".swf")); updatePackageFileName(newBC, fileName); if (newNature.isApp() && newNature.isMobilePlatform()) { newBC.getAndroidPackagingOptions().setEnabled(dialog.isAndroidEnabled()); newBC.getIosPackagingOptions().setEnabled(dialog.isIOSEnabled()); } createConfigurableNode(newBC, unwrapped.getModule(), treeNodeNameUpdater); } private static void updatePackageFileName(final ModifiableFlexBuildConfiguration bc, final String packageFileName) { final BuildConfigurationNature nature = bc.getNature(); if (nature.isApp()) { if (nature.isDesktopPlatform()) { bc.getAirDesktopPackagingOptions().setPackageFileName(packageFileName); } else if (nature.isMobilePlatform()) { bc.getAndroidPackagingOptions().setPackageFileName(packageFileName); bc.getIosPackagingOptions().setPackageFileName(packageFileName); } } } private void createConfigurableNode(ModifiableFlexBuildConfiguration bc, Module module, Runnable treeNodeNameUpdater) { CompositeConfigurable wrapped = createBcConfigurable(module, bc, treeNodeNameUpdater); myConfigurablesMap.put(bc, wrapped); final MasterDetailsComponent.MyNode node = new BuildConfigurationNode(wrapped); final ModuleStructureConfigurable moduleStructureConfigurable = ModuleStructureConfigurable.getInstance(module.getProject()); moduleStructureConfigurable.addNode(node, moduleStructureConfigurable.findModuleNode(module)); Place place = new Place().putPath(ProjectStructureConfigurable.CATEGORY, moduleStructureConfigurable) .putPath(MasterDetailsComponent.TREE_OBJECT, bc); ProjectStructureConfigurable.getInstance(module.getProject()).navigateTo(place, true); } private Collection<String> getUsedNames(final Module module) { final Collection<String> result = new LinkedList<>(); for (final ModifiableFlexBuildConfiguration configuration : myConfigEditor.getConfigurations(module)) { result.add(myConfigurablesMap.get(configuration).getDisplayName()); } return result; } public List<CompositeConfigurable> getBCConfigurables(@NotNull Module module) { return ContainerUtil.map(myConfigEditor.getConfigurations(module), configuration -> myConfigurablesMap.get(configuration)); } public FlexBCConfigurable getBCConfigurable(@NotNull ModifiableFlexBuildConfiguration bc) { return FlexBCConfigurable.unwrap(myConfigurablesMap.get(bc)); } @Nullable public List<ModifiableFlexBuildConfiguration> getBCsByOutputPath(final String outputPath) { return myBCToOutputPathMap.getKeysByValue(outputPath); } public Place getPlaceFor(final Module module, final String bcName) { Place p = new Place(); p = p.putPath(ProjectStructureConfigurable.CATEGORY, ModuleStructureConfigurable.getInstance(myConfigEditor.getProject())); p = p.putPath(MasterDetailsComponent.TREE_OBJECT, myConfigEditor.findCurrentConfiguration(module, bcName)); return p; } public boolean canBeRemoved(ModifiableFlexBuildConfiguration[] configurations) { Map<Module, Integer> module2ConfigCount = new HashMap<>(); for (ModifiableFlexBuildConfiguration bc : configurations) { Module module = myConfigEditor.getModule(bc); Integer count = module2ConfigCount.get(module); module2ConfigCount.put(module, count != null ? count + 1 : 1); } for (Map.Entry<Module, Integer> entry : module2ConfigCount.entrySet()) { Module module = entry.getKey(); if (myConfigEditor.getConfigurations(module).length == entry.getValue()) { return false; } } return true; } }