mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-15 18:02:58 +08:00
feat: terminal preview
This commit is contained in:
@@ -67,6 +67,8 @@ class TerminalDisplay(
|
||||
}
|
||||
|
||||
override fun paint(g: Graphics) {
|
||||
if (isShowing.not()) return
|
||||
|
||||
if (g is Graphics2D) {
|
||||
setupAntialiasing(g)
|
||||
clear(g)
|
||||
@@ -238,15 +240,27 @@ class TerminalDisplay(
|
||||
}
|
||||
|
||||
private fun drawCharacters(g: Graphics2D) {
|
||||
drawCharacters(
|
||||
g = g,
|
||||
verticalScrollOffset = terminal.getScrollingModel().getVerticalScrollOffset(),
|
||||
rows = terminal.getTerminalModel().getRows(),
|
||||
overflowBreak = false,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun drawCharacters(
|
||||
g: Graphics2D,
|
||||
verticalScrollOffset: Int,
|
||||
rows: Int,
|
||||
overflowBreak: Boolean,
|
||||
) {
|
||||
val terminalModel = terminal.getTerminalModel()
|
||||
val reverseVideo = terminalModel.getData(DataKey.ReverseVideo, false)
|
||||
val rows = terminalModel.getRows()
|
||||
val cols = terminalModel.getCols()
|
||||
val triple = Triple(Char.Space.toString(), TextStyle.Default, 1)
|
||||
val cursorPosition = terminal.getCursorModel().getPosition()
|
||||
val averageCharWidth = getAverageCharWidth()
|
||||
val maxVerticalScrollOffset = terminal.getScrollingModel().getMaxVerticalScrollOffset()
|
||||
val verticalScrollOffset = terminal.getScrollingModel().getVerticalScrollOffset()
|
||||
val selectionModel = terminal.getSelectionModel()
|
||||
val cursorStyle = terminalModel.getData(DataKey.CursorStyle)
|
||||
val showCursor = terminalModel.getData(DataKey.ShowCursor)
|
||||
@@ -255,11 +269,13 @@ class TerminalDisplay(
|
||||
val blink = terminalBlink.blink
|
||||
val cursorBlink = terminalBlink.cursorBlink
|
||||
val hasFocus = terminalModel.getData(TerminalPanel.Focused, false)
|
||||
|
||||
val lineCount = terminal.getDocument().getLineCount()
|
||||
|
||||
for (i in 1..rows) {
|
||||
var xOffset = 0
|
||||
val row = verticalScrollOffset + i - 1
|
||||
// 超出总行数则熔断,避免扩容情况出现
|
||||
if (overflowBreak && row >= lineCount) break
|
||||
val characters = smartCharacters(row).iterator()
|
||||
var j = 1
|
||||
while (j <= cols) {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package app.termora.terminal.panel
|
||||
|
||||
import app.termora.Disposable
|
||||
import app.termora.Disposer
|
||||
import app.termora.TerminalTab
|
||||
import app.termora.*
|
||||
import app.termora.actions.DataProvider
|
||||
import app.termora.actions.DataProviderSupport
|
||||
import app.termora.actions.DataProviders
|
||||
@@ -10,6 +8,7 @@ import app.termora.database.DatabaseManager
|
||||
import app.termora.plugin.internal.ssh.SSHTerminalTab
|
||||
import app.termora.terminal.*
|
||||
import app.termora.terminal.panel.vw.*
|
||||
import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
@@ -27,11 +26,11 @@ import java.text.AttributedCharacterIterator
|
||||
import java.text.AttributedString
|
||||
import java.text.BreakIterator
|
||||
import java.text.CharacterIterator
|
||||
import javax.swing.JLayeredPane
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@@ -47,6 +46,17 @@ class TerminalPanel(val tab: TerminalTab?, val terminal: Terminal, private val w
|
||||
val FocusMode = DataKey(Boolean::class)
|
||||
}
|
||||
|
||||
/**
|
||||
* 内边距
|
||||
*/
|
||||
var padding = Insets(4, 4, 4, 4)
|
||||
set(value) {
|
||||
field = value
|
||||
repaintImmediate()
|
||||
}
|
||||
|
||||
private val disposable = Disposer.newDisposable()
|
||||
private val myOwner get() = SwingUtilities.getWindowAncestor(this)
|
||||
private val properties get() = DatabaseManager.getInstance().properties
|
||||
private val terminalBlink = TerminalBlink(terminal)
|
||||
private val terminalFindPanel = TerminalFindPanel(this, terminal)
|
||||
@@ -54,6 +64,7 @@ class TerminalPanel(val tab: TerminalTab?, val terminal: Terminal, private val w
|
||||
private val terminalDisplay = TerminalDisplay(this, terminal, terminalBlink)
|
||||
private val layeredPane = TerminalLayeredPane()
|
||||
private var visualWindows = emptyArray<VisualWindow>()
|
||||
private val terminalPreviewDialog = TerminalPreviewDialog()
|
||||
|
||||
val scrollBar = TerminalScrollBar(this, terminalFindPanel, terminal)
|
||||
var enableFloatingToolbar = true
|
||||
@@ -95,14 +106,6 @@ class TerminalPanel(val tab: TerminalTab?, val terminal: Terminal, private val w
|
||||
*/
|
||||
var resizeToast = true
|
||||
|
||||
/**
|
||||
* 内边距
|
||||
*/
|
||||
var padding = Insets(4, 4, 4, 4)
|
||||
set(value) {
|
||||
field = value
|
||||
repaintImmediate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Toast 总开关
|
||||
@@ -166,6 +169,7 @@ class TerminalPanel(val tab: TerminalTab?, val terminal: Terminal, private val w
|
||||
override fun focusLost(e: FocusEvent) {
|
||||
terminal.getTerminalModel().setData(Focused, false)
|
||||
repaintImmediate()
|
||||
hidePreview()
|
||||
}
|
||||
|
||||
override fun focusGained(e: FocusEvent) {
|
||||
@@ -197,7 +201,11 @@ class TerminalPanel(val tab: TerminalTab?, val terminal: Terminal, private val w
|
||||
// 滚动相关
|
||||
this.addMouseWheelListener(object : MouseWheelListener {
|
||||
override fun mouseWheelMoved(e: MouseWheelEvent) {
|
||||
if (!terminal.getScrollingModel().canVerticalScroll()) {
|
||||
if (terminal.getScrollingModel().canVerticalScroll().not()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isShowingPreview()) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -236,6 +244,7 @@ class TerminalPanel(val tab: TerminalTab?, val terminal: Terminal, private val w
|
||||
// 监听悬浮工具栏变化,然后重新渲染
|
||||
floatingToolbar.addPropertyChangeListener { repaintImmediate() }
|
||||
|
||||
Disposer.register(this, disposable)
|
||||
}
|
||||
|
||||
private fun enableDropTarget() {
|
||||
@@ -416,6 +425,7 @@ class TerminalPanel(val tab: TerminalTab?, val terminal: Terminal, private val w
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
terminalPreviewDialog.dispose()
|
||||
Disposer.dispose(terminalBlink)
|
||||
Disposer.dispose(floatingToolbar)
|
||||
}
|
||||
@@ -494,6 +504,64 @@ class TerminalPanel(val tab: TerminalTab?, val terminal: Terminal, private val w
|
||||
super.paint(g)
|
||||
}
|
||||
|
||||
internal fun showPreview(location: Point) {
|
||||
if (terminal.getScrollingModel().canVerticalScroll().not()) return
|
||||
|
||||
val lineHeight = terminalDisplay.getLineHeight()
|
||||
val size = Dimension(
|
||||
terminalDisplay.width + min((terminalDisplay.width * 0.1).toInt(), 50) + (padding.left + padding.right),
|
||||
lineHeight * 10 + padding.top + padding.bottom * if (SystemInfo.isWindows_11_orLater) 2 else 1
|
||||
)
|
||||
val myLocation = Point(
|
||||
scrollBar.locationOnScreen.x - (scrollBar.width / 2) - size.width,
|
||||
location.y - lineHeight * 2
|
||||
)
|
||||
|
||||
// 如果超过了滚动条位置,那么使用最安全的大小
|
||||
if (abs(myLocation.x) + size.width > scrollBar.locationOnScreen.x) {
|
||||
size.width = terminalDisplay.width
|
||||
myLocation.x = scrollBar.locationOnScreen.x - (scrollBar.width / 2) - size.width
|
||||
}
|
||||
|
||||
// 如果超出了屏幕底部边界,那么修改弹窗位置
|
||||
val rectangle = getUsableDeviceBounds(terminalPreviewDialog.graphicsConfiguration)
|
||||
if (abs(myLocation.y) + size.height > rectangle.y + rectangle.height) {
|
||||
myLocation.y = location.y + lineHeight * 2 - size.height
|
||||
}
|
||||
|
||||
val point = Point(location)
|
||||
SwingUtilities.convertPointFromScreen(point, scrollBar)
|
||||
val scrollRatio = 1.0 * point.y / scrollBar.height
|
||||
val count = max(terminal.getDocument().getLineCount(), terminal.getTerminalModel().getRows())
|
||||
val totalHeight = count * lineHeight
|
||||
val scrollTop = scrollRatio * totalHeight
|
||||
val rowIndex = floor(scrollTop / lineHeight).toInt()
|
||||
|
||||
terminalPreviewDialog.row = rowIndex
|
||||
terminalPreviewDialog.size = size
|
||||
terminalPreviewDialog.location = myLocation
|
||||
terminalPreviewDialog.isVisible = true
|
||||
}
|
||||
|
||||
private fun getUsableDeviceBounds(gc: GraphicsConfiguration): Rectangle {
|
||||
val bounds = gc.bounds
|
||||
val insets = Toolkit.getDefaultToolkit().getScreenInsets(gc)
|
||||
|
||||
bounds.x += insets.left
|
||||
bounds.y += insets.top
|
||||
bounds.width -= (insets.left + insets.right)
|
||||
bounds.height -= (insets.top + insets.bottom)
|
||||
|
||||
return bounds
|
||||
}
|
||||
|
||||
|
||||
internal fun hidePreview() {
|
||||
terminalPreviewDialog.isVisible = false
|
||||
}
|
||||
|
||||
internal fun isShowingPreview() = terminalPreviewDialog.isVisible
|
||||
|
||||
private inner class TerminalLayeredPane : JLayeredPane() {
|
||||
override fun doLayout() {
|
||||
val averageCharWidth = getAverageCharWidth()
|
||||
@@ -556,6 +624,82 @@ class TerminalPanel(val tab: TerminalTab?, val terminal: Terminal, private val w
|
||||
}
|
||||
}
|
||||
|
||||
private inner class TerminalPreviewDialog : JDialog() {
|
||||
|
||||
private val terminalPreviewPanel = TerminalPreviewPanel()
|
||||
var row
|
||||
get() = terminalPreviewPanel.row
|
||||
set(value) {
|
||||
terminalPreviewPanel.row = value
|
||||
}
|
||||
|
||||
init {
|
||||
initView()
|
||||
}
|
||||
|
||||
override fun setVisible(b: Boolean) {
|
||||
if (b) terminalPreviewPanel.repaint()
|
||||
super.setVisible(b)
|
||||
}
|
||||
|
||||
|
||||
private fun initView() {
|
||||
isAlwaysOnTop = true
|
||||
focusableWindowState = false
|
||||
defaultCloseOperation = DISPOSE_ON_CLOSE
|
||||
|
||||
if (SystemInfo.isMacOS) {
|
||||
rootPane.putClientProperty("apple.awt.windowTitleVisible", false)
|
||||
rootPane.putClientProperty("apple.awt.fullWindowContent", true)
|
||||
rootPane.putClientProperty("apple.awt.transparentTitleBar", true)
|
||||
} else {
|
||||
rootPane.putClientProperty(FlatClientProperties.FULL_WINDOW_CONTENT, true)
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_HEIGHT, 0)
|
||||
}
|
||||
|
||||
val panel = JPanel(BorderLayout())
|
||||
panel.border = BorderFactory.createEmptyBorder(
|
||||
padding.top, padding.left,
|
||||
padding.bottom, padding.right
|
||||
)
|
||||
panel.add(terminalPreviewPanel, BorderLayout.CENTER)
|
||||
rootPane.contentPane = panel
|
||||
|
||||
|
||||
}
|
||||
|
||||
override fun addNotify() {
|
||||
super.addNotify()
|
||||
if (SystemInfo.isMacOS) {
|
||||
NativeMacLibrary.setControlsVisible(this, false)
|
||||
} else {
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_ICONIFFY, false)
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_MAXIMIZE, false)
|
||||
rootPane.putClientProperty(FlatClientProperties.TITLE_BAR_SHOW_CLOSE, false)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private inner class TerminalPreviewPanel : JComponent() {
|
||||
var row = 0
|
||||
|
||||
private val lineCount get() = terminal.getDocument().getLineCount()
|
||||
private val rows = 10
|
||||
override fun paint(g: Graphics) {
|
||||
if (g !is Graphics2D) return
|
||||
|
||||
|
||||
var row = this.row
|
||||
if (row + rows > lineCount) row = row - (row + rows - lineCount)
|
||||
|
||||
g.save()
|
||||
setupAntialiasing(g)
|
||||
terminalDisplay.drawCharacters(g, verticalScrollOffset = max(0, row), rows = rows, overflowBreak = true)
|
||||
g.restore()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : Any> getData(dataKey: DataKey<T>): T? {
|
||||
if (dataKey == DataProviders.TerminalTab) {
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
package app.termora.terminal.panel
|
||||
|
||||
import app.termora.swingCoroutineScope
|
||||
import app.termora.terminal.Terminal
|
||||
import app.termora.terminal.TerminalColor
|
||||
import com.formdev.flatlaf.ui.FlatScrollBarUI
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.swing.Swing
|
||||
import java.awt.Color
|
||||
import java.awt.Graphics
|
||||
import java.awt.Rectangle
|
||||
import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JScrollBar
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.plaf.ScrollBarUI
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class TerminalScrollBar(
|
||||
private val terminalPanel: TerminalPanel,
|
||||
@@ -19,15 +29,24 @@ class TerminalScrollBar(
|
||||
) : JScrollBar() {
|
||||
private val colorPalette get() = terminal.getTerminalModel().getColorPalette()
|
||||
private val myUI = MyScrollBarUI()
|
||||
private val owner get() = SwingUtilities.getWindowAncestor(this)
|
||||
|
||||
|
||||
init {
|
||||
setUI(myUI)
|
||||
initEvents()
|
||||
}
|
||||
|
||||
override fun setUI(ui: ScrollBarUI) {
|
||||
super.setUI(myUI)
|
||||
}
|
||||
|
||||
private fun initEvents() {
|
||||
val previewMouseAdapter = PreviewMouseAdapter()
|
||||
addMouseMotionListener(previewMouseAdapter)
|
||||
addMouseListener(previewMouseAdapter)
|
||||
}
|
||||
|
||||
private fun drawFindMap(g: Graphics, trackBounds: Rectangle) {
|
||||
if (!terminalPanel.findMap) return
|
||||
val kinds = terminalFindPanel.kinds
|
||||
@@ -53,13 +72,54 @@ class TerminalScrollBar(
|
||||
val y = max(ceil(trackBounds.height * n).toInt() - lineHeight, 0)
|
||||
g.fillRect(trackBounds.width - averageCharWidth, y, averageCharWidth, lineHeight)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private inner class MyScrollBarUI : FlatScrollBarUI() {
|
||||
override fun paintTrack(g: Graphics, c: JComponent, trackBounds: Rectangle) {
|
||||
super.paintTrack(g, c, trackBounds)
|
||||
drawFindMap(g, trackBounds)
|
||||
}
|
||||
|
||||
public override fun getThumbBounds(): Rectangle {
|
||||
return super.getThumbBounds()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class PreviewMouseAdapter : MouseAdapter() {
|
||||
private var job: Job? = null
|
||||
|
||||
override fun mouseMoved(e: MouseEvent) {
|
||||
if (terminal.getScrollingModel().canVerticalScroll().not()) {
|
||||
mouseExited(e)
|
||||
return
|
||||
}
|
||||
|
||||
if (myUI.thumbBounds.contains(e.point)) {
|
||||
mouseExited(e)
|
||||
return
|
||||
}
|
||||
|
||||
if (terminalPanel.isShowingPreview()) {
|
||||
doMouseMoved(e)
|
||||
} else {
|
||||
job?.cancel()
|
||||
job = swingCoroutineScope.launch(Dispatchers.Swing) {
|
||||
delay(250.milliseconds)
|
||||
doMouseMoved(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doMouseMoved(e: MouseEvent) {
|
||||
if (owner.isFocused.not()) return
|
||||
terminalPanel.showPreview(e.locationOnScreen)
|
||||
}
|
||||
|
||||
override fun mouseExited(e: MouseEvent) {
|
||||
job?.cancel()
|
||||
terminalPanel.hidePreview()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user