diff --git a/images/resources/messages/ImagesBundle.properties b/images/resources/messages/ImagesBundle.properties index 3c33cb1d8e7a7..8d927ef7fc519 100644 --- a/images/resources/messages/ImagesBundle.properties +++ b/images/resources/messages/ImagesBundle.properties @@ -1,4 +1,5 @@ error.title.launching.external.editor=Problem Launching External Executable +error.cannot.edit.image.external.editor=Can't edit image in external editor select.external.executable.title=Select Editor select.external.executable.message=Select external graphics editor diff --git a/images/src/org/intellij/images/actions/EditExternallyAction.kt b/images/src/org/intellij/images/actions/EditExternallyAction.kt index 528e869d65bce..5a7e11c5d157a 100644 --- a/images/src/org/intellij/images/actions/EditExternallyAction.kt +++ b/images/src/org/intellij/images/actions/EditExternallyAction.kt @@ -5,22 +5,37 @@ import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.util.ExecUtil import com.intellij.ide.util.PropertiesComponent +import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.PlatformCoreDataKeys +import com.intellij.openapi.application.EDT +import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.ui.Messages +import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VfsUtilCore +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.toNioPathOrNull +import com.intellij.ui.UIBundle import com.intellij.util.EnvironmentUtil +import com.intellij.util.io.delete +import kotlinx.coroutines.* import org.intellij.images.ImagesBundle import org.intellij.images.fileTypes.ImageFileTypeManager import org.intellij.images.options.impl.ImagesConfigurable +import org.intellij.images.ui.ImageComponentDecorator import java.awt.Desktop import java.io.File import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption.REPLACE_EXISTING /** * Open image file externally. @@ -31,6 +46,29 @@ import java.io.IOException internal class EditExternallyAction : DumbAwareAction() { override fun actionPerformed(e: AnActionEvent) { val imageFile = e.getData(CommonDataKeys.VIRTUAL_FILE) ?: return + if (imageFile.toNioPathOrNull() != null) { + actionPerformed(e, imageFile) + } + else { + performActionWithBackingFile(e, imageFile) + } + } + + override fun update(e: AnActionEvent) { + val file = e.getData(CommonDataKeys.VIRTUAL_FILE) + val enabled = file != null && ImageFileTypeManager.getInstance().isImage(file) + if (e.isFromContextMenu) { + e.presentation.isVisible = enabled + } + + e.presentation.isEnabled = enabled + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.BGT + } + + private fun actionPerformed(e: AnActionEvent, imageFile: VirtualFile) { var executablePath = PropertiesComponent.getInstance().getValue(EditExternalImageEditorAction.EXT_PATH_KEY, "") if (!StringUtil.isEmpty(executablePath)) { EnvironmentUtil.getEnvironmentMap().forEach { (varName, varValue) -> @@ -75,17 +113,78 @@ internal class EditExternallyAction : DumbAwareAction() { } } - override fun update(e: AnActionEvent) { - val file = e.getData(CommonDataKeys.VIRTUAL_FILE) - val enabled = file != null && ImageFileTypeManager.getInstance().isImage(file) - if (e.isFromContextMenu) { - e.presentation.isVisible = enabled + // Try to create a temporary backing file for the external editor to use + private fun performActionWithBackingFile(e: AnActionEvent, imageFile: VirtualFile) { + try { + val disposable = e.getDisposable() + disposable.launch { + try { + val backingFile = imageFile.copyToBackingFile(disposable) + actionPerformed(e, backingFile) + } + catch (e: IllegalStateException) { + thisLogger().warn("Failed to open external image editor", e) + withContext(Dispatchers.EDT) { + Messages.showErrorDialog(ImagesBundle.message("error.cannot.edit.image.external.editor"), UIBundle.message("error.dialog.title")) + } + } + } } + catch (e: IllegalStateException) { + thisLogger().warn("Failed to open external image editor", e) + Messages.showErrorDialog(ImagesBundle.message("error.cannot.edit.image.external.editor"), UIBundle.message("error.dialog.title")) + } + } +} - e.presentation.isEnabled = enabled +private fun VirtualFile.copyToBackingFile(disposable: Disposable): VirtualFile { + val filePath = Files.createTempFile("EditExternallyAction", name) + Disposer.register(disposable, Disposable { + filePath.safeDelete() + }) + inputStream.use { inputStream -> + try { + Files.copy(inputStream, filePath, REPLACE_EXISTING) + } + catch (e: IOException) { + filePath.safeDelete() + throw IllegalStateException("Failed to create backing file", e) + } } + return LocalFileSystem.getInstance().refreshAndFindFileByNioFile(filePath) ?: throw IllegalStateException("Failed to create virtual file") +} - override fun getActionUpdateThread(): ActionUpdateThread { - return ActionUpdateThread.BGT +private fun Path.safeDelete() { + try { + delete() + } + catch (ignore: IOException) { + } +} + +private fun Disposable.launch(block: suspend CoroutineScope.() -> Unit) { + val job = SupervisorJob() + @Suppress("SSBasedInspection") + CoroutineScope(job + Dispatchers.IO).launch { + block() + } + Disposer.register(this, Disposable { + job.cancel() + }) +} + +/** + * Tries to get a Disposable from the event + * + * If `ImageComponentDecorator.DATA_KEY` exists and is a `Disposable`, use it (ImageEditorUI returns a + * [org.intellij.images.editor.ImageEditor] which is a `Disposable`) + * Otherwise, use PlatformCoreDataKeys.FILE_EDITOR. + */ +private fun AnActionEvent.getDisposable(): Disposable { + val data = getData(ImageComponentDecorator.DATA_KEY) + if (data is Disposable) { + return data } -} \ No newline at end of file + return getData(PlatformCoreDataKeys.FILE_EDITOR) + ?: throw IllegalStateException("Component does not provide a Disposable object") +}