/*
* Copyright 2015 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.ideamindmap.utils;
import com.igormaznitsa.ideamindmap.editor.MindMapDocumentEditor;
import com.igormaznitsa.ideamindmap.filetype.MindMapFileType;
import com.igormaznitsa.ideamindmap.lang.MMDFile;
import com.igormaznitsa.ideamindmap.lang.psi.PsiExtraFile;
import com.igormaznitsa.ideamindmap.swing.ColorChooserButton;
import com.igormaznitsa.ideamindmap.swing.FileEditPanel;
import com.igormaznitsa.ideamindmap.swing.PlainTextEditor;
import com.igormaznitsa.ideamindmap.swing.UriEditPanel;
import com.igormaznitsa.meta.annotation.MustNotContainNull;
import com.igormaznitsa.mindmap.model.MMapURI;
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.DialogProvider;
import com.igormaznitsa.mindmap.swing.panel.HasPreferredFocusComponent;
import com.intellij.CommonBundle;
import com.intellij.ide.BrowserUtil;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtil;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.ui.popup.BalloonBuilder;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.WindowManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiManager;
import com.intellij.psi.search.FileTypeIndex;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.util.indexing.FileBasedIndex;
import com.intellij.util.ui.UIUtil;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.velocity.exception.MethodInvocationException;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.swing.*;
import javax.swing.filechooser.FileFilter;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
public final class IdeaUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(IdeaUtils.class);
private static final ResourceBundle BUNDLE = java.util.ResourceBundle.getBundle("/i18n/Bundle");
public static final MMapURI EMPTY_URI;
public static final String PROJECT_KNOWLEDGE_FOLDER_NAME = ".projectKnowledge";
private static final boolean ALLOWS_TRANSACTION_GUARD = true;
static {
try {
EMPTY_URI = new MMapURI("http://igormaznitsa.com/specialuri#empty"); //NOI18N
} catch (URISyntaxException ex) {
throw new Error("Unexpected exception", ex); //NOI18N
}
}
@Nullable
public static VirtualFile findPotentialRootFolderForModule(@Nullable final Module module) {
VirtualFile moduleRoot = module == null ? null : module.getModuleFile();
if (moduleRoot != null) {
moduleRoot = moduleRoot.isDirectory() ? moduleRoot : moduleRoot.getParent();
if (moduleRoot.getName().equals(".idea")) {
moduleRoot = moduleRoot.getParent();
}
}
return moduleRoot;
}
@Nullable
public static Object callGetInstance(@Nonnull final String className) {
Object result = null;
try {
final Class<?> foundClass = Class.forName(className);
try {
final Method instanceMethod = foundClass.getMethod("getInstance");
if (Modifier.isStatic(instanceMethod.getModifiers())) {
result = instanceMethod.invoke(null);
}
} catch (MethodInvocationException ex) {
LOGGER.error("Error during dynamic getInstance() call", ex);
} catch (Exception ex) {
result = null;
}
} catch (final ClassNotFoundException ex) {
result = null;
}
return result;
}
public static boolean submitTransactionLater(@Nonnull final Runnable runnable) {
final Object transactionGuardInstance = callGetInstance("com.intellij.openapi.application.TransactionGuard");
boolean result = false;
if (transactionGuardInstance != null) {
result = safeInvokeMethodNoResult(transactionGuardInstance, "submitTransactionLater", new Class<?>[]{Disposable.class, Runnable.class}, new Object[]{new Disposable() {
@Override
public void dispose() {
}
}, runnable});
}
return result;
}
public static void executeWriteAction(@Nullable final Project project, @Nullable final Document document, @Nonnull final Runnable action) {
final Runnable wrapper = new Runnable() {
@Override
public void run() {
CommandProcessor.getInstance().executeCommand(project, new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().runWriteAction(action);
}
}, "MMD.executeWriteAction", null, document);
}
};
if (ALLOWS_TRANSACTION_GUARD && submitTransactionLater(new Runnable() {
@Override
public void run() {
wrapper.run();
}
})) {
LOGGER.info("Using TransactionGuard for write action");
} else {
LOGGER.info("Using CommandProcessor for write action");
wrapper.run();
}
}
public static void executeReadAction(@Nullable final Project project, @Nullable final Document document, @Nonnull final Runnable action) {
final Runnable wrapper = new Runnable() {
@Override
public void run() {
CommandProcessor.getInstance().executeCommand(project, new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().runReadAction(action);
}
}, "MMD>executeReadAction", null, document);
}
};
if (ALLOWS_TRANSACTION_GUARD && submitTransactionLater(new Runnable() {
@Override
public void run() {
wrapper.run();
}
})) {
LOGGER.info("Using TransactionGuard for read action");
} else {
LOGGER.info("Using CommandProcessor for read action");
wrapper.run();
}
}
@SuppressWarnings("unchecked")
@Nullable
public static <T> T safeInvokeMethodForResult(@Nonnull final Object instance, @Nonnull final T defaultResult, @Nonnull final String methodName, @Nonnull @MustNotContainNull final Class<?>[] argumentClasses, @Nonnull @MustNotContainNull final Object[] arguments) {
final Method method;
try {
method = instance.getClass().getMethod(methodName, argumentClasses);
} catch (NoSuchMethodException ex) {
LOGGER.info("Can't find method '" + methodName + "' in class " + instance.getClass().getName());
return defaultResult;
}
try {
return (T) method.invoke(instance, arguments);
} catch (Exception ex) {
LOGGER.error("Error during call " + instance.getClass().getName() + "." + methodName + ", default result will be returned");
}
return defaultResult;
}
public static boolean safeInvokeMethodNoResult(@Nonnull final Object instance, @Nonnull final String methodName, @Nonnull @MustNotContainNull final Class<?>[] argumentClasses, @Nonnull @MustNotContainNull final Object[] arguments) {
final Method method;
try {
method = instance.getClass().getMethod(methodName, argumentClasses);
} catch (NoSuchMethodException ex) {
LOGGER.info("Can't find method '" + methodName + "' in class " + instance.getClass().getName());
return false;
}
try {
method.invoke(instance, arguments);
return true;
} catch (Exception ex) {
LOGGER.error("Error during call " + instance.getClass().getName() + "." + methodName);
}
return false;
}
@Nullable
public static VirtualFile findKnowledgeFolderForModule(@Nullable final Module module, final boolean createIfMissing) {
final VirtualFile rootFolder = IdeaUtils.findPotentialRootFolderForModule(module);
final AtomicReference<VirtualFile> result = new AtomicReference<VirtualFile>();
if (rootFolder != null) {
result.set(rootFolder.findChild(PROJECT_KNOWLEDGE_FOLDER_NAME));
if (result.get() == null || !result.get().isDirectory()) {
if (createIfMissing) {
CommandProcessor.getInstance().executeCommand(module.getProject(), new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
try {
result.set(VfsUtil.createDirectoryIfMissing(rootFolder, PROJECT_KNOWLEDGE_FOLDER_NAME));
LOGGER.info("Created knowledge folder for " + module);
} catch (IOException ex) {
LOGGER.error("Can't create knowledge folder for " + module, ex);
}
}
});
}
}, null, null);
} else {
result.set(null);
}
}
}
return result.get();
}
@Nullable
public static Module findModuleForFile(@Nullable final Project project, @Nullable final VirtualFile file) {
return project == null || file == null ? null : ModuleUtil.findModuleForFile(file, project);
}
public static boolean browseURI(final URI uri, final boolean useInsideBrowser) {
try {
BrowserUtil.browse(uri);
} catch (Exception ex) {
ex.printStackTrace();
return false;
}
return true;
}
public static void openInSystemViewer(@Nonnull final DialogProvider dialogProvider, @Nullable final VirtualFile theFile) {
final File file = vfile2iofile(theFile);
if (file == null) {
LOGGER.error("Can't find file to open, null provided");
dialogProvider.msgError("Can't find file to open");
} else {
final Runnable startEdit = new Runnable() {
@Override
public void run() {
boolean ok = false;
if (Desktop.isDesktopSupported()) {
final Desktop dsk = Desktop.getDesktop();
if (dsk.isSupported(Desktop.Action.OPEN)) {
try {
dsk.open(file);
ok = true;
} catch (Throwable ex) {
LOGGER.error("Can't open file in system viewer : " + file, ex);//NOI18N
}
}
}
if (!ok) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
dialogProvider.msgError("Can't open file in system viewer! See the log!");//NOI18N
Toolkit.getDefaultToolkit().beep();
}
});
}
}
};
final Thread thr = new Thread(startEdit, " MMDStartFileEdit");//NOI18N
thr.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(final Thread t, final Throwable e) {
LOGGER.error("Detected uncaught exception in openInSystemViewer() for file " + file, e);
}
});
thr.setDaemon(true);
thr.start();
}
}
public static boolean isDarkTheme() {
return UIUtil.isUnderDarcula();
}
private static class DialogComponent extends DialogWrapper {
private final JComponent component;
private final JComponent prefferedComponent;
public DialogComponent(final Project project, final String title, final JComponent component, final JComponent prefferedComponent, final boolean defaultButtonEnabled) {
super(project, false, IdeModalityType.PROJECT);
this.component = component;
this.prefferedComponent = prefferedComponent == null ? component : prefferedComponent;
init();
setTitle(title);
if (!defaultButtonEnabled)
getRootPane().setDefaultButton(null);
}
@Nullable
@Override
public JComponent getPreferredFocusedComponent() {
return this.prefferedComponent == null ? this.component : this.prefferedComponent;
}
@Nullable
@Override
protected JComponent createCenterPanel() {
return this.component;
}
}
public static File chooseFile(final Component parent, final boolean filesOnly, final String title, final File selectedFile, final FileFilter filter) {
final JFileChooser chooser = new JFileChooser(selectedFile);
chooser.setApproveButtonText("Select");
if (filter != null)
chooser.setFileFilter(filter);
chooser.setDialogTitle(title);
chooser.setMultiSelectionEnabled(false);
if (filesOnly)
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
else
chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
if (chooser.showOpenDialog(parent) == JFileChooser.APPROVE_OPTION) {
return chooser.getSelectedFile();
} else {
return null;
}
}
public static String editText(final Project project, final String title, final String text) {
final PlainTextEditor editor = new PlainTextEditor(project, text);
editor.setPreferredSize(new Dimension(550, 450));
final DialogComponent dialog = new DialogComponent(project, title, editor, editor.getEditor(), false);
return dialog.showAndGet() ? editor.getEditor().getText() : null;
}
public static boolean plainMessageOkCancel(final Project project, final String title, final JComponent centerComponent) {
final DialogComponent dialog = new DialogComponent(project, title, centerComponent,
centerComponent instanceof HasPreferredFocusComponent ? ((HasPreferredFocusComponent) centerComponent).getComponentPreferredForFocus() : centerComponent, true);
return dialog.showAndGet();
}
public static void plainMessageClose(final Project project, final String title, final JComponent centerComponent) {
final DialogComponent dialog = new DialogComponent(project, title, centerComponent,
centerComponent instanceof HasPreferredFocusComponent ? ((HasPreferredFocusComponent) centerComponent).getComponentPreferredForFocus() : centerComponent, true) {
@Nonnull
@Override
protected Action[] createActions() {
return new Action[]{new DialogWrapperAction(CommonBundle.getCloseButtonText()) {
@Override
protected void doAction(ActionEvent e) {
doCancelAction();
}
}};
}
};
dialog.show();
}
@Nullable
public static File vfile2iofile(@Nullable final VirtualFile vfFile) {
return vfFile == null ? null : VfsUtilCore.virtualToIoFile(vfFile);
}
public static Color extractCommonColorForColorChooserButton(final String colorAttribute, final Topic[] topics) {
Color result = null;
for (final Topic t : topics) {
final Color color = html2color(t.getAttribute(colorAttribute), false);
if (result == null) {
result = color;
} else {
if (!result.equals(color)) {
return ColorChooserButton.DIFF_COLORS;
}
}
}
return result;
}
public static Color html2color(final String str, final boolean hasAlpha) {
Color result = null;
if (str != null && !str.isEmpty() && str.charAt(0) == '#') {
try {
result = new Color(Integer.parseInt(str.substring(1), 16), hasAlpha);
} catch (NumberFormatException ex) {
LOGGER.warn(String.format("Can't convert %s to color", str));
}
}
return result;
}
public static FileEditPanel.DataContainer editFilePath(final MindMapDocumentEditor editor, final String title, final File projectFolder, final FileEditPanel.DataContainer data) {
final FileEditPanel filePathEditor = new FileEditPanel(editor.getDialogProvider(), projectFolder, data);
filePathEditor.doLayout();
filePathEditor.setPreferredSize(new Dimension(450, filePathEditor.getPreferredSize().height));
if (plainMessageOkCancel(editor.getProject(), title, filePathEditor)) {
final FileEditPanel.DataContainer result = filePathEditor.getData();
if (result.isValid()) {
return result;
} else {
Messages.showErrorDialog(editor.getMindMapPanel(), String.format(BUNDLE.getString("MMDGraphEditor.editFileLinkForTopic.errorCantFindFile"), result.getPath()), "Error");
return null;
}
} else {
return null;
}
}
public static MMapURI editURI(final MindMapDocumentEditor editor, final String title, final MMapURI uri) {
final UriEditPanel uriEditor = new UriEditPanel(uri == null ? null : uri.asString(false, false));
uriEditor.doLayout();
uriEditor.setPreferredSize(new Dimension(450, uriEditor.getPreferredSize().height));
if (plainMessageOkCancel(editor.getProject(), title, uriEditor)) {
final String text = uriEditor.getText();
if (text.isEmpty()) {
return EMPTY_URI;
}
try {
if (!new URI(text).isAbsolute())
throw new URISyntaxException(text, "URI is not absolute one");
return new MMapURI(text.trim());
} catch (URISyntaxException ex) {
editor.getDialogProvider()
.msgError(String.format(BUNDLE.getString("NbUtils.errMsgIllegalURI"), text));
return null;
}
} else {
return null;
}
}
public static boolean isInProjectContentRoot(@Nonnull final Project project, @Nonnull final VirtualFile file) {
for (final VirtualFile root : ProjectRootManager.getInstance(project).getContentRoots()) {
if (VfsUtil.isAncestor(root, file, false)) {
return true;
}
}
return false;
}
public static void showPopup(@Nonnull final String text, @Nonnull final MessageType type) {
SwingUtils.safeSwing(new Runnable() {
@Override
public void run() {
final JBPopupFactory factory = JBPopupFactory.getInstance();
final BalloonBuilder builder = factory.createHtmlTextBalloonBuilder(StringEscapeUtils.escapeHtml(text), type, null);
final Balloon balloon = builder.createBalloon();
balloon.setAnimationEnabled(true);
final Component frame = WindowManager.getInstance().findVisibleFrame();
if (frame != null)
balloon.show(new RelativePoint(frame, new Point(frame.getWidth(), frame.getHeight())), Balloon.Position.below);
}
});
}
@Nullable
public static File findProjectFolder(@Nullable final Project project) {
if (project == null) return null;
return IdeaUtils.vfile2iofile(project.getBaseDir());
}
@Nullable
public static File findProjectFolder(@Nullable final PsiElement element) {
if (element == null)
return null;
return findProjectFolder(element.getProject());
}
public static List<PsiExtraFile> findPsiFileLinksForProjectScope(final Project project) {
List<PsiExtraFile> result = new ArrayList<PsiExtraFile>();
Collection<VirtualFile> virtualFiles = FileBasedIndex.getInstance().getContainingFiles(FileTypeIndex.NAME, MindMapFileType.INSTANCE,
GlobalSearchScope.allScope(project));
for (VirtualFile virtualFile : virtualFiles) {
final MMDFile simpleFile = (MMDFile) PsiManager.getInstance(project).findFile(virtualFile);
if (simpleFile != null) {
final PsiExtraFile[] fileLinks = PsiTreeUtil.getChildrenOfType(simpleFile, PsiExtraFile.class);
if (fileLinks != null) {
Collections.addAll(result, fileLinks);
}
}
}
return result;
}
@Nonnull
public static GlobalSearchScope moduleScope(@Nonnull Project project, @Nullable Module module) {
return module != null ? moduleScope(module) : GlobalSearchScope.projectScope(project);
}
@Nonnull
public static GlobalSearchScope moduleScope(@Nonnull PsiElement element) {
return moduleScope(element.getProject(), ModuleUtilCore.findModuleForPsiElement(element));
}
@Nonnull
public static GlobalSearchScope moduleScope(@Nonnull Module module) {
return GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(module).uniteWith(module.getModuleContentWithDependenciesScope());
}
}