src/main/java/com/maddyhome/idea/vim/ui/ExOutputPanel.kt (250 lines of code) (raw):

/* * Copyright 2003-2023 The IdeaVim authors * * Use of this source code is governed by an MIT-style * license that can be found in the LICENSE.txt file or at * https://opensource.org/licenses/MIT. */ package com.maddyhome.idea.vim.ui import com.intellij.ide.ui.LafManager import com.intellij.ide.ui.LafManagerListener import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.editor.Editor import com.intellij.openapi.wm.impl.IdeBackgroundUtil import com.intellij.ui.ClientProperty import com.intellij.ui.components.JBPanel import com.intellij.ui.components.JBScrollPane import com.intellij.util.IJSwingUtilities import com.maddyhome.idea.vim.KeyHandler.Companion.getInstance import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.VimOutputPanel import com.maddyhome.idea.vim.api.globalOptions import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.diagnostic.VimLogger import com.maddyhome.idea.vim.helper.requestFocus import com.maddyhome.idea.vim.helper.selectEditorFont import com.maddyhome.idea.vim.helper.vimMorePanel import com.maddyhome.idea.vim.newapi.IjVimEditor import java.awt.BorderLayout import java.awt.Color import java.awt.LayoutManager import java.awt.event.ComponentAdapter import java.awt.event.ComponentEvent import java.awt.event.KeyAdapter import java.awt.event.KeyEvent import javax.swing.JComponent import javax.swing.JLabel import javax.swing.JScrollPane import javax.swing.JTextArea import javax.swing.KeyStroke import javax.swing.SwingUtilities import kotlin.math.ceil import kotlin.math.min /** * This panel displays text in a `more` like window. */ class ExOutputPanel private constructor(private val myEditor: Editor) : JBPanel<ExOutputPanel?>() { val myLabel: JLabel = JLabel("more") private val myText = JTextArea() private val myScrollPane: JScrollPane = JBScrollPane(myText, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER) private val myAdapter: ComponentAdapter private var myLineHeight = 0 private var myOldGlass: JComponent? = null private var myOldLayout: LayoutManager? = null private var myWasOpaque = false var myActive: Boolean = false init { // Create a text editor for the text and a label for the prompt val layout = BorderLayout(0, 0) setLayout(layout) add(myScrollPane, BorderLayout.CENTER) add(myLabel, BorderLayout.SOUTH) // Set the text area read only, and support wrap myText.isEditable = false myText.setLineWrap(true) myAdapter = object : ComponentAdapter() { override fun componentResized(e: ComponentEvent?) { positionPanel() } } // Setup some listeners to handle keystrokes val moreKeyListener = MoreKeyListener() addKeyListener(moreKeyListener) myText.addKeyListener(moreKeyListener) // Suppress the fancy frame background used in the Islands theme, which comes from a custom Graphics implementation // applied to the IdeRoot, and used to paint all children, including this panel. This client property is checked by // JBPanel.getComponentGraphics to give us the original Graphics, opting out of the fancy painting. ClientProperty.putRecursive<Boolean?>(this, IdeBackgroundUtil.NO_BACKGROUND, true) updateUI() } // Called automatically when the LAF is changed and the component is visible, and manually by the LAF listener handler override fun updateUI() { super.updateUI() setBorder(ExPanelBorder()) // Swing uses a bad pattern of calling updateUI() from the constructor. At this moment, all these variables are null @Suppress("SENSELESS_COMPARISON") if (myText != null && myLabel != null && myScrollPane != null) { setFontForElements() myText.setBorder(null) myScrollPane.setBorder(null) myLabel.setForeground(myText.getForeground()) // Make sure the panel is positioned correctly in case we're changing font size positionPanel() } } var text: String? get() = myText.getText() set(data) { var data: String = data!! if (!data.isEmpty() && data[data.length - 1] == '\n') { data = data.dropLast(1) } myText.text = data myText.setFont(selectEditorFont(myEditor, data)) myText.setCaretPosition(0) if (!data.isEmpty()) { activate() } } override fun getForeground(): Color? { @Suppress("SENSELESS_COMPARISON") if (myText == null) { // Swing uses a bad pattern of calling getForeground() from the constructor. At this moment, `myText` is null. return super.getForeground() } return myText.getForeground() } override fun getBackground(): Color? { @Suppress("SENSELESS_COMPARISON") if (myText == null) { // Swing uses a bad pattern of calling getBackground() from the constructor. At this moment, `myText` is null. return super.getBackground() } return myText.getBackground() } /** * Turns off the ex entry field and optionally puts the focus back to the original component */ fun deactivate(refocusOwningEditor: Boolean) { if (!myActive) return myActive = false myText.text = "" if (refocusOwningEditor) { requestFocus(myEditor.contentComponent) } if (myOldGlass != null) { myOldGlass!!.removeComponentListener(myAdapter) myOldGlass!!.isVisible = false myOldGlass!!.remove(this) myOldGlass!!.setOpaque(myWasOpaque) myOldGlass!!.setLayout(myOldLayout) } } /** * Turns on the more window for the given editor */ fun activate() { val root = SwingUtilities.getRootPane(myEditor.contentComponent) myOldGlass = root.getGlassPane() as JComponent? if (myOldGlass != null) { myOldLayout = myOldGlass!!.layout myWasOpaque = myOldGlass!!.isOpaque myOldGlass!!.setLayout(null) myOldGlass!!.setOpaque(false) myOldGlass!!.add(this) myOldGlass!!.addComponentListener(myAdapter) } setFontForElements() positionPanel() if (myOldGlass != null) { myOldGlass!!.isVisible = true } myActive = true requestFocus(myText) } private fun setFontForElements() { myText.setFont(selectEditorFont(myEditor, myText.getText())) myLabel.setFont(selectEditorFont(myEditor, myLabel.text)) } fun scrollLine() { scrollOffset(myLineHeight) } fun scrollPage() { scrollOffset(myScrollPane.getVerticalScrollBar().visibleAmount) } fun scrollHalfPage() { val sa = myScrollPane.getVerticalScrollBar().visibleAmount / 2.0 val offset = ceil(sa / myLineHeight) * myLineHeight scrollOffset(offset.toInt()) } fun onBadKey() { myLabel.setText(injector.messages.message("message.ex.output.more.prompt.full")) myLabel.setFont(selectEditorFont(myEditor, myLabel.text)) } private fun scrollOffset(more: Int) { val `val` = myScrollPane.getVerticalScrollBar().value myScrollPane.getVerticalScrollBar().setValue(`val` + more) myScrollPane.getHorizontalScrollBar().setValue(0) if (`val` + more >= myScrollPane.getVerticalScrollBar().maximum - myScrollPane.getVerticalScrollBar().visibleAmount ) { myLabel.setText(injector.messages.message("message.ex.output.end.prompt")) } else { myLabel.setText(injector.messages.message("message.ex.output.more.prompt")) } myLabel.setFont(selectEditorFont(myEditor, myLabel.text)) } val isAtEnd: Boolean get() { val `val` = myScrollPane.getVerticalScrollBar().value return `val` >= myScrollPane.getVerticalScrollBar().maximum - myScrollPane.getVerticalScrollBar().visibleAmount } private fun positionPanel() { val contentComponent = myEditor.contentComponent val scroll = SwingUtilities.getAncestorOfClass(JScrollPane::class.java, contentComponent) val rootPane = SwingUtilities.getRootPane(contentComponent) if (scroll == null || rootPane == null) { // These might be null if we're invoked during component initialisation and before it's been added to the tree return } size = scroll.size myLineHeight = myText.getFontMetrics(myText.getFont()).height val count: Int = countLines(myText.getText()) val visLines = size.height / myLineHeight - 1 val lines = min(count, visLines) setSize( size.width, lines * myLineHeight + myLabel.getPreferredSize().height + border.getBorderInsets(this).top * 2 ) val height = size.height val bounds = scroll.bounds bounds.translate(0, scroll.getHeight() - height) bounds.height = height val pos = SwingUtilities.convertPoint(scroll.getParent(), bounds.location, rootPane.getGlassPane()) bounds.location = pos setBounds(bounds) myScrollPane.getVerticalScrollBar().setValue(0) if (!injector.globalOptions().more) { // FIX scrollOffset(100000) } else { scrollOffset(0) } } @JvmOverloads fun close(key: KeyStroke? = null) { ApplicationManager.getApplication().invokeLater { deactivate(true) val project = myEditor.project if (project != null && key != null && key.keyChar != '\n') { val keys: MutableList<KeyStroke> = ArrayList(1) keys.add(key) if (LOG.isTrace()) { LOG.trace( "Adding new keys to keyStack as part of playback. State before adding keys: " + getInstance().keyStack.dump() ) } getInstance().keyStack.addKeys(keys) val context: ExecutionContext = injector.executionContextManager.getEditorExecutionContext(IjVimEditor(myEditor)) VimPlugin.getMacro().playbackKeys(IjVimEditor(myEditor), context, 1) } } } private class MoreKeyListener : KeyAdapter() { /** * Invoked when a key has been pressed. */ override fun keyTyped(e: KeyEvent) { val currentPanel: VimOutputPanel = injector.outputPanel.getCurrentOutputPanel() ?: return val keyCode = e.getKeyCode() val keyChar = e.getKeyChar() val modifiers = e.modifiersEx val keyStroke = if (keyChar == KeyEvent.CHAR_UNDEFINED) KeyStroke.getKeyStroke(keyCode, modifiers) else KeyStroke.getKeyStroke(keyChar, modifiers) currentPanel.handleKey(keyStroke) } } class LafListener : LafManagerListener { override fun lookAndFeelChanged(source: LafManager) { if (VimPlugin.isNotEnabled()) return // This listener is only invoked for local scenarios, and we only need to update local editor UI. This will invoke // updateUI on the output pane and it's child components for (vimEditor in injector.editorGroup.getEditors()) { val editor = (vimEditor as IjVimEditor).editor if (!isPanelActive(editor)) continue IJSwingUtilities.updateComponentTreeUI(getInstance(editor)) } } } companion object { private val LOG: VimLogger = injector.getLogger<ExOutputPanel>(ExOutputPanel::class.java) fun getNullablePanel(editor: Editor): ExOutputPanel? { return editor.vimMorePanel } fun isPanelActive(editor: Editor): Boolean { return getNullablePanel(editor) != null } fun getInstance(editor: Editor): ExOutputPanel { var panel: ExOutputPanel? = getNullablePanel(editor) if (panel == null) { panel = ExOutputPanel(editor) editor.vimMorePanel = panel } return panel } private fun countLines(text: String): Int { if (text.isEmpty()) { return 0 } var count = 0 var pos = -1 while ((text.indexOf('\n', pos + 1).also { pos = it }) != -1) { count++ } if (text[text.length - 1] != '\n') { count++ } return count } } }