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