diff-preview: move escape handler setup to file editor constructor
[idea/community.git] / platform / vcs-impl / src / com / intellij / openapi / vcs / changes / EditorTabPreview.kt
1 // Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.openapi.vcs.changes
3
4 import com.intellij.diff.chains.DiffRequestChain
5 import com.intellij.diff.chains.SimpleDiffRequestChain
6 import com.intellij.diff.editor.DiffVirtualFile
7 import com.intellij.diff.impl.DiffRequestProcessor
8 import com.intellij.diff.util.DiffUserDataKeysEx
9 import com.intellij.ide.actions.SplitAction
10 import com.intellij.openapi.Disposable
11 import com.intellij.openapi.ListSelection
12 import com.intellij.openapi.actionSystem.ActionManager
13 import com.intellij.openapi.actionSystem.AnActionEvent
14 import com.intellij.openapi.actionSystem.IdeActions
15 import com.intellij.openapi.fileEditor.FileEditor
16 import com.intellij.openapi.fileEditor.FileEditorManager
17 import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
18 import com.intellij.openapi.project.DumbAwareAction
19 import com.intellij.openapi.project.DumbService
20 import com.intellij.openapi.project.Project
21 import com.intellij.openapi.util.Disposer
22 import com.intellij.openapi.util.Disposer.isDisposed
23 import com.intellij.openapi.vcs.changes.ui.ChangesTree
24 import com.intellij.openapi.vfs.VirtualFile
25 import com.intellij.openapi.wm.ToolWindowManager
26 import com.intellij.util.EditSourceOnDoubleClickHandler.isToggleEvent
27 import com.intellij.util.IJSwingUtilities
28 import com.intellij.util.Processor
29 import com.intellij.util.ui.update.DisposableUpdate
30 import com.intellij.util.ui.update.MergingUpdateQueue
31 import org.jetbrains.annotations.Nls
32 import java.awt.event.KeyEvent
33 import java.awt.event.MouseEvent
34 import javax.swing.JComponent
35 import kotlin.streams.toList
36
37 abstract class EditorTabPreview(protected val diffProcessor: DiffRequestProcessor) : DiffPreview {
38   protected val project get() = diffProcessor.project!!
39   private val previewFile = EditorTabDiffPreviewVirtualFile(this)
40   private val updatePreviewQueue =
41     MergingUpdateQueue("updatePreviewQueue", 100, true, null, diffProcessor).apply {
42       setRestartTimerOnAdd(true)
43     }
44   private val updatePreviewProcessor: DiffPreviewUpdateProcessor? get() = diffProcessor as? DiffPreviewUpdateProcessor
45
46   var escapeHandler: Runnable? = null
47
48   fun openWithDoubleClick(tree: ChangesTree) {
49     installDoubleClickHandler(tree)
50     installEnterKeyHandler(tree)
51     installSelectionChangedHandler(tree) { updatePreview(false) }
52   }
53
54   fun openWithSingleClick(tree: ChangesTree) {
55     //do not open file aggressively on start up, do it later
56     DumbService.getInstance(project).smartInvokeLater {
57       if (isDisposed(updatePreviewQueue)) return@smartInvokeLater
58
59       installSelectionChangedHandler(tree) {
60         if (!openPreview(false)) closePreview() // auto-close editor tab if nothing to preview
61       }
62     }
63   }
64
65   fun installNextDiffActionOn(component: JComponent) {
66     DumbAwareAction.create { openPreview(true) }.apply {
67       copyShortcutFrom(ActionManager.getInstance().getAction(IdeActions.ACTION_NEXT_DIFF))
68       registerCustomShortcutSet(component, diffProcessor)
69     }
70   }
71
72   protected open fun isPreviewOnDoubleClickAllowed(): Boolean = true
73   protected open fun isPreviewOnEnterAllowed(): Boolean = true
74
75   private fun installDoubleClickHandler(tree: ChangesTree) {
76     val oldDoubleClickHandler = tree.doubleClickHandler
77     val newDoubleClickHandler = Processor<MouseEvent> { e ->
78       if (isToggleEvent(tree, e)) return@Processor false
79
80       isPreviewOnDoubleClickAllowed() && openPreview(true) || oldDoubleClickHandler?.process(e) == true
81     }
82
83     tree.doubleClickHandler = newDoubleClickHandler
84     Disposer.register(diffProcessor, Disposable { tree.doubleClickHandler = oldDoubleClickHandler })
85   }
86
87   private fun installEnterKeyHandler(tree: ChangesTree) {
88     val oldEnterKeyHandler = tree.enterKeyHandler
89     val newEnterKeyHandler = Processor<KeyEvent> { e ->
90       isPreviewOnEnterAllowed() && openPreview(false) || oldEnterKeyHandler?.process(e) == true
91     }
92
93     tree.enterKeyHandler = newEnterKeyHandler
94     Disposer.register(diffProcessor, Disposable { tree.enterKeyHandler = oldEnterKeyHandler })
95   }
96
97   private fun installSelectionChangedHandler(tree: ChangesTree, handler: () -> Unit) =
98     tree.addSelectionListener(
99       Runnable {
100         updatePreviewQueue.queue(DisposableUpdate.createDisposable(updatePreviewQueue, this) {
101           if (!skipPreviewUpdate()) handler()
102         })
103       },
104       updatePreviewQueue
105     )
106
107   protected abstract fun getCurrentName(): String?
108
109   protected abstract fun hasContent(): Boolean
110
111   protected open fun skipPreviewUpdate(): Boolean = ToolWindowManager.getInstance(project).isEditorComponentActive
112
113   override fun updatePreview(fromModelRefresh: Boolean) {
114     if (isPreviewOpen()) {
115       updatePreviewProcessor?.refresh(false)
116       FileEditorManagerEx.getInstanceEx(project).updateFilePresentation(previewFile)
117     }
118     else {
119       updatePreviewProcessor?.clear()
120     }
121   }
122
123   override fun setPreviewVisible(isPreviewVisible: Boolean, focus: Boolean) {
124     if (isPreviewVisible) openPreview(focus) else closePreview()
125   }
126
127   private fun isPreviewOpen(): Boolean = FileEditorManager.getInstance(project).isFileOpen(previewFile)
128
129   fun closePreview() {
130     FileEditorManager.getInstance(project).closeFile(previewFile)
131     updatePreviewProcessor?.clear()
132   }
133
134   fun openPreview(focusEditor: Boolean): Boolean {
135     updatePreviewProcessor?.refresh(false)
136     if (!hasContent()) return false
137
138     escapeHandler?.let { handler -> registerEscapeHandler(previewFile, handler) }
139
140     openPreview(project, previewFile, focusEditor)
141
142     return true
143   }
144
145   private class EditorTabDiffPreviewVirtualFile(val preview: EditorTabPreview)
146     : PreviewDiffVirtualFile(EditorTabDiffPreviewProvider(preview.diffProcessor) { preview.getCurrentName() }) {
147     init {
148       // EditorTabDiffPreviewProvider does not create new processor, so general assumptions of DiffVirtualFile are violated
149       preview.diffProcessor.putContextUserData(DiffUserDataKeysEx.DIFF_IN_EDITOR_WITH_EXPLICIT_DISPOSABLE, true)
150       putUserData(SplitAction.FORBID_TAB_SPLIT, true)
151     }
152   }
153
154   companion object {
155     fun openPreview(project: Project, file: PreviewDiffVirtualFile, focusEditor: Boolean): Array<out FileEditor> {
156       return VcsEditorTabFilesManager.getInstance().openFile(project, file, focusEditor)
157     }
158
159     fun registerEscapeHandler(file: VirtualFile, handler: Runnable) {
160       file.putUserData(DiffVirtualFile.ESCAPE_HANDLER, EditorTabPreviewEscapeAction(handler))
161     }
162   }
163 }
164
165 internal class EditorTabPreviewEscapeAction(private val escapeHandler: Runnable) : DumbAwareAction() {
166   override fun actionPerformed(e: AnActionEvent) = escapeHandler.run()
167 }
168
169 private class EditorTabDiffPreviewProvider(
170   private val diffProcessor: DiffRequestProcessor,
171   private val tabNameProvider: () -> String?
172 ) : ChainBackedDiffPreviewProvider {
173   override fun createDiffRequestProcessor(): DiffRequestProcessor {
174     IJSwingUtilities.updateComponentTreeUI(diffProcessor.component)
175     return diffProcessor
176   }
177
178   override fun getOwner(): Any = this
179
180   override fun getEditorTabName(): @Nls String = tabNameProvider().orEmpty()
181
182   override fun createDiffRequestChain(): DiffRequestChain? {
183     if (diffProcessor is ChangeViewDiffRequestProcessor) {
184       val selection = ListSelection.create(diffProcessor.allChanges.toList(), diffProcessor.currentChange)
185       val producers = selection.map { it!!.createProducer(diffProcessor.project) }
186       val chain = SimpleDiffRequestChain.fromProducers(producers.list)
187       chain.index = producers.selectedIndex
188       return chain
189     }
190     return null
191   }
192 }