/* * 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.ui.dialogs.properties; import com.intellij.icons.AllIcons; import com.intellij.icons.AllIcons.Actions; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.CommonShortcuts; import com.intellij.openapi.actionSystem.ShortcutSet; import com.intellij.openapi.options.Configurable; import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.ui.ComboBoxTableRenderer; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.util.Disposer; import com.intellij.ui.AnActionButton; import com.intellij.ui.AnActionButtonRunnable; import com.intellij.ui.BooleanTableCellRenderer; import com.intellij.ui.IdeBorderFactory; import com.intellij.ui.SpeedSearchBase; import com.intellij.ui.SpeedSearchComparator; import com.intellij.ui.TabbedPaneWrapper; import com.intellij.ui.TableUtil; import com.intellij.ui.ToolbarDecorator; import com.intellij.ui.components.JBLabel; import com.intellij.ui.table.JBTable; import com.intellij.uiDesigner.core.GridConstraints; import com.intellij.uiDesigner.core.GridLayoutManager; import com.intellij.util.ui.JBInsets; import jetbrains.mps.icons.MPSIcons.General; import jetbrains.mps.ide.ThreadUtils; import jetbrains.mps.ide.findusages.model.IResultProvider; import jetbrains.mps.ide.findusages.model.SearchQuery; import jetbrains.mps.ide.findusages.view.UsageToolOptions; import jetbrains.mps.ide.findusages.view.UsagesViewTool; import jetbrains.mps.ide.icons.IdeIcons; import jetbrains.mps.ide.project.ProjectHelper; import jetbrains.mps.ide.save.SaveRepositoryCommand; import jetbrains.mps.ide.ui.dialogs.properties.renders.DependencyCellState; import jetbrains.mps.ide.ui.dialogs.properties.renders.LanguageTableCellRenderer; import jetbrains.mps.ide.ui.dialogs.properties.tables.items.DependenciesTableItem; import jetbrains.mps.ide.ui.dialogs.properties.tables.models.DependTableModel; import jetbrains.mps.ide.ui.dialogs.properties.tables.models.UsedLangsTableModel; import jetbrains.mps.ide.ui.dialogs.properties.tables.models.UsedLangsTableModel.Import; import jetbrains.mps.ide.ui.dialogs.properties.tables.models.UsedLangsTableModel.ValidImportCondition; import jetbrains.mps.ide.ui.dialogs.properties.tabs.BaseTab; import jetbrains.mps.project.DevKit; import jetbrains.mps.project.Project; import jetbrains.mps.util.IterableUtil; import jetbrains.mps.util.NotCondition; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.language.SLanguage; import org.jetbrains.mps.openapi.model.SModelReference; import org.jetbrains.mps.openapi.module.SDependencyScope; import org.jetbrains.mps.openapi.module.SModule; import org.jetbrains.mps.openapi.module.SModuleReference; import org.jetbrains.mps.openapi.module.SRepository; import org.jetbrains.mps.openapi.ui.Modifiable; import org.jetbrains.mps.openapi.ui.persistence.Tab; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.ListSelectionModel; import javax.swing.event.ChangeListener; import javax.swing.table.AbstractTableModel; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableModel; import java.awt.Dimension; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; import java.util.List; /** * Configuration page consisting of {@link Tab tabs}. */ public abstract class MPSPropertiesConfigurable implements Configurable, Disposable { private TabbedPaneWrapper myTabbedPaneWrapper = new TabbedPaneWrapper(this); private List<Tab> myTabs = new ArrayList<>(); protected final Project myProject; private DialogWrapper myParentForCallBack = null; public MPSPropertiesConfigurable(Project project) { myProject = project; } public final void setParentForCallBack(DialogWrapper parentForCallBack) { myParentForCallBack = parentForCallBack; } protected final void forceCancelCloseDialog() { if (myParentForCallBack == null) { return; } ThreadUtils.runInUIThreadNoWait(new Runnable() { @Override public void run() { myParentForCallBack.doCancelAction(); } }); } @Override public String getHelpTopic() { return null; } @Override public void dispose() { } /** * Subclasses may choose whether to instantiate initial tabs on demand with {@link #createInitialTabs()} * or register them at once with this method. Supplied tabs would get a chance to build UI * at a proper moment some time later (with {@link Modifiable#init()} call. * <p> * The method may be invoked few times before the UI is created, registered tabs sum up. * * @param tabs initial set of tabs */ protected void registerTabs(Tab... tabs) { myTabs.addAll(Arrays.asList(tabs)); } /** * Provides initial set of tabs to appear in the UI. Subclasses may override or could use {@link #registerTabs(Tab...)} instead. * Note, tabs supplied would get {@link Modifiable#init() initialized} from EDT thread some moment later, when deemed appropriate * (i.e. there's no guarantee all tabs get initialized, chances are implementation may opt to initialize visible tabs only) */ protected Collection<Tab> createInitialTabs() { return myTabs; } @Override public final JComponent createComponent() { for (Tab tab : createInitialTabs()) { tab.init(); addTab(tab); } return myTabbedPaneWrapper.getComponent(); } public int getTabsCount() { return myTabbedPaneWrapper.getTabCount(); } public final Tab getTab(int index) { return myTabs.get(index); } public void selectTab(Tab tab) { selectTab(indexOfTab(tab)); } public void selectTab(int index) { myTabbedPaneWrapper.setSelectedIndex(index); } public void addTab(Tab tab) { if (tab == null || tab.getTabComponent() == null) { return; } if (tab.getToolTip() == null && tab instanceof BaseTab) { ((BaseTab) tab).setToolTip(tab.getTitle()); } if (!myTabs.contains(tab)) { myTabs.add(tab); } if (myTabbedPaneWrapper.indexOfComponent(tab.getTabComponent()) < 0) { myTabbedPaneWrapper.addTab(tab.getTitle(), tab.getIcon(), tab.getTabComponent(), tab.getToolTip()); } } public void insertTab(Tab tab, int index) { if (tab == null || tab.getTabComponent() == null) { return; } if (tab.getToolTip() == null && tab instanceof BaseTab) { ((BaseTab) tab).setToolTip(tab.getTitle()); } if (!myTabs.contains(tab)) { myTabs.add(tab); } if (myTabbedPaneWrapper.indexOfComponent(tab.getTabComponent()) < 0) { myTabbedPaneWrapper.insertTab(tab.getTitle(), tab.getIcon(), tab.getTabComponent(), tab.getToolTip(), index); } } private void removeTab(int index) { if (index < 0) { return; } myTabbedPaneWrapper.removeTabAt(index); } protected void removeTab(Tab tab) { if (tab == null) { return; } removeTab(myTabbedPaneWrapper.indexOfComponent(tab.getTabComponent())); myTabs.remove(tab); } public int indexOfTab(Tab tab) { return myTabbedPaneWrapper.indexOfComponent(tab.getTabComponent()); } public boolean containsTab(Tab tab) { return myTabbedPaneWrapper.indexOfComponent(tab.getTabComponent()) >= 0; } public final void addChangeListener(final ChangeListener listener) { if (myTabbedPaneWrapper != null) { myTabbedPaneWrapper.addChangeListener(listener); } } @Override public void apply() throws ConfigurationException { ThreadUtils.assertEDT(); //see MPS-18743 new SaveRepositoryCommand(myProject.getRepository()).execute(); myProject.getModelAccess().executeCommand(new Runnable() { @Override public void run() { for (Tab tab : myTabs) { tab.apply(); } save(); } }); } @Override public boolean isModified() { for (Tab tab : myTabs) { if (tab.isModified()) { return true; } } return false; } @Override public void reset() { } @Override public void disposeUIResources() { Disposer.dispose(this); } /** * If apply method in each tab separately take a lot of time, * override this method to perform real save after all applies */ protected void save() { } /** * All modules visible in the current project */ protected final Iterable<SModule> getProjectModules() { // wrap into Iterable to ensure lazy construction of module sequence. // getModules operation requires read access, but I don't see a reason to // move creation of conditional sequence into a read runnable. return new Iterable<SModule>() { @Override public Iterator<SModule> iterator() { return myProject.getRepository().getModules().iterator(); } }; } // keep usage view options common to properties page in a single place /*package*/ void showUsageImpl(SearchQuery query, IResultProvider provider) { final UsageToolOptions uvOpt = new UsageToolOptions().allowRunAgain(true).forceNewTab(true).navigateIfSingle(false).transientView(true); UsagesViewTool.showUsages(ProjectHelper.toIdeaProject(myProject), provider, query, uvOpt); } public abstract class CommonTab extends BaseTab { protected JTextField myTextFieldName; public CommonTab() { super(PropertiesBundle.message("mps.properties.common.title"), AllIcons.General.ProjectSettings, PropertiesBundle.message("mps.properties.common.tip")); } protected abstract String getConfigItemName(); protected abstract String getConfigItemPath(); protected abstract JComponent getBottomComponent(); protected JComponent getTopComponent() { return null; } @Override public void init() { JComponent topComponent = getTopComponent(); int rowCount = 0; JPanel sourcesTab = new JPanel(); sourcesTab.setLayout(new GridLayoutManager(topComponent != null ? 4 : 3, 2, INSETS, -1, -1)); JBLabel label = new JBLabel(PropertiesBundle.message("mps.properties.common.namelabel")); sourcesTab.add(label, new GridConstraints(rowCount++, 0, 1, 1, GridConstraints.ANCHOR_NORTHWEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); myTextFieldName = new JTextField(); myTextFieldName.setText(getConfigItemName()); sourcesTab.add(myTextFieldName, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_NORTHWEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); label = new JBLabel(PropertiesBundle.message("mps.properties.common.filepathlabel")); sourcesTab.add(label, new GridConstraints(rowCount, 0, 1, 1, GridConstraints.ANCHOR_NORTHWEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); JTextField textField = new JTextField(); textField.setEditable(false); textField.setText(getConfigItemPath()); sourcesTab.add(textField, new GridConstraints(rowCount++, 1, 1, 1, GridConstraints.ANCHOR_NORTHWEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); if (topComponent != null) { sourcesTab.add(topComponent, new GridConstraints(rowCount++, 0, 1, 2, GridConstraints.ANCHOR_NORTHWEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); } sourcesTab.add(getBottomComponent(), new GridConstraints(rowCount, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); setTabComponent(sourcesTab); } @Override public boolean isModified() { return !myTextFieldName.getText().equals(getConfigItemName()); } } public abstract class DependenciesTab extends BaseTab { protected DependTableModel myDependTableModel; public DependenciesTab() { super(PropertiesBundle.message("mps.properties.dependencies.title"), General.Dependencies, PropertiesBundle.message("mps.properties.dependencies.tip")); } protected abstract DependTableModel getDependTableModel(); /*CellEditor for scope cell */ protected abstract TableCellEditor getTableCellEditor(); @Override public void init() { JPanel dependenciesTab = new JPanel(); dependenciesTab.setLayout(new GridLayoutManager(1, 1, INSETS, -1, -1)); final JBTable tableDepend = new JBTable(); tableDepend.setShowHorizontalLines(false); tableDepend.setShowVerticalLines(false); tableDepend.setAutoCreateRowSorter(false); tableDepend.setAutoscrolls(true); tableDepend.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); myDependTableModel = getDependTableModel(); tableDepend.setModel(myDependTableModel); tableDepend.setDefaultRenderer(DependenciesTableItem.class, getTableCellRender()); tableDepend.setDefaultRenderer(Boolean.class, new BooleanTableCellRenderer()); tableDepend.setDefaultRenderer(SDependencyScope.class, new ComboBoxTableRenderer<>(SDependencyScope.values())); tableDepend.setDefaultEditor(SDependencyScope.class, getTableCellEditor()); TableColumn column; if (myDependTableModel.getExportColumnIndex() >= 0) { column = tableDepend.getTableHeader().getColumnModel().getColumn(myDependTableModel.getExportColumnIndex()); column.setMinWidth(20); column.setPreferredWidth(50); column.setMaxWidth(50); } if (myDependTableModel.getRoleColumnIndex() >= 0) { column = tableDepend.getTableHeader().getColumnModel().getColumn(myDependTableModel.getRoleColumnIndex()); column.setMinWidth(80); column.setPreferredWidth(130); column.setMaxWidth(200); } if (myDependTableModel.getItemColumnIndex() >= 0) { column = tableDepend.getTableHeader().getColumnModel().getColumn(myDependTableModel.getItemColumnIndex()); column.setPreferredWidth(250); } ToolbarDecorator decorator = ToolbarDecorator.createDecorator(tableDepend); decorator.setAddAction(getAnActionButtonRunnable()).setRemoveAction(new RemoveEntryAction(tableDepend) { @Override protected boolean confirmRemove(int row) { return DependenciesTab.this.confirmRemove(myDependTableModel.getValueAt(row, myDependTableModel.getItemColumnIndex())); } }); FindActionButton findActionButton = getFindAnAction(tableDepend); if (findActionButton != null) { decorator.addExtraAction(findActionButton); } decorator.setPreferredSize(new Dimension(500, 300)); JPanel table = decorator.createPanel(); table.setBorder(IdeBorderFactory.createBorder()); dependenciesTab.add(table, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); setTabComponent(dependenciesTab); new TableColumnSearch(tableDepend, myDependTableModel.getItemColumnIndex()).setComparator(new SpeedSearchComparator(false, true)); } /*CellRenderer for module column, actual value supplied to renderer is DependenciesTableItem instance */ protected abstract TableCellRenderer getTableCellRender(); protected boolean confirmRemove(final Object value) { return true; } @Nullable protected FindActionButton getFindAnAction(JBTable table) { return null; } @Override public boolean isModified() { return myDependTableModel.isModified(); } @Override public void apply() { myDependTableModel.apply(); } protected abstract AnActionButtonRunnable getAnActionButtonRunnable(); } public abstract class UsedLanguagesTab extends BaseTab { protected UsedLangsTableModel myUsedLangsTableModel; protected JBTable myUsedLangsTable; public UsedLanguagesTab() { super(PropertiesBundle.message("mps.properties.usedlanguages.title"), IdeIcons.LANGUAGE_ICON, PropertiesBundle.message("mps.properties.usedlanguages.tip")); } protected abstract UsedLangsTableModel getUsedLangsTableModel(); protected TableCellRenderer getTableCellRender() { SRepository contextRepo = myProject.getRepository(); LanguageTableCellRenderer tcr = new LanguageTableCellRenderer(contextRepo); tcr.addCellState(NotCondition.negate(new ValidImportCondition(contextRepo)), DependencyCellState.NOT_AVAILABLE); return tcr; } @Override public void init() { JPanel usedLangsTab = new JPanel(); usedLangsTab.setLayout(new GridLayoutManager(1, 1, INSETS, -1, -1)); final JBTable usedLangsTable = new JBTable(); usedLangsTable.setShowHorizontalLines(false); usedLangsTable.setShowVerticalLines(false); usedLangsTable.setAutoCreateRowSorter(false); usedLangsTable.setAutoscrolls(true); usedLangsTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); myUsedLangsTableModel = getUsedLangsTableModel(); usedLangsTable.setModel(myUsedLangsTableModel); myUsedLangsTable = usedLangsTable; final TableCellRenderer tableCellRender = getTableCellRender(); usedLangsTable.setDefaultRenderer(UsedLangsTableModel.Import.class, tableCellRender); ToolbarDecorator decorator = createToolbar(usedLangsTable); decorator.setPreferredSize(new Dimension(500, 300)); JPanel table = decorator.createPanel(); table.setBorder(IdeBorderFactory.createBorder()); usedLangsTab.add(table, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); setTabComponent(usedLangsTab); new TableColumnSearch(usedLangsTable, UsedLangsTableModel.ITEM_COLUMN).setComparator(new SpeedSearchComparator(false, true)); } protected ToolbarDecorator createToolbar(JBTable usedLangsTable) { return ToolbarDecorator.createDecorator(usedLangsTable); } @Override public boolean isModified() { return myUsedLangsTableModel.isModified(); } protected final List<SLanguage> getSelectedLanguages() { final List<SLanguage> languages = new LinkedList<>(); myProject.getModelAccess().runReadAction(new Runnable() { @Override public void run() { for (int i : myUsedLangsTable.getSelectedRows()) { Object value = myUsedLangsTableModel.getValueAt(i, UsedLangsTableModel.ITEM_COLUMN); if (value instanceof UsedLangsTableModel.Import) { final Import entry = (Import) value; if (entry.myLanguage != null) { languages.add(entry.myLanguage); } else { final SModule devkit = entry.myDevKit.resolve(myProject.getRepository()); if (devkit instanceof DevKit) { languages.addAll(IterableUtil.asCollection(((DevKit) devkit).getAllExportedLanguageIds())); } } } } } }); return languages; } } public abstract static class FindActionButton extends AnActionButton { protected final JBTable myTable; public FindActionButton(JBTable table) { myTable = table; this.getTemplatePresentation().setEnabledAndVisible(true); this.getTemplatePresentation().setIcon(Actions.Find); this.getTemplatePresentation().setText("Find usages"); } @Override public boolean isEnabled() { return !(myTable.getSelectionModel().isSelectionEmpty()); } @Override public ShortcutSet getShortcut() { return CommonShortcuts.getFind(); } } /** * Search in a single column of a table. * <p> * To extract text, recognizes SModelReference, SModuleReference as column values, otherwise resort to Object.toString(). Please * refactor instead sub-classing if different behavior is desired. * <p> * Might use com.intellij.ui.TableSpeedSearch instead, if there would be any explanation about what to override there. */ protected static class TableColumnSearch extends SpeedSearchBase<JBTable> { private final int myColumnIndex; public TableColumnSearch(JBTable table, int columnIndex) { super(table); myColumnIndex = columnIndex; } @Override public int getSelectedIndex() { return myComponent.getSelectedRow(); } @Override protected int convertIndexToModel(int viewIndex) { return myComponent.convertRowIndexToModel(viewIndex); } @Override public Object[] getAllElements() { final TableModel tableModel = myComponent.getModel(); final int count = tableModel.getRowCount(); Object[] elements = new Object[count]; for (int idx = 0; idx < count; idx++) { elements[idx] = tableModel.getValueAt(idx, myColumnIndex); } return elements; } @Override public String getElementText(Object element) { if (element instanceof SModuleReference) { return ((SModuleReference) element).getModuleName(); } if (element instanceof SModelReference) { return ((SModelReference) element).getModelName(); } return String.valueOf(element); } @Override public void selectElement(Object element, String selectedText) { final TableModel tableModel = myComponent.getModel(); final int count = tableModel.getRowCount(); for (int row = 0; row < count; row++) { if (element.equals(tableModel.getValueAt(row, myColumnIndex))) { final int viewRow = myComponent.convertRowIndexToView(row); myComponent.getSelectionModel().setSelectionInterval(viewRow, viewRow); TableUtil.scrollSelectionToVisible(myComponent); break; } } } } protected static class RemoveEntryAction implements AnActionButtonRunnable { private final JTable myTable; public RemoveEntryAction(@NotNull JTable table) { myTable = table; } @Override public void run(AnActionButton anActionButton) { int first = myTable.getSelectionModel().getMinSelectionIndex(); int last = myTable.getSelectionModel().getMaxSelectionIndex(); for (int i : myTable.getSelectedRows()) { if (!confirmRemove(i)) { return; } } TableUtil.removeSelectedItems(myTable); if (myTable.getModel() instanceof AbstractTableModel) { ((AbstractTableModel) myTable.getModel()).fireTableRowsDeleted(first, last); } first = Math.max(0, first - 1); myTable.getSelectionModel().setSelectionInterval(first, first); } protected boolean confirmRemove(int row) { return true; } } public static final JBInsets INSETS = new JBInsets(10, 10, 10, 10); }