Java tutorial
/* * The MIT License (MIT) * * Copyright (c) 2015 hsz Jakub Chrzanowski <jakub@hsz.mobi> * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package mobi.hsz.idea.gitignore.ui; import com.intellij.icons.AllIcons; import com.intellij.ide.CommonActionsManager; import com.intellij.ide.DefaultTreeExpander; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.EditorFactory; import com.intellij.openapi.editor.markup.HighlighterTargetArea; import com.intellij.openapi.editor.markup.TextAttributes; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.openapi.ui.OptionAction; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.PsiFile; import com.intellij.ui.*; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.tree.TreeUtil; import mobi.hsz.idea.gitignore.IgnoreBundle; import mobi.hsz.idea.gitignore.command.AppendFileCommandAction; import mobi.hsz.idea.gitignore.command.CreateFileCommandAction; import mobi.hsz.idea.gitignore.util.Resources; import mobi.hsz.idea.gitignore.util.Utils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreePath; import java.awt.*; import java.awt.event.ActionEvent; import java.util.List; import java.util.Set; /** * {@link GeneratorDialog} responsible for displaying list of all available templates and adding selected ones * to the specified file. * * @author Jakub Chrzanowski <jakub@hsz.mobi> * @since 0.2 */ public class GeneratorDialog extends DialogWrapper { /** {@link FilterComponent} search history key. */ private static final String TEMPLATES_FILTER_HISTORY = "TEMPLATES_FILTER_HISTORY"; /** Cache set to store checked templates for the current action. */ private final Set<Resources.Template> checked = ContainerUtil.newHashSet(); /** Current working project. */ @NotNull private final Project project; /** Current working file. */ @Nullable private PsiFile file; /** Templates tree root node. */ @NotNull private final TemplateTreeNode root; /** {@link CreateFileCommandAction} action instance to generate new file in the proper time. */ @Nullable private CreateFileCommandAction action; /** Templates tree with checkbox feature. */ private CheckboxTree tree; /** Tree expander responsible for expanding and collapsing tree structure. */ private DefaultTreeExpander treeExpander; /** Dynamic templates filter. */ private FilterComponent profileFilter; /** Preview editor with syntax highlight. */ private Editor preview; /** {@link Document} related to the {@link Editor} feature. */ private Document previewDocument; /** * Builds a new instance of {@link GeneratorDialog}. * * @param project current working project * @param file current working file */ public GeneratorDialog(@NotNull Project project, @Nullable PsiFile file) { super(project, false); this.project = project; this.file = file; this.root = new TemplateTreeNode(); this.action = null; setTitle(IgnoreBundle.message("dialog.generator.title")); setOKButtonText(IgnoreBundle.message("global.generate")); setCancelButtonText(IgnoreBundle.message("global.cancel")); init(); } /** * Builds a new instance of {@link GeneratorDialog}. * * @param project current working project * @param action {@link CreateFileCommandAction} action instance to generate new file in the proper time */ public GeneratorDialog(@NotNull Project project, @Nullable CreateFileCommandAction action) { this(project, (PsiFile) null); this.action = action; } /** * Returns component which should be focused when the dialog appears on the screen. * * @return component to focus */ @Nullable @Override public JComponent getPreferredFocusedComponent() { return profileFilter; } /** * Dispose the wrapped and releases all resources allocated be the wrapper to help * more efficient garbage collection. You should never invoke this method twice or * invoke any method of the wrapper after invocation of <code>dispose</code>. * * @throws IllegalStateException if the dialog is disposed not on the event dispatch thread */ @Override protected void dispose() { EditorFactory.getInstance().releaseEditor(preview); super.dispose(); } /** * This method is invoked by default implementation of "OK" action. It just closes dialog * with <code>OK_EXIT_CODE</code>. This is convenient place to override functionality of "OK" action. * Note that the method does nothing if "OK" action isn't enabled. */ @Override protected void doOKAction() { if (isOKActionEnabled()) { performAppendAction(false); } } /** * Performs {@link AppendFileCommandAction} action. * * @param ignoreDuplicates ignores duplicated rules */ private void performAppendAction(boolean ignoreDuplicates) { String content = ""; for (Resources.Template template : checked) { if (template == null) { continue; } content += IgnoreBundle.message("file.templateSection", template.getName()); content += "\n" + template.getContent(); } if (file == null && action != null) { file = action.execute().getResultObject(); } if (file != null && !content.isEmpty()) { new AppendFileCommandAction(project, file, content, ignoreDuplicates).execute(); } super.doOKAction(); } @Override protected void createDefaultActions() { super.createDefaultActions(); myOKAction = new OptionOkAction(); } /** * Factory method. It creates panel with dialog options. Options panel is located at the * center of the dialog's content pane. The implementation can return <code>null</code> * value. In this case there will be no options panel. * * @return center panel */ @Nullable @Override protected JComponent createCenterPanel() { // general panel final JPanel centerPanel = new JPanel(new BorderLayout()); centerPanel.setPreferredSize(new Dimension(800, 500)); // splitter panel - contains tree panel and preview component final JBSplitter splitter = new JBSplitter(false, 0.4f); centerPanel.add(splitter, BorderLayout.CENTER); final JPanel treePanel = new JPanel(new BorderLayout()); previewDocument = EditorFactory.getInstance().createDocument(""); preview = Utils.createPreviewEditor(previewDocument, project, true); splitter.setFirstComponent(treePanel); splitter.setSecondComponent(preview.getComponent()); /* Scroll panel for the templates tree. */ JScrollPane treeScrollPanel = createTreeScrollPanel(); treePanel.add(treeScrollPanel, BorderLayout.CENTER); final JPanel northPanel = new JPanel(new GridBagLayout()); northPanel.setBorder(IdeBorderFactory.createEmptyBorder(2, 0, 2, 0)); northPanel.add(createTreeActionsToolbarPanel(treeScrollPanel).getComponent(), new GridBagConstraints(0, 0, 1, 1, 1, 1, GridBagConstraints.BASELINE_LEADING, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0)); northPanel.add(profileFilter, new GridBagConstraints(1, 0, 1, 1, 1, 1, GridBagConstraints.BASELINE_TRAILING, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0)); treePanel.add(northPanel, BorderLayout.NORTH); return centerPanel; } /** * Creates scroll panel with templates tree in it. * * @return scroll panel */ private JScrollPane createTreeScrollPanel() { fillTreeData(null, true); final TemplateTreeRenderer renderer = new TemplateTreeRenderer() { protected String getFilter() { return profileFilter != null ? profileFilter.getFilter() : null; } }; tree = new CheckboxTree(renderer, root) { public Dimension getPreferredScrollableViewportSize() { Dimension size = super.getPreferredScrollableViewportSize(); size = new Dimension(size.width + 10, size.height); return size; } @Override protected void onNodeStateChanged(CheckedTreeNode node) { super.onNodeStateChanged(node); Resources.Template template = ((TemplateTreeNode) node).getTemplate(); if (node.isChecked()) { checked.add(template); } else { checked.remove(template); } } }; tree.setCellRenderer(renderer); tree.setRootVisible(false); tree.setShowsRootHandles(true); UIUtil.setLineStyleAngled(tree); TreeUtil.installActions(tree); tree.addTreeSelectionListener(new TreeSelectionListener() { public void valueChanged(TreeSelectionEvent e) { if (tree.getSelectionPaths() != null && tree.getSelectionPaths().length == 1) { updateDescriptionPanel(tree.getSelectionPaths()[0]); } } }); final JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(tree); scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); TreeUtil.expandAll(tree); treeExpander = new DefaultTreeExpander(tree); profileFilter = new TemplatesFilterComponent(); return scrollPane; } /** * Creates tree toolbar panel with actions for working with templates tree. * * @param target templates tree * @return action toolbar */ private ActionToolbar createTreeActionsToolbarPanel(JComponent target) { final CommonActionsManager actionManager = CommonActionsManager.getInstance(); DefaultActionGroup actions = new DefaultActionGroup(); actions.add(actionManager.createExpandAllAction(treeExpander, tree)); actions.add(actionManager.createCollapseAllAction(treeExpander, tree)); actions.add(new AnAction(IgnoreBundle.message("dialog.generator.unselectAll"), null, AllIcons.Actions.Unselectall) { @Override public void update(AnActionEvent e) { e.getPresentation().setEnabled(!checked.isEmpty()); } @Override public void actionPerformed(AnActionEvent e) { checked.clear(); filterTree(profileFilter.getTextEditor().getText()); } }); final ActionToolbar actionToolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, actions, true); actionToolbar.setTargetComponent(target); return actionToolbar; } /** * Updates editor's content depending on the selected {@link TreePath}. * * @param path selected tree path */ private void updateDescriptionPanel(TreePath path) { final TemplateTreeNode node = (TemplateTreeNode) path.getLastPathComponent(); final Resources.Template template = node.getTemplate(); ApplicationManager.getApplication().runWriteAction(new Runnable() { @Override public void run() { CommandProcessor.getInstance().runUndoTransparentAction(new Runnable() { @Override public void run() { String content = template != null ? StringUtil.replaceChar(template.getContent(), '\r', '\0') : ""; previewDocument.replaceString(0, previewDocument.getTextLength(), content); List<Pair<Integer, Integer>> pairs = getFilterRanges( profileFilter.getTextEditor().getText(), content); highlightWords(pairs); } }); } }); } /** * Fills templates tree with templates fetched with {@link Resources#getGitignoreTemplates()}. * * @param filter templates filter * @param forceInclude force include */ private void fillTreeData(String filter, boolean forceInclude) { root.removeAllChildren(); root.setChecked(false); for (Resources.Template.Container container : Resources.Template.Container.values()) { TemplateTreeNode node = new TemplateTreeNode(container); node.setChecked(false); root.add(node); } List<Resources.Template> templatesList = Resources.getGitignoreTemplates(); for (Resources.Template template : templatesList) { if (filter != null && filter.length() > 0 && !isTemplateAccepted(template, filter)) { continue; } final TemplateTreeNode node = new TemplateTreeNode(template); node.setChecked(checked.contains(template)); getGroupNode(root, template.getContainer()).add(node); } if (filter != null && forceInclude && root.getChildCount() == 0) { fillTreeData(filter, false); } TreeUtil.sort(root, new TemplateTreeComparator()); } /** * Creates or gets existing group node for specified element. * * @param root tree root node * @param container container type to search * @return group node */ private static TemplateTreeNode getGroupNode(TemplateTreeNode root, Resources.Template.Container container) { final int childCount = root.getChildCount(); for (int i = 0; i < childCount; i++) { TemplateTreeNode child = (TemplateTreeNode) root.getChildAt(i); if (container.equals(child.getContainer())) { return child; } } TemplateTreeNode child = new TemplateTreeNode(container); root.add(child); return child; } /** * Finds for the filter's words in the given content and returns their positions. * * @param filter templates filter * @param content templates content * @return text ranges */ private List<Pair<Integer, Integer>> getFilterRanges(String filter, String content) { List<Pair<Integer, Integer>> pairs = ContainerUtil.newArrayList(); content = content.toLowerCase(); for (String word : Utils.getWords(filter)) { for (int index = content.indexOf(word); index >= 0; index = content.indexOf(word, index + 1)) { pairs.add(Pair.create(index, index + word.length())); } } return pairs; } /** * Checks if given template is accepted by passed filter. * * @param template to check * @param filter templates filter * @return template is accepted */ private boolean isTemplateAccepted(Resources.Template template, String filter) { filter = filter.toLowerCase(); if (StringUtil.containsIgnoreCase(template.getName(), filter)) { return true; } boolean nameAccepted = true; for (String word : Utils.getWords(filter)) { if (!StringUtil.containsIgnoreCase(template.getName(), word)) { nameAccepted = false; } } List<Pair<Integer, Integer>> ranges = getFilterRanges(filter, template.getContent()); return nameAccepted || ranges.size() > 0; } /** * Filters templates tree. * * @param filter text */ private void filterTree(String filter) { if (tree != null) { fillTreeData(filter, true); reloadModel(); TreeUtil.expandAll(tree); if (tree.getSelectionPath() == null) { TreeUtil.selectFirstNode(tree); } } } /** * Highlights given text ranges in {@link #preview} content. * * @param pairs text ranges */ private void highlightWords(@NotNull List<Pair<Integer, Integer>> pairs) { final TextAttributes attr = new TextAttributes(); attr.setBackgroundColor(UIUtil.getTreeSelectionBackground()); attr.setForegroundColor(UIUtil.getTreeSelectionForeground()); for (Pair<Integer, Integer> pair : pairs) { preview.getMarkupModel().addRangeHighlighter(pair.first, pair.second, 0, attr, HighlighterTargetArea.EXACT_RANGE); } } /** * Reloads tree model. */ private void reloadModel() { ((DefaultTreeModel) tree.getModel()).reload(); } @Nullable public PsiFile getFile() { return file; } /** * Custom templates {@link FilterComponent}. */ private class TemplatesFilterComponent extends FilterComponent { /** Builds a new instance of {@link TemplatesFilterComponent}. */ public TemplatesFilterComponent() { super(TEMPLATES_FILTER_HISTORY, 10); } /** Filters tree using current filter's value. */ @Override public void filter() { filterTree(getFilter()); } } protected class OptionOkAction extends OkAction implements OptionAction { @NotNull @Override public Action[] getOptions() { return new Action[] { new DialogWrapperAction(IgnoreBundle.message("global.generate.without.duplicates")) { @Override protected void doAction(ActionEvent e) { performAppendAction(true); } } }; } } }