/*
* Copyright 2016 Igor Maznitsa.
*
* 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.igormaznitsa.sciareto.ui.tree;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.swing.DropMode;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import com.igormaznitsa.meta.annotation.MustNotContainNull;
import com.igormaznitsa.meta.common.utils.Assertions;
import com.igormaznitsa.mindmap.model.ExtraFile;
import com.igormaznitsa.mindmap.model.MMapURI;
import com.igormaznitsa.mindmap.model.MindMap;
import com.igormaznitsa.mindmap.model.Topic;
import com.igormaznitsa.mindmap.model.logger.Logger;
import com.igormaznitsa.mindmap.model.logger.LoggerFactory;
import com.igormaznitsa.mindmap.swing.panel.MindMapPanel;
import com.igormaznitsa.mindmap.swing.panel.utils.Utils;
import com.igormaznitsa.sciareto.Context;
import com.igormaznitsa.sciareto.Main;
import com.igormaznitsa.sciareto.ui.DialogProviderManager;
import com.igormaznitsa.sciareto.ui.FindUsagesPanel;
import com.igormaznitsa.sciareto.ui.UiUtils;
import com.igormaznitsa.sciareto.ui.editors.EditorContentType;
import com.igormaznitsa.sciareto.ui.editors.MMDEditor;
import com.igormaznitsa.sciareto.ui.tabs.TabTitle;
public final class ExplorerTree extends JScrollPane {
private static final long serialVersionUID = 3894835807758698784L;
private static final Logger LOGGER = LoggerFactory.getLogger(ExplorerTree.class);
private final DnDTree projectTree;
private final Context context;
public ExplorerTree(@Nonnull final Context context) {
super();
this.projectTree = new DnDTree();
this.context = context;
this.projectTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
this.projectTree.setDropMode(DropMode.ON);
this.projectTree.setEditable(true);
ToolTipManager.sharedInstance().registerComponent(this.projectTree);
this.projectTree.setCellRenderer(new TreeCellRenderer());
this.projectTree.setModel(new NodeProjectGroup(context, ".")); //NOI18N
this.projectTree.setRootVisible(false);
this.setViewportView(this.projectTree);
this.projectTree.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(@Nonnull final KeyEvent e) {
if (!e.isConsumed() && e.getModifiers() == 0 && e.getKeyCode() == KeyEvent.VK_ENTER) {
e.consume();
final TreePath selectedPath = projectTree.getSelectionPath();
if (selectedPath != null) {
final NodeFileOrFolder node = (NodeFileOrFolder) selectedPath.getLastPathComponent();
if (node != null) {
if (node.isLeaf()) {
final File file = node.makeFileForNode();
if (file != null && !context.openFileAsTab(file)) {
UiUtils.openInSystemViewer(file);
}
} else {
projectTree.expandPath(selectedPath);
}
}
}
}
}
});
this.projectTree.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(@Nonnull final MouseEvent e) {
if (e.getClickCount() > 1) {
final int selRow = projectTree.getRowForLocation(e.getX(), e.getY());
final TreePath selPath = projectTree.getPathForLocation(e.getX(), e.getY());
if (selRow >= 0) {
final NodeFileOrFolder node = (NodeFileOrFolder) selPath.getLastPathComponent();
if (node != null && node.isLeaf()) {
final File file = node.makeFileForNode();
if (file != null) {
if (context.openFileAsTab(file)) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
context.centerRootTopicIfFocusedMMD();
}
});
} else {
UiUtils.openInSystemViewer(file);
}
}
}
}
}
}
@Override
public void mouseReleased(@Nonnull final MouseEvent e) {
if (e.isPopupTrigger()) {
processPopup(e);
}
}
@Override
public void mousePressed(@Nonnull final MouseEvent e) {
if (e.isPopupTrigger()) {
processPopup(e);
}
}
private void processPopup(@Nonnull final MouseEvent e) {
final TreePath selPath = projectTree.getPathForLocation(e.getX(), e.getY());
if (selPath != null) {
projectTree.setSelectionPath(selPath);
final Object last = selPath.getLastPathComponent();
if (last instanceof NodeFileOrFolder) {
makePopupMenu((NodeFileOrFolder) last).show(e.getComponent(), e.getX(), e.getY());
}
}
}
});
}
public boolean hasSelectedItem() {
return this.projectTree.getSelectionPath() != null;
}
public void showPopUpForSelectedItem() {
final TreePath path = this.projectTree.getSelectionPath();
if (path != null) {
final NodeFileOrFolder component = (NodeFileOrFolder) path.getLastPathComponent();
final Rectangle rect = this.projectTree.getRowBounds(this.projectTree.getRowForPath(path));
final JPopupMenu popupMenu = makePopupMenu(component);
popupMenu.show(this.projectTree, rect.x + rect.width / 2, rect.y + rect.height / 2);
}
}
@Override
public void requestFocus() {
this.projectTree.requestFocus();
}
@Nonnull
@MustNotContainNull
public List<NodeFileOrFolder> findForNamePattern(@Nullable final Pattern namePattern) {
return getCurrentGroup().findForNamePattern(namePattern);
}
@Nonnull
@MustNotContainNull
public List<NodeFileOrFolder> findNodesForFile(@Nonnull final File file) {
return getCurrentGroup().findRelatedNodes(file, new ArrayList<NodeFileOrFolder>());
}
public void сloseProject(@Nonnull final NodeProject tree) {
((NodeProjectGroup) this.projectTree.getModel()).removeProject(tree);
this.context.onCloseProject(tree);
}
public void focusToFirstElement() {
this.projectTree.focusToFirstElement();
}
public void focusToFileItem(@Nonnull final File file) {
final NodeProjectGroup group = getCurrentGroup();
final TreePath pathToFile = group.findPathToFile(file);
if (pathToFile != null) {
this.projectTree.setSelectionPath(pathToFile);
this.projectTree.scrollPathToVisible(pathToFile);
}
}
public void unfoldProject(@Nonnull final NodeProject node) {
Utils.safeSwingCall(new Runnable() {
@Override
public void run() {
projectTree.expandPath(new TreePath(new Object[]{getCurrentGroup(), node}));
}
});
}
@Nonnull
private JPopupMenu makePopupMenu(@Nonnull final NodeFileOrFolder node) {
final JPopupMenu result = new JPopupMenu();
if (!node.isLeaf()) {
final JMenu makeNew = new JMenu("New...");
JMenuItem item = new JMenuItem("Folder");
item.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@Nonnull final ActionEvent e) {
addChildTo(node, null);
}
});
makeNew.add(item);
item = new JMenuItem("Mind map");
item.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@Nonnull final ActionEvent e) {
addChildTo(node, "mmd"); //NOI18N
}
});
makeNew.add(item);
item = new JMenuItem("Text");
item.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@Nonnull final ActionEvent e) {
addChildTo(node, "txt"); //NOI18N
}
});
makeNew.add(item);
result.add(makeNew);
}
if (!node.isProjectKnowledgeFolder()) {
final JMenuItem rename = new JMenuItem("Rename");
rename.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@Nonnull final ActionEvent e) {
projectTree.startEditingAtPath(node.makeTreePath());
}
});
result.add(rename);
}
if (node instanceof NodeProject) {
final JMenuItem close = new JMenuItem("Close");
close.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@Nonnull final ActionEvent e) {
сloseProject((NodeProject) node);
}
});
result.add(close);
final JMenuItem refresh = new JMenuItem("Reload");
refresh.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@Nonnull final ActionEvent e) {
getCurrentGroup().refreshProjectFolder((NodeProject) node);
}
});
result.add(refresh);
}
final JMenuItem delete = new JMenuItem("Delete");
delete.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@Nonnull final ActionEvent e) {
if (DialogProviderManager.getInstance().getDialogProvider().msgConfirmYesNo("Delete", "Do you really want to delete \"" + node + "\"?")) {
context.deleteTreeNode(node);
}
}
});
result.add(delete);
final JMenuItem openInSystem = new JMenuItem("Open in System");
openInSystem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@Nonnull final ActionEvent e) {
final File file = node.makeFileForNode();
if (file != null && file.exists()) {
UiUtils.openInSystemViewer(file);
}
}
});
result.add(openInSystem);
if (node instanceof NodeProject) {
final NodeProject theProject = (NodeProject) node;
if (!theProject.hasKnowledgeFolder()) {
final File knowledgeFolder = new File(theProject.getFolder(), Context.KNOWLEDGE_FOLDER);
final JMenuItem addKnowledgeFolder = new JMenuItem("Create Knowledge folder");
addKnowledgeFolder.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@Nonnull final ActionEvent e) {
if (knowledgeFolder.mkdirs()) {
getCurrentGroup().refreshProjectFolder(theProject);
context.focusInTree(knowledgeFolder);
} else {
LOGGER.error("Can't create knowledge folder : " + knowledgeFolder); //NOI18N
}
}
});
result.add(addKnowledgeFolder);
}
}
final String BUILD_GRAPH_ITEM = "Build file links graph";
if (node instanceof NodeProject) {
final JMenuItem buildMindMapGraph = new JMenuItem(BUILD_GRAPH_ITEM);
buildMindMapGraph.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@Nonnull final ActionEvent e) {
context.showGraphMindMapFileLinksDialog(((NodeProject) node).getFolder(), null, true);
}
});
result.add(buildMindMapGraph);
} else if (node.isLeaf() && node.isMindMapFile()) {
final JMenuItem buildMindMapGraph = new JMenuItem(BUILD_GRAPH_ITEM);
buildMindMapGraph.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@Nonnull final ActionEvent e) {
final NodeProject project = node.findProject();
context.showGraphMindMapFileLinksDialog(project == null ? null : project.getFolder(), node.makeFileForNode(), true);
}
});
result.add(buildMindMapGraph);
}
final List<JMenuItem> optional = new ArrayList<>();
final JMenuItem menuSearchUsage = new JMenuItem("Find usages in maps");
menuSearchUsage.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@Nonnull final ActionEvent e) {
if (context.hasUnsavedDocument() && !DialogProviderManager.getInstance().getDialogProvider().msgConfirmOkCancel("Detected unsaved documents", "Unsaved content will not be processed!")) {
return;
}
final FindUsagesPanel panel = new FindUsagesPanel(context, node, false);
if (DialogProviderManager.getInstance().getDialogProvider().msgOkCancel("Find usages in all opened projects", panel)) {
final NodeFileOrFolder selected = panel.getSelected();
panel.dispose();
if (selected != null) {
final File file = selected.makeFileForNode();
if (file != null) {
context.focusInTree(file);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
requestFocus();
}
});
}
}
} else {
panel.dispose();
}
}
});
optional.add(menuSearchUsage);
final TabTitle editingTab = this.context.getFocusedTab();
if (editingTab != null && editingTab.getType() == EditorContentType.MINDMAP) {
final JMenuItem addIntoMap = new JMenuItem("Add File as topic");
addIntoMap.addActionListener(new ActionListener() {
@Override
public void actionPerformed(@Nonnull final ActionEvent e) {
addTreeAsTopic(context.findProjectForFile(editingTab.getAssociatedFile()), node, ((MMDEditor) editingTab.getProvider().getEditor()));
}
});
optional.add(addIntoMap);
}
if (!optional.isEmpty()) {
result.add(new JSeparator());
for (final JMenuItem i : optional) {
result.add(i);
}
}
return result;
}
private void addTreeAsTopic(@Nullable final NodeProject project, @Nonnull final NodeFileOrFolder node, @Nonnull final MMDEditor editor) {
final File projectFolder = project == null ? null : project.getFolder();
if (project != null) {
if (node.findProject() != project) {
if (!DialogProviderManager.getInstance().getDialogProvider().msgConfirmOkCancel("Different projects", "Opened Map file from another project. File paths will not be relative ones.")) {
return;
}
}
}
final List<Topic> targetTopics = new ArrayList<>(Arrays.asList(editor.getMindMapPanel().getSelectedTopics()));
if (targetTopics.size() > 1) {
if (!DialogProviderManager.getInstance().getDialogProvider().msgConfirmOkCancel("Multiple selection detected", "New children will be generated for all focused topics.")) {
return;
}
} else {
if (targetTopics.isEmpty() && editor.getMindMapPanel().getModel().getRoot() != null) {
if (!DialogProviderManager.getInstance().getDialogProvider().msgConfirmOkCancel("No selected parent", "There is not selected topic. The Root will be used as the parent.")) {
return;
}
targetTopics.add(editor.getMindMapPanel().getModel().getRoot());
}
}
editor.getMindMapPanel().executeModelJobs(new MindMapPanel.ModelJob() {
@Nonnull
private Topic recursiveGenerateTopics(@Nullable final File projectFolder, @Nonnull final MindMap model, @Nullable final Topic parent, @Nonnull final NodeFileOrFolder node) {
final ExtraFile fileLink = new ExtraFile(new MMapURI(projectFolder, node.makeFileForNode(), null));
final Topic theTopic;
if (parent == null) {
theTopic = new Topic(model, null, node.toString(), fileLink);
} else {
theTopic = parent.makeChild(node.toString(), null);
theTopic.setExtra(fileLink);
}
if (!node.isLeaf()) {
final Enumeration<NodeFileOrFolder> children = node.children();
while (children.hasMoreElements()) {
recursiveGenerateTopics(projectFolder, model, theTopic, children.nextElement());
}
}
return theTopic;
}
@Override
public boolean doChangeModel(@Nonnull final MindMap model) {
Topic createdTopic = null;
if (targetTopics.isEmpty()) {
createdTopic = recursiveGenerateTopics(projectFolder, model, null, node);
} else {
boolean first = true;
for (final Topic t : targetTopics) {
final Topic generated = recursiveGenerateTopics(projectFolder, model, t, node);
if (first) {
createdTopic = generated;
}
first = false;
}
}
if (editor.getMindMapPanel().getSelectedTopics().length == 0 && createdTopic != null) {
final Topic forfocus = createdTopic;
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
editor.getMindMapPanel().focusTo(forfocus);
}
});
}
return true;
}
});
}
private void addChildTo(@Nonnull final NodeFileOrFolder folder, @Nullable final String extension) {
String fileName = JOptionPane.showInputDialog(Main.getApplicationFrame(), extension == null ? "Folder name" : "File name", extension == null ? "New folder" : "New " + extension.toUpperCase(Locale.ENGLISH) + " file", JOptionPane.QUESTION_MESSAGE);
if (fileName != null) {
fileName = fileName.trim();
if (NodeProjectGroup.FILE_NAME.matcher(fileName).matches()) {
if (extension != null) {
final String providedExtension = FilenameUtils.getExtension(fileName);
if (!extension.equalsIgnoreCase(providedExtension)) {
fileName += '.' + extension;
}
}
final File file = new File(folder.makeFileForNode(), fileName);
if (file.exists()) {
DialogProviderManager.getInstance().getDialogProvider().msgError("File '" + fileName + "' already exists!");
return;
}
boolean ok = false;
if (extension == null) {
if (!file.mkdirs()) {
LOGGER.error("Can't create folder"); //NOI18N
DialogProviderManager.getInstance().getDialogProvider().msgError("Can't create folder '" + fileName + "'!");
} else {
ok = true;
}
} else {
switch (extension) {
case "mmd": { //NOI18N
final MindMap model = new MindMap(null, true);
model.setAttribute("showJumps", "true"); //NOI18N
final Topic root = model.getRoot();
if (root != null) {
root.setText("Root"); //NOI18N
}
try {
FileUtils.write(file, model.write(new StringWriter()).toString(), "UTF-8"); //NOI18N
ok = true;
}
catch (IOException ex) {
LOGGER.error("Can't create MMD file", ex); //NOI18N
DialogProviderManager.getInstance().getDialogProvider().msgError("Can't create mind map '" + fileName + "'!");
}
}
break;
case "txt": { //NOI18N
try {
FileUtils.write(file, "", "UTF-8"); //NOI18N
ok = true;
}
catch (IOException ex) {
LOGGER.error("Can't create TXT file", ex); //NOI18N
DialogProviderManager.getInstance().getDialogProvider().msgError("Can't create txt file '" + fileName + "'!");
}
}
break;
default:
throw new Error("Unexpected extension : " + extension); //NOI18N
}
}
if (ok) {
getCurrentGroup().addChild(folder, file);
context.openFileAsTab(file);
context.focusInTree(file);
}
} else {
DialogProviderManager.getInstance().getDialogProvider().msgError("Illegal file name!");
}
}
}
public boolean deleteNode(@Nonnull final NodeFileOrFolder node) {
return getCurrentGroup().fireNotificationThatNodeDeleted(node);
}
@Nonnull
public NodeProjectGroup getCurrentGroup() {
return (NodeProjectGroup) this.projectTree.getModel();
}
public void setModel(@Nonnull final NodeProjectGroup model, final boolean expandFirst) {
this.projectTree.setModel(Assertions.assertNotNull(model));
if (expandFirst && model.getChildCount() > 0) {
this.projectTree.expandPath(new TreePath(new Object[]{model, model.getChildAt(0)}));
}
}
}