// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See License.txt in the project root. package com.microsoft.alm.plugin.idea.common.utils; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ApplicationNamesInfo; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.options.ShowSettingsUtil; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.wm.IdeFocusManager; import com.intellij.openapi.wm.IdeFrame; import com.microsoft.alm.plugin.external.tools.TfTool; import com.microsoft.alm.plugin.idea.common.resources.TfPluginBundle; import com.microsoft.alm.plugin.idea.tfvc.core.TFSVcs; import com.microsoft.alm.plugin.services.PluginServiceProvider; import com.microsoft.alm.plugin.services.PropertyService; import git4idea.GitVcs; import git4idea.config.GitExecutableValidator; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.Icon; import java.io.File; import java.io.FileNotFoundException; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLDecoder; import static com.intellij.openapi.ui.Messages.getWarningIcon; public class IdeaHelper { private static final Logger logger = LoggerFactory.getLogger(IdeaHelper.class); public static final PluginId PLUGIN_ID = PluginId.getId("com.microsoft.vso.idea"); private static final String CHARSET_UTF8 = "utf-8"; public static final String TEST_RESOURCES_SUB_PATH = "/externals/platform/"; private static final String PROD_RESOURCES_SUB_PATH = "platform"; public static final String RIDER_PRODUCT_NAME = "Rider"; public IdeaHelper() { } public static void setProgress(final ProgressIndicator indicator, final double fraction, final String text) { setProgress(indicator, fraction, text, false); } public static void setProgress(final ProgressIndicator indicator, final double fraction, final String text, final boolean delay) { if (indicator == null) { // Tests send null to skip showing progress return; } IdeaHelper.runOnUIThread(new Runnable() { @Override public void run() { indicator.setFraction(fraction); indicator.setText(text); } }); if (delay) { // Give time for the progress to be updated try { Thread.sleep(1000); } catch (InterruptedException e) { logger.warn("setting progress failed", e); } } } /** * Verifies if Git exe is configured, show notification and warning message if not * * @param project Idea project * @return true if Git exe is configured, false if Git exe is not correctly configured */ public static boolean isGitExeConfigured(@NotNull final Project project) { final GitExecutableValidator validator = GitVcs.getInstance(project).getExecutableValidator(); if (!validator.checkExecutableAndNotifyIfNeeded()) { //Git.exe is not configured, show warning message in addition to notification from Git plugin Messages.showWarningDialog(project, TfPluginBundle.message(TfPluginBundle.KEY_GIT_NOT_CONFIGURED), TfPluginBundle.message(TfPluginBundle.KEY_TF_GIT)); return false; } return true; } /** * Verifies if TF is configured, show notification and warning message if not * * @param project Idea project * @return true if TF is configured, false if TF is not correctly configured */ public static boolean isTFConfigured(@NotNull final Project project) { String tfLocation = TfTool.getLocation(); if (StringUtils.isEmpty(tfLocation)) { tfLocation = TfTool.tryDetectTf(); if (!StringUtils.isEmpty(tfLocation)) { PluginServiceProvider.getInstance().getPropertyService().setProperty(PropertyService.PROP_TF_HOME, tfLocation); return true; } //TF is not configured, show warning message int result = Messages.showDialog(project, TfPluginBundle.message(TfPluginBundle.KEY_TFVC_NOT_CONFIGURED), TfPluginBundle.message(TfPluginBundle.KEY_TFVC), new String[] { TfPluginBundle.message(TfPluginBundle.KEY_TFVC_NOT_CONFIGURED_DIALOG_OPEN_SETTINGS), TfPluginBundle.message(TfPluginBundle.KEY_TFVC_NOT_CONFIGURED_DIALOG_CANCEL)}, 0, getWarningIcon()); if (result == 0) { ShowSettingsUtil.getInstance().showSettingsDialog(project, TFSVcs.TFVC_NAME); } return false; } return true; } public static void runOnUIThread(final Runnable runnable) { runOnUIThread(runnable, false); } public static void runOnUIThread(final Runnable runnable, final boolean wait) { runOnUIThread(runnable, wait, ApplicationManager.getApplication() != null ? ApplicationManager.getApplication().getAnyModalityState() : null); } public static void runOnUIThread(final Runnable runnable, final boolean wait, final ModalityState modalityState) { Application application = ApplicationManager.getApplication(); if (application != null && !application.isDispatchThread() && !application.isUnitTestMode()) { if (wait) { application.invokeAndWait(runnable, modalityState); // TODO: use this instead after deprecating IDEA 15: TransactionGuard.getInstance().submitTransactionAndWait(runnable); } else { application.invokeLater(runnable, modalityState); // TODO: use this instead after deprecating IDEA 15: TransactionGuard.getInstance().submitTransaction(runnable); } } else { // If we are already on the dispatch thread then we can run it here // If we don't have an application or we are testing, just run the runnable here runnable.run(); } } /** * Shows a dialog with OK and cancel actions to prompt for confirmation from user * * @return true if user clicks on ok action and false if user clicks on cancel action */ public static boolean showConfirmationDialog(@NotNull final Project project, final String message, final String title, final Icon logo, final String okActionMessage, final String cancelActionMessage) { final int result = Messages.showYesNoDialog(project, message, title, okActionMessage, cancelActionMessage, logo); return result == 0 ? true : false; } /** * Shows an error dialog * * @param project * @param throwable */ public static void showErrorDialog(@NotNull final Project project, final Throwable throwable) { if (throwable != null && StringUtils.isNotEmpty(throwable.getMessage())) { showErrorDialog(project, throwable.getMessage()); } else { showErrorDialog(project, TfPluginBundle.message(TfPluginBundle.KEY_MESSAGE_TEAM_SERVICES_UNEXPECTED_ERROR)); } } /** * Shows an error dialog * * @param project * @param message */ public static void showErrorDialog(@NotNull final Project project, @NotNull final String message) { if (ApplicationManager.getApplication() == null) { // No application manager means no IntelliJ, we are probably in a test context return; } Messages.showErrorDialog(project, message, TfPluginBundle.message(TfPluginBundle.KEY_TITLE_TEAM_SERVICES_ERROR)); } /** * Finds the full path to a resource whether it's installed by the idea or being run inside the idea * * @param resourceUrl the URL for the resource * @param resourceName name of the resource * @param directory the directory under the idea.plugin/resource directory to for the resource * @return the path to the resource * @throws UnsupportedEncodingException */ public static String getResourcePath(final URL resourceUrl, final String resourceName, final String directory) throws UnsupportedEncodingException { // find location of the resource String resourcePath = resourceUrl.getPath(); resourcePath = URLDecoder.decode(resourcePath, CHARSET_UTF8); resourcePath = StringUtils.chomp(resourcePath, "/"); // When running the plugin inside of the idea for test, the path to the app needs to be // manipulated to look in a different location than where the resource resides in production. // For prod the url is .../../.IdeaIC15/config/plugins/com.microsoft.alm/lib but for test // the url is ../.IdeaIC15/system/plugins-sandbox/plugins/com.microsoft.alm.plugin.idea/classes if (resourcePath != null && resourcePath.endsWith(PROD_RESOURCES_SUB_PATH)) { return resourcePath + "/" + directory + "/" + resourceName; } else { return resourcePath + TEST_RESOURCES_SUB_PATH + directory + "/" + resourceName; } } /** * Check if a file is executable and if not sets it to be executable. When the plugin is unzipped, * the permissions of a file is not persisted so that is why a check is needed. * * @param executable executable file for application * @throws FileNotFoundException */ public static void setExecutablePermissions(final File executable) throws FileNotFoundException { if (!executable.exists()) { throw new FileNotFoundException(executable.getPath() + " not found while trying to set permissions."); } // set the executable to execute for all users if (!executable.canExecute()) { executable.setExecutable(true, false); } } /** * Find the project that is associated with the current open frame * Got this method from how Git gets the current project * * @return project based on repo */ public static Project getCurrentProject() { final IdeFrame frame = IdeFocusManager.getGlobalInstance().getLastFocusedFrame(); return frame == null || frame.getProject() == null ? ProjectManager.getInstance().getDefaultProject() : frame.getProject(); } /** * Checks if the IDE is Rider * * @return */ public static boolean isRider() { return StringUtils.equalsIgnoreCase(ApplicationNamesInfo.getInstance().getProductName(), RIDER_PRODUCT_NAME); } /** * Runs a task async in IntelliJ (shows no status so not for user actions that need to show progress) * * @param runnable */ public static void executeOnPooledThread(final Runnable runnable) { ApplicationManager.getApplication().executeOnPooledThread(runnable); } }