blob: 119d80fa70fa1a106c0b0e0930d21aad78721078 [file] [log] [blame]
/*
* 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.application.options.ExportSchemeAction;
import com.intellij.application.options.SchemesToImportPopup;
import com.intellij.ide.highlighter.custom.SyntaxTable;
import com.intellij.ide.highlighter.custom.impl.ReadFileType;
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.fileTypes.ex.FileTypeManagerEx;
import com.intellij.openapi.options.*;
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.psi.templateLanguages.TemplateDataLanguagePatterns;
import com.intellij.ui.*;
import com.intellij.ui.components.JBList;
import com.intellij.util.PairConvertor;
import com.intellij.util.PlatformIcons;
import gnu.trove.THashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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 extends BaseConfigurable 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");
}
@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()]);
}
@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);
}
});
}
@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());
}
@Override
public boolean isModified() {
if (!myManager.isIgnoredFilesListEqualToCurrent(myFileTypePanel.myIgnoreFilesField.getText())) return true;
HashSet<FileType> types = new HashSet<FileType>(Arrays.asList(getModifiableFileTypes()));
return !myTempPatternsTable.equals(myManager.getExtensionMap()) || !myTempFileTypes.equals(types) ||
!myOriginalToEditedMap.isEmpty() ||
!myTempTemplateDataLanguages.equals(TemplateDataLanguagePatterns.getInstance().getAssocTable());
}
@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 new Dimension(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 && !(fileType instanceof ImportedFileType); //todo: add API for canBeModified
}
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);
}
myPatterns.myPatternsList.requestFocus();
}
}
private void addPattern() {
editPattern(null);
}
@Nullable
public FileType findExistingFileType(FileNameMatcher matcher) {
FileType fileTypeByExtension = myTempPatternsTable.findAssociatedFileType(matcher);
if (fileTypeByExtension != null && fileTypeByExtension != FileTypes.UNKNOWN) {
return fileTypeByExtension;
}
FileType registeredFileType = FileTypeManager.getInstance().getFileTypeByExtension(matcher.getPresentableString());
if (registeredFileType != FileTypes.UNKNOWN && 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);
myPatterns.myPatternsList.requestFocus();
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);
myPatterns.myPatternsList.requestFocus();
}
@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);
final boolean shared = getSchemesManager().isShared(fileType);
return shared || modified;
}
})
.disableUpDownActions();
if (getSchemesManager().isImportAvailable()) {
toolbarDecorator.addExtraAction(new AnActionButton("Import Shared...", PlatformIcons.IMPORT_ICON) {
@Override
public void actionPerformed(AnActionEvent e) {
new SchemesToImportPopup<FileType, AbstractFileType>(myFileTypesList) {
@Override
protected void onSchemeSelected(final AbstractFileType scheme) {
myController.importFileType(scheme);
}
}.show(getSchemesManager(), collectRegisteredFileTypes());
}
});
}
if (getSchemesManager().isExportAvailable()) {
toolbarDecorator.addExtraAction(new AnActionButton("Share...", PlatformIcons.EXPORT_ICON) {
@Override
public void actionPerformed(AnActionEvent e) {
FileType selected = (FileType)myFileTypesList.getSelectedValue();
if (selected instanceof AbstractFileType) {
ExportSchemeAction.doExport((AbstractFileType)selected, getSchemesManager());
}
}
@Override
public void updateButton(AnActionEvent e) {
FileType fileType = getSelectedFileType();
boolean b = canBeModified(fileType);
boolean shared = getSchemesManager().isShared(fileType);
setEnabled(b && !shared);
}
});
}
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;
}
}
}
private SchemesManager<FileType, AbstractFileType> getSchemesManager() {
return ((FileTypeManagerEx)FileTypeManager.getInstance()).getSchemesManager();
}
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 != FileTypes.UNKNOWN) {
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);
myFileTypesList.requestFocus();
}
}
private void importFileType(final FileType type) {
ReadFileType readFileType = (ReadFileType)type;
ImportedFileType actualType = new ImportedFileType(readFileType.getSyntaxTable(), readFileType.getExternalInfo());
actualType.setDescription(readFileType.getDescription());
actualType.setName(readFileType.getName());
actualType.readOriginalMatchers(readFileType.getElement());
for (FileNameMatcher matcher : actualType.getOriginalPatterns()) {
myTempPatternsTable.addAssociation(matcher, actualType);
}
myTempFileTypes.add(actualType);
updateFileTypeList();
updateExtensionList();
myRecognizedFileType.selectFileType(type);
}
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 (FileNameMatcherEx.acceptsCharSequence(matcher, 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;
}
}