/* * Copyright 2000-2013 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 com.intellij.openapi.fileTypes.impl; import com.intellij.CommonBundle; import com.intellij.ide.highlighter.custom.SyntaxTable; import com.intellij.lang.Language; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.fileTypes.*; import com.intellij.openapi.options.Configurable; import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.options.SearchableConfigurable; import com.intellij.openapi.options.SettingsEditor; import com.intellij.openapi.ui.DialogBuilder; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.wm.IdeFocusManager; import com.intellij.psi.templateLanguages.TemplateDataLanguagePatterns; import com.intellij.ui.*; import com.intellij.ui.components.JBList; import com.intellij.util.PairConvertor; import com.intellij.util.ui.JBUI; import gnu.trove.THashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import consulo.annotations.RequiredDispatchThread; import javax.swing.*; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import java.awt.*; import java.awt.event.MouseEvent; import java.util.*; import java.util.List; /** * @author Eugene Belyaev */ public class FileTypeConfigurable implements SearchableConfigurable, Configurable.NoScroll { private RecognizedFileTypes myRecognizedFileType; private PatternsPanel myPatterns; private FileTypePanel myFileTypePanel; private HashSet<FileType> myTempFileTypes; private final FileTypeManagerImpl myManager; private FileTypeAssocTable<FileType> myTempPatternsTable; private final Map<FileNameMatcher, FileType> myReassigned = new THashMap<FileNameMatcher, FileType>(); private FileTypeAssocTable<Language> myTempTemplateDataLanguages; private final Map<UserFileType, UserFileType> myOriginalToEditedMap = new HashMap<UserFileType, UserFileType>(); public FileTypeConfigurable(FileTypeManager fileTypeManager) { myManager = (FileTypeManagerImpl)fileTypeManager; } @Override public String getDisplayName() { return FileTypesBundle.message("filetype.settings.title"); } @RequiredDispatchThread @Override public JComponent createComponent() { myFileTypePanel = new FileTypePanel(); myRecognizedFileType = myFileTypePanel.myRecognizedFileType; myPatterns = myFileTypePanel.myPatterns; myRecognizedFileType.attachActions(this); myRecognizedFileType.myFileTypesList.addListSelectionListener(new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent e) { updateExtensionList(); } }); myPatterns.attachActions(this); myFileTypePanel.myIgnoreFilesField.setColumns(30); return myFileTypePanel.getComponent(); } private void updateFileTypeList() { FileType[] types = myTempFileTypes.toArray(new FileType[myTempFileTypes.size()]); Arrays.sort(types, new Comparator() { @Override public int compare(Object o1, Object o2) { FileType fileType1 = (FileType)o1; FileType fileType2 = (FileType)o2; return fileType1.getDescription().compareToIgnoreCase(fileType2.getDescription()); } }); myRecognizedFileType.setFileTypes(types); } private static FileType[] getModifiableFileTypes() { FileType[] registeredFileTypes = FileTypeManager.getInstance().getRegisteredFileTypes(); ArrayList<FileType> result = new ArrayList<FileType>(); for (FileType fileType : registeredFileTypes) { if (!fileType.isReadOnly()) result.add(fileType); } return result.toArray(new FileType[result.size()]); } @RequiredDispatchThread @Override public void apply() throws ConfigurationException { Set<UserFileType> modifiedUserTypes = myOriginalToEditedMap.keySet(); for (UserFileType oldType : modifiedUserTypes) { UserFileType newType = myOriginalToEditedMap.get(oldType); oldType.copyFrom(newType); } myOriginalToEditedMap.clear(); ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { if (!myManager.isIgnoredFilesListEqualToCurrent(myFileTypePanel.myIgnoreFilesField.getText())) { myManager.setIgnoredFilesList(myFileTypePanel.myIgnoreFilesField.getText()); } myManager.setPatternsTable(myTempFileTypes, myTempPatternsTable); for (FileNameMatcher matcher : myReassigned.keySet()) { myManager.getRemovedMappings().put(matcher, Pair.create(myReassigned.get(matcher), true)); } TemplateDataLanguagePatterns.getInstance().setAssocTable(myTempTemplateDataLanguages); } }); } @RequiredDispatchThread @Override public void reset() { myTempPatternsTable = myManager.getExtensionMap().copy(); myTempTemplateDataLanguages = TemplateDataLanguagePatterns.getInstance().getAssocTable(); myTempFileTypes = new HashSet<FileType>(Arrays.asList(getModifiableFileTypes())); myOriginalToEditedMap.clear(); updateFileTypeList(); updateExtensionList(); myFileTypePanel.myIgnoreFilesField.setText(myManager.getIgnoredFilesList()); } @RequiredDispatchThread @Override public boolean isModified() { if (!myManager.isIgnoredFilesListEqualToCurrent(myFileTypePanel.myIgnoreFilesField.getText())) return true; HashSet types = new HashSet(Arrays.asList(getModifiableFileTypes())); return !myTempPatternsTable.equals(myManager.getExtensionMap()) || !myTempFileTypes.equals(types) || !myOriginalToEditedMap.isEmpty() || !myTempTemplateDataLanguages.equals(TemplateDataLanguagePatterns.getInstance().getAssocTable()); } @RequiredDispatchThread @Override public void disposeUIResources() { if (myFileTypePanel != null) myFileTypePanel.dispose(); myFileTypePanel = null; myRecognizedFileType = null; myPatterns = null; } private static class ExtensionRenderer extends DefaultListCellRenderer { @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); setText(" " + getText()); return this; } @Override public Dimension getPreferredSize() { return JBUI.size(0, 20); } } private void updateExtensionList() { FileType type = myRecognizedFileType.getSelectedFileType(); if (type == null) return; List<String> extensions = new ArrayList<String>(); final List<FileNameMatcher> assocs = myTempPatternsTable.getAssociations(type); for (FileNameMatcher assoc : assocs) { extensions.add(assoc.getPresentableString()); } myPatterns.clearList(); Collections.sort(extensions); for (String extension : extensions) { myPatterns.addPattern(extension); } myPatterns.ensureSelectionExists(); } private void editFileType() { FileType fileType = myRecognizedFileType.getSelectedFileType(); if (!canBeModified(fileType)) return; UserFileType ftToEdit = myOriginalToEditedMap.get(fileType); if (ftToEdit == null) ftToEdit = ((UserFileType)fileType).clone(); TypeEditor editor = new TypeEditor(myRecognizedFileType.myFileTypesList, ftToEdit, FileTypesBundle.message("filetype.edit.existing.title")); editor.show(); if (editor.isOK()) { myOriginalToEditedMap.put((UserFileType)fileType, ftToEdit); } } private void removeFileType() { FileType fileType = myRecognizedFileType.getSelectedFileType(); if (fileType == null) return; myTempFileTypes.remove(fileType); myOriginalToEditedMap.remove(fileType); myTempPatternsTable.removeAllAssociations(fileType); updateFileTypeList(); updateExtensionList(); } private static boolean canBeModified(FileType fileType) { return fileType instanceof AbstractFileType; } private void addFileType() { //TODO: support adding binary file types... AbstractFileType type = new AbstractFileType(new SyntaxTable()); TypeEditor<AbstractFileType> editor = new TypeEditor<AbstractFileType>(myRecognizedFileType.myFileTypesList, type, FileTypesBundle .message("filetype.edit.new.title")); editor.show(); if (editor.isOK()) { myTempFileTypes.add(type); updateFileTypeList(); updateExtensionList(); myRecognizedFileType.selectFileType(type); } } private void editPattern() { final String item = myPatterns.getSelectedItem(); if (item == null) return; editPattern(item); } private void editPattern(@Nullable final String item) { final FileType type = myRecognizedFileType.getSelectedFileType(); if (type == null) return; final String title = item == null ? FileTypesBundle.message("filetype.edit.add.pattern.title") : FileTypesBundle.message("filetype.edit.edit.pattern.title"); final Language oldLanguage = item == null ? null : myTempTemplateDataLanguages.findAssociatedFileType(item); final FileTypePatternDialog dialog = new FileTypePatternDialog(item, type, oldLanguage); final DialogBuilder builder = new DialogBuilder(myPatterns); builder.setPreferredFocusComponent(dialog.getPatternField()); builder.setCenterPanel(dialog.getMainPanel()); builder.setTitle(title); builder.showModal(true); if (builder.getDialogWrapper().isOK()) { final String pattern = dialog.getPatternField().getText(); if (StringUtil.isEmpty(pattern)) return; final FileNameMatcher matcher = FileTypeManager.parseFromString(pattern); FileType registeredFileType = findExistingFileType(matcher); if (registeredFileType != null && registeredFileType != type) { if (registeredFileType.isReadOnly()) { Messages.showMessageDialog(myPatterns.myPatternsList, FileTypesBundle .message("filetype.edit.add.pattern.exists.error", registeredFileType.getDescription()), title, Messages.getErrorIcon()); return; } else { if (Messages.OK == Messages.showOkCancelDialog(myPatterns.myPatternsList, FileTypesBundle .message("filetype.edit.add.pattern.exists.message", registeredFileType.getDescription()), FileTypesBundle.message("filetype.edit.add.pattern.exists.title"), FileTypesBundle.message("filetype.edit.add.pattern.reassign.button"), CommonBundle.getCancelButtonText(), Messages.getQuestionIcon())) { myTempPatternsTable.removeAssociation(matcher, registeredFileType); myTempTemplateDataLanguages.removeAssociation(matcher, oldLanguage); myReassigned.put(matcher, registeredFileType); } else { return; } } } if (item != null) { final FileNameMatcher oldMatcher = FileTypeManager.parseFromString(item); myTempPatternsTable.removeAssociation(oldMatcher, type); myTempTemplateDataLanguages.removeAssociation(oldMatcher, oldLanguage); } myTempPatternsTable.addAssociation(matcher, type); myTempTemplateDataLanguages.addAssociation(matcher, dialog.getTemplateDataLanguage()); updateExtensionList(); final int index = myPatterns.getListModel().indexOf(matcher.getPresentableString()); if (index >= 0) { ListScrollingUtil.selectItem(myPatterns.myPatternsList, index); } IdeFocusManager.getGlobalInstance().doForceFocusWhenFocusSettlesDown(myPatterns.myPatternsList); } } private void addPattern() { editPattern(null); } @Nullable public FileType findExistingFileType(FileNameMatcher matcher) { FileType fileTypeByExtension = myTempPatternsTable.findAssociatedFileType(matcher); if (fileTypeByExtension != null && fileTypeByExtension != UnknownFileType.INSTANCE) { return fileTypeByExtension; } FileType registeredFileType = FileTypeManager.getInstance().getFileTypeByExtension(matcher.getPresentableString()); if (registeredFileType != UnknownFileType.INSTANCE && registeredFileType.isReadOnly()) { return registeredFileType; } return null; } @Nullable public FileType addNewPattern(FileType type, String pattern) { FileNameMatcher matcher = FileTypeManager.parseFromString(pattern); final FileType existing = findExistingFileType(matcher); if (existing != null) { return existing; } myTempPatternsTable.addAssociation(matcher, type); myPatterns.addPatternAndSelect(pattern); IdeFocusManager.getGlobalInstance().doForceFocusWhenFocusSettlesDown(myPatterns.myPatternsList); return null; } private void removePattern() { FileType type = myRecognizedFileType.getSelectedFileType(); if (type == null) return; String extension = myPatterns.removeSelected(); if (extension == null) return; FileNameMatcher matcher = FileTypeManager.parseFromString(extension); myTempPatternsTable.removeAssociation(matcher, type); IdeFocusManager.getGlobalInstance().doForceFocusWhenFocusSettlesDown(myPatterns.myPatternsList); } @Override public String getHelpTopic() { return "preferences.fileTypes"; } public static class RecognizedFileTypes extends JPanel { private final JList myFileTypesList; private final MySpeedSearch mySpeedSearch; private FileTypeConfigurable myController; public RecognizedFileTypes() { super(new BorderLayout()); myFileTypesList = new JBList(new DefaultListModel()); myFileTypesList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); myFileTypesList.setCellRenderer( new FileTypeRenderer(new FileTypeRenderer.FileTypeListProvider() { @Override public Iterable<FileType> getCurrentFileTypeList() { ArrayList<FileType> result = new ArrayList<FileType>(); for (int i = 0; i < myFileTypesList.getModel().getSize(); i++) { result.add((FileType)myFileTypesList.getModel().getElementAt(i)); } return result; } })); new DoubleClickListener() { @Override protected boolean onDoubleClick(MouseEvent e) { myController.editFileType(); return true; } }.installOn(myFileTypesList); ToolbarDecorator toolbarDecorator = ToolbarDecorator.createDecorator(myFileTypesList).setAddAction(new AnActionButtonRunnable() { @Override public void run(AnActionButton button) { myController.addFileType(); } }).setRemoveAction(new AnActionButtonRunnable() { @Override public void run(AnActionButton button) { myController.removeFileType(); } }).setEditAction(new AnActionButtonRunnable() { @Override public void run(AnActionButton button) { myController.editFileType(); } }).setEditActionUpdater(new AnActionButtonUpdater() { @Override public boolean isEnabled(AnActionEvent e) { final FileType fileType = getSelectedFileType(); return canBeModified(fileType); } }).setRemoveActionUpdater(new AnActionButtonUpdater() { @Override public boolean isEnabled(AnActionEvent e) { final FileType fileType = getSelectedFileType(); final boolean modified = canBeModified(fileType); return modified; } }).disableUpDownActions(); add(toolbarDecorator.createPanel(), BorderLayout.CENTER); setBorder(IdeBorderFactory.createTitledBorder(FileTypesBundle.message("filetypes.recognized.group"), false)); mySpeedSearch = new MySpeedSearch(myFileTypesList); } private static class MySpeedSearch extends MultipleTraitsListSpeedSearch { private FileTypeConfigurable myController; private Object myCurrentType; private String myExtension; private MySpeedSearch(JList component) { super(component, new ArrayList<PairConvertor<Object, String, Boolean>>()); initConvertors(); } @Override protected void selectElement(Object element, String selectedText) { super.selectElement(element, selectedText); if (myCurrentType != null && myCurrentType.equals(element) && myController != null) { myController.myPatterns.select(myExtension); } } private void initConvertors() { final PairConvertor<Object, String, Boolean> simpleConvertor = new PairConvertor<Object, String, Boolean>() { @Override public Boolean convert(Object element, String s) { String value = element.toString(); if (element instanceof FileType) { value = ((FileType)element).getDescription(); } return getComparator().matchingFragments(s, value) != null; } }; final PairConvertor<Object, String, Boolean> byExtensionsConvertor = new PairConvertor<Object, String, Boolean>() { @Override public Boolean convert(Object element, String s) { if (element instanceof FileType && myCurrentType != null) { return myCurrentType.equals(element); } return false; } }; myOrderedConvertors.add(simpleConvertor); myOrderedConvertors.add(byExtensionsConvertor); } @Override protected void onSearchFieldUpdated(String s) { if (myController == null || myController.myTempPatternsTable == null) return; int index = s.lastIndexOf('.'); if (index < 0) { s = "." + s; } myCurrentType = myController.myTempPatternsTable.findAssociatedFileType(s); if (myCurrentType != null) { myExtension = s; } else { myExtension = null; } } } public void attachActions(final FileTypeConfigurable controller) { myController = controller; mySpeedSearch.myController = controller; } private Collection<FileType> collectRegisteredFileTypes() { HashSet<FileType> result = new HashSet<FileType>(); for (int i = 0; i < myFileTypesList.getModel().getSize(); i++) { result.add((FileType)myFileTypesList.getModel().getElementAt(i)); } return result; } public FileType getSelectedFileType() { return (FileType)myFileTypesList.getSelectedValue(); } public JComponent getComponent() { return this; } public void setFileTypes(FileType[] types) { DefaultListModel listModel = (DefaultListModel)myFileTypesList.getModel(); listModel.clear(); for (FileType type : types) { if (type != UnknownFileType.INSTANCE) { listModel.addElement(type); } } ListScrollingUtil.ensureSelectionExists(myFileTypesList); } public int getSelectedIndex() { return myFileTypesList.getSelectedIndex(); } public void setSelectionIndex(int selectedIndex) { myFileTypesList.setSelectedIndex(selectedIndex); } public void selectFileType(FileType fileType) { myFileTypesList.setSelectedValue(fileType, true); IdeFocusManager.getGlobalInstance().doForceFocusWhenFocusSettlesDown(myFileTypesList); } } public static class PatternsPanel extends JPanel { private final JBList myPatternsList; private FileTypeConfigurable myController; public PatternsPanel() { super(new BorderLayout()); myPatternsList = new JBList(new DefaultListModel()); myPatternsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); myPatternsList.setCellRenderer(new ExtensionRenderer()); myPatternsList.getEmptyText().setText(FileTypesBundle.message("filetype.settings.no.patterns")); add(ToolbarDecorator.createDecorator(myPatternsList).setAddAction(new AnActionButtonRunnable() { @Override public void run(AnActionButton button) { myController.addPattern(); } }).setEditAction(new AnActionButtonRunnable() { @Override public void run(AnActionButton button) { myController.editPattern(); } }).setRemoveAction(new AnActionButtonRunnable() { @Override public void run(AnActionButton button) { myController.removePattern(); } }).disableUpDownActions().createPanel(), BorderLayout.CENTER); setBorder(IdeBorderFactory .createTitledBorder(FileTypesBundle.message("filetype.registered.patterns.group"), false)); } public void attachActions(final FileTypeConfigurable controller) { myController = controller; } public JComponent getComponent() { return this; } public void clearList() { getListModel().clear(); myPatternsList.clearSelection(); } private DefaultListModel getListModel() { return (DefaultListModel)myPatternsList.getModel(); } public void addPattern(String pattern) { getListModel().addElement(pattern); } public void ensureSelectionExists() { ListScrollingUtil.ensureSelectionExists(myPatternsList); } public void addPatternAndSelect(String pattern) { addPattern(pattern); ListScrollingUtil.selectItem(myPatternsList, getListModel().getSize() - 1); } public void select(final String pattern) { for (int i = 0; i < myPatternsList.getItemsCount(); i++) { final Object at = myPatternsList.getModel().getElementAt(i); if (at instanceof String) { final FileNameMatcher matcher = FileTypeManager.parseFromString((String)at); if (matcher != null && matcher.accept(pattern)) { ListScrollingUtil.selectItem(myPatternsList, i); return; } } } } public boolean isListEmpty() { return getListModel().isEmpty(); } public String removeSelected() { Object selectedValue = myPatternsList.getSelectedValue(); if (selectedValue == null) return null; ListUtil.removeSelectedItems(myPatternsList); return (String)selectedValue; } public String getDefaultExtension() { return (String)getListModel().getElementAt(0); } public String getSelectedItem() { return (String)myPatternsList.getSelectedValue(); } } private static class FileTypePanel { private JPanel myWholePanel; private RecognizedFileTypes myRecognizedFileType; private PatternsPanel myPatterns; private JTextField myIgnoreFilesField; public JComponent getComponent() { return myWholePanel; } public void dispose() { myRecognizedFileType.setFileTypes(FileType.EMPTY_ARRAY); myPatterns.clearList(); } } private static class TypeEditor<T extends UserFileType<T>> extends DialogWrapper { private final T myFileType; private final SettingsEditor<T> myEditor; public TypeEditor(Component parent, T fileType, final String title) { super(parent, false); myFileType = fileType; myEditor = fileType.getEditor(); setTitle(title); init(); Disposer.register(myDisposable, myEditor); } @Override protected void init() { super.init(); myEditor.resetFrom(myFileType); } @Override protected JComponent createCenterPanel() { return myEditor.getComponent(); } @Override protected void doOKAction() { try { myEditor.applyTo(myFileType); } catch (ConfigurationException e) { Messages.showErrorDialog(getContentPane(), e.getMessage(), e.getTitle()); return; } super.doOKAction(); } @Override protected String getHelpId() { return "reference.dialogs.newfiletype"; } } @Override @NotNull public String getId() { return getHelpTopic(); } @Override @Nullable public Runnable enableSearch(String option) { return null; } }