feat: Floating Toolbar (#231)

This commit is contained in:
hstyi
2025-02-14 20:38:46 +08:00
committed by GitHub
parent 4e12c32566
commit a25b97614f
16 changed files with 319 additions and 8 deletions

View File

@@ -18,9 +18,6 @@ import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import java.io.File
import java.util.*
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.set
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import kotlin.time.Duration.Companion.minutes
@@ -475,6 +472,11 @@ class Database private constructor(private val env: Environment) : Disposable {
* 终端断开连接时自动关闭Tab
*/
var autoCloseTabWhenDisconnected by BooleanPropertyDelegate(false)
/**
* 是否显示悬浮工具栏
*/
var floatingToolbar by BooleanPropertyDelegate(true)
}
/**

View File

@@ -3,6 +3,8 @@ package app.termora
object Icons {
val bulletList by lazy { DynamicIcon("icons/bulletList.svg", "icons/bulletList_dark.svg") }
val up by lazy { DynamicIcon("icons/up.svg", "icons/up_dark.svg") }
val closeSmall by lazy { DynamicIcon("icons/closeSmall.svg", "icons/closeSmall_dark.svg") }
val closeSmallHovered by lazy { DynamicIcon("icons/closeSmallHovered.svg", "icons/closeSmallHovered_dark.svg") }
val plugin by lazy { DynamicIcon("icons/plugin.svg", "icons/plugin_dark.svg") }
val moveUp by lazy { DynamicIcon("icons/moveUp.svg", "icons/moveUp_dark.svg") }
val down by lazy { DynamicIcon("icons/down.svg", "icons/down_dark.svg") }

View File

@@ -5,6 +5,7 @@ import app.termora.actions.ActionManager
import app.termora.terminal.Terminal
import app.termora.terminal.TerminalColor
import app.termora.terminal.TextStyle
import app.termora.terminal.panel.FloatingToolbarPanel
import app.termora.terminal.panel.TerminalDisplay
import app.termora.terminal.panel.TerminalPaintListener
import app.termora.terminal.panel.TerminalPanel
@@ -32,13 +33,25 @@ class MultipleTerminalListener : TerminalPaintListener {
// 正在搜索那么需要下移
val finding = terminal.getTerminalModel().getData(TerminalPanel.Finding, false)
// 如果悬浮窗正在显示,那么需要下移
val floatingToolBar = terminalPanel.getData(FloatingToolbarPanel.FloatingToolbar)?.isVisible == true
var y = g.fontMetrics.ascent
if (finding) {
y += g.fontMetrics.height + g.fontMetrics.ascent / 2
}
if (floatingToolBar) {
y += g.fontMetrics.height + g.fontMetrics.ascent / 2
}
g.font = font
g.color = Color(colorPalette.getColor(TerminalColor.Normal.RED))
g.drawString(
text,
terminalDisplay.width - width - terminalPanel.getAverageCharWidth() / 2,
g.fontMetrics.ascent + if (finding)
g.fontMetrics.height + g.fontMetrics.ascent / 2 else 0
y
)
g.font = oldFont
}

View File

@@ -1,7 +1,11 @@
package app.termora
import app.termora.actions.AnActionEvent
import app.termora.terminal.panel.FloatingToolbarPanel
import org.apache.commons.lang3.StringUtils
import java.beans.PropertyChangeEvent
import java.beans.PropertyChangeListener
import java.util.*
abstract class PropertyTerminalTab : TerminalTab {
protected val listeners = mutableListOf<PropertyChangeListener>()
@@ -26,6 +30,10 @@ abstract class PropertyTerminalTab : TerminalTab {
override fun onLostFocus() {
hasFocus = false
// 切换标签时,尝试隐藏悬浮工具栏
val evt = AnActionEvent(getJComponent(), StringUtils.EMPTY, EventObject(getJComponent()))
evt.getData(FloatingToolbarPanel.FloatingToolbar)?.triggerHide()
}

View File

@@ -20,6 +20,7 @@ import app.termora.sync.SyncType
import app.termora.sync.SyncerProvider
import app.termora.terminal.CursorStyle
import app.termora.terminal.DataKey
import app.termora.terminal.panel.FloatingToolbarPanel
import app.termora.terminal.panel.TerminalPanel
import cash.z.ecc.android.bip39.Mnemonics
import com.formdev.flatlaf.FlatClientProperties
@@ -310,6 +311,7 @@ class SettingsOptionsPane : OptionsPane() {
private val terminalSetting get() = Database.getDatabase().terminal
private val selectCopyComboBox = YesOrNoComboBox()
private val autoCloseTabComboBox = YesOrNoComboBox()
private val floatingToolbarComboBox = YesOrNoComboBox()
init {
initView()
@@ -332,6 +334,19 @@ class SettingsOptionsPane : OptionsPane() {
}
autoCloseTabComboBox.toolTipText = I18n.getString("termora.settings.terminal.auto-close-tab-description")
floatingToolbarComboBox.addItemListener { e ->
if (e.stateChange == ItemEvent.SELECTED) {
terminalSetting.floatingToolbar = floatingToolbarComboBox.selectedItem as Boolean
TerminalPanelFactory.getAllTerminalPanel().forEach { tp ->
if (terminalSetting.floatingToolbar && FloatingToolbarPanel.isPined) {
tp.getData(FloatingToolbarPanel.FloatingToolbar)?.triggerShow()
} else {
tp.getData(FloatingToolbarPanel.FloatingToolbar)?.triggerHide()
}
}
}
}
selectCopyComboBox.addItemListener { e ->
if (e.stateChange == ItemEvent.SELECTED) {
terminalSetting.selectCopy = selectCopyComboBox.selectedItem as Boolean
@@ -477,6 +492,7 @@ class SettingsOptionsPane : OptionsPane() {
cursorStyleComboBox.selectedItem = terminalSetting.cursor
selectCopyComboBox.selectedItem = terminalSetting.selectCopy
autoCloseTabComboBox.selectedItem = terminalSetting.autoCloseTabWhenDisconnected
floatingToolbarComboBox.selectedItem = terminalSetting.floatingToolbar
}
override fun getIcon(isSelected: Boolean): Icon {
@@ -494,7 +510,7 @@ class SettingsOptionsPane : OptionsPane() {
private fun getCenterComponent(): JComponent {
val layout = FormLayout(
"left:pref, $formMargin, default:grow, $formMargin, left:pref, $formMargin, pref, default:grow",
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
"pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref, $formMargin, pref"
)
val beepBtn = JButton(Icons.run)
@@ -521,6 +537,8 @@ class SettingsOptionsPane : OptionsPane() {
.add(selectCopyComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.settings.terminal.cursor-style")}:").xy(1, rows)
.add(cursorStyleComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.settings.terminal.floating-toolbar")}:").xy(1, rows)
.add(floatingToolbarComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.settings.terminal.auto-close-tab")}:").xy(1, rows)
.add(autoCloseTabComboBox).xy(3, rows).apply { rows += step }
.add("${I18n.getString("termora.settings.terminal.local-shell")}:").xy(1, rows)

View File

@@ -16,6 +16,12 @@ class TerminalPanelFactory {
fun getInstance(scope: Scope): TerminalPanelFactory {
return scope.getOrCreate(TerminalPanelFactory::class) { TerminalPanelFactory() }
}
fun getAllTerminalPanel(): List<TerminalPanel> {
return ApplicationScope.forApplicationScope().windowScopes()
.map { getInstance(it) }
.flatMap { it.getTerminalPanels() }
}
}
fun createTerminalPanel(terminal: Terminal, ptyConnector: PtyConnector): TerminalPanel {

View File

@@ -0,0 +1,156 @@
package app.termora.terminal.panel
import app.termora.*
import app.termora.actions.AnAction
import app.termora.actions.AnActionEvent
import app.termora.actions.DataProviders
import app.termora.terminal.DataKey
import com.formdev.flatlaf.extras.components.FlatToolBar
import com.formdev.flatlaf.ui.FlatRoundBorder
import org.apache.commons.lang3.StringUtils
import java.awt.event.ActionListener
import javax.swing.JButton
class FloatingToolbarPanel : FlatToolBar(), Disposable {
private val floatingToolbarEnable get() = Database.getDatabase().terminal.floatingToolbar
private var closed = false
companion object {
val FloatingToolbar = DataKey(FloatingToolbarPanel::class)
val isPined get() = pinAction.isSelected
private val pinAction by lazy {
object : AnAction() {
private val properties get() = Database.getDatabase().properties
private val key = "FloatingToolbar.pined"
init {
setStateAction()
isSelected = properties.getString(key, StringUtils.EMPTY).toBoolean()
}
override fun actionPerformed(evt: AnActionEvent) {
isSelected = !isSelected
properties.putString(key, isSelected.toString())
actionListeners.forEach { it.actionPerformed(evt) }
if (isSelected) {
TerminalPanelFactory.getAllTerminalPanel().forEach {
it.getData(FloatingToolbar)?.triggerShow()
}
} else {
// 触发者的不隐藏
val c = evt.getData(FloatingToolbar)
TerminalPanelFactory.getAllTerminalPanel().forEach {
val e = it.getData(FloatingToolbar)
if (c != e) {
e?.triggerHide()
}
}
}
}
}
}
}
init {
border = FlatRoundBorder()
isOpaque = false
isFocusable = false
isFloatable = false
isVisible = false
if (floatingToolbarEnable) {
if (pinAction.isSelected) {
isVisible = true
}
}
initActions()
}
fun triggerShow() {
if (!floatingToolbarEnable || closed) {
return
}
if (isVisible == false) {
isVisible = true
firePropertyChange("visible", false, true)
}
}
fun triggerHide() {
if (floatingToolbarEnable && !closed) {
if (pinAction.isSelected) {
return
}
}
if (isVisible == true) {
isVisible = false
firePropertyChange("visible", true, false)
}
}
private fun initActions() {
// Pin
add(initPinActionButton())
// 重连
add(initReconnectActionButton())
// 关闭
add(initCloseActionButton())
}
private fun initPinActionButton(): JButton {
val btn = JButton(Icons.pin)
btn.isSelected = pinAction.isSelected
val actionListener = ActionListener { btn.isSelected = pinAction.isSelected }
pinAction.addActionListener(actionListener)
btn.addActionListener(pinAction)
Disposer.register(this, object : Disposable {
override fun dispose() {
btn.removeActionListener(pinAction)
pinAction.removeActionListener(actionListener)
}
})
return btn
}
private fun initCloseActionButton(): JButton {
val btn = JButton(Icons.closeSmall)
btn.pressedIcon = Icons.closeSmallHovered
btn.rolloverIcon = Icons.closeSmallHovered
btn.addActionListener {
closed = true
triggerHide()
}
return btn
}
private fun initReconnectActionButton(): JButton {
val btn = JButton(Icons.refresh)
btn.toolTipText = I18n.getString("termora.tabbed.contextmenu.reconnect")
btn.addActionListener(object : AnAction() {
override fun actionPerformed(evt: AnActionEvent) {
val tab = evt.getData(DataProviders.TerminalTab) ?: return
if (tab.canReconnect()) {
tab.reconnect()
}
}
})
return btn
}
override fun dispose() {
}
}

View File

@@ -1,6 +1,7 @@
package app.termora.terminal.panel
import app.termora.Disposable
import app.termora.Disposer
import app.termora.actions.DataProvider
import app.termora.actions.DataProviderSupport
import app.termora.actions.DataProviders
@@ -40,10 +41,12 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
}
private val terminalFindPanel = TerminalFindPanel(this, terminal)
private val floatingToolbar = FloatingToolbarPanel()
private val terminalDisplay = TerminalDisplay(this, terminal)
val scrollBar = TerminalScrollBar(this@TerminalPanel, terminalFindPanel, terminal)
private val dataProviderSupport = DataProviderSupport()
val scrollBar = TerminalScrollBar(this@TerminalPanel, terminalFindPanel, terminal)
/**
* 键盘事件
@@ -117,6 +120,7 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
val layeredPane = TerminalLayeredPane()
layeredPane.add(terminalDisplay, JLayeredPane.DEFAULT_LAYER as Any)
layeredPane.add(terminalFindPanel, JLayeredPane.POPUP_LAYER as Any)
layeredPane.add(floatingToolbar, JLayeredPane.POPUP_LAYER as Any)
add(layeredPane, BorderLayout.CENTER)
add(scrollBar, BorderLayout.EAST)
@@ -127,6 +131,7 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
dataProviderSupport.addData(DataProviders.TerminalPanel, this)
dataProviderSupport.addData(DataProviders.Terminal, terminal)
dataProviderSupport.addData(DataProviders.PtyConnector, ptyConnector)
dataProviderSupport.addData(FloatingToolbarPanel.FloatingToolbar, floatingToolbar)
}
private fun initEvents() {
@@ -158,6 +163,11 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
this.addMouseListener(trackingAdapter)
this.addMouseWheelListener(trackingAdapter)
// 悬浮工具栏
val floatingToolBarAdapter = TerminalPanelMouseFloatingToolBarAdapter(this, terminalDisplay)
this.addMouseMotionListener(floatingToolBarAdapter)
this.addMouseListener(floatingToolBarAdapter)
// 滚动相关
this.addMouseWheelListener(object : MouseWheelListener {
override fun mouseWheelMoved(e: MouseWheelEvent) {
@@ -197,6 +207,8 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
// 开启拖拽
enableDropTarget()
// 监听悬浮工具栏变化,然后重新渲染
floatingToolbar.addPropertyChangeListener { repaintImmediate() }
}
@@ -373,6 +385,9 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
}
override fun dispose() {
Disposer.dispose(floatingToolbar)
}
fun getAverageCharWidth(): Int {
return terminalDisplay.getAverageCharWidth()
@@ -450,6 +465,7 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
synchronized(treeLock) {
val w = width
val h = height
val findPanelHeight = max(terminalFindPanel.preferredSize.height, terminalFindPanel.height)
for (c in components) {
when (c) {
terminalDisplay -> {
@@ -467,7 +483,19 @@ class TerminalPanel(val terminal: Terminal, private val ptyConnector: PtyConnect
w - width,
0,
width,
max(terminalFindPanel.preferredSize.height, terminalFindPanel.height)
findPanelHeight
)
}
floatingToolbar -> {
val width = floatingToolbar.preferredSize.width
val height = floatingToolbar.preferredSize.height
val y = 4
c.setBounds(
w - width,
if (terminalFindPanel.isVisible) findPanelHeight + y else y,
width,
height
)
}
}

View File

@@ -0,0 +1,49 @@
package app.termora.terminal.panel
import app.termora.Database
import java.awt.Rectangle
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
class TerminalPanelMouseFloatingToolBarAdapter(
private val terminalPanel: TerminalPanel,
private val terminalDisplay: TerminalDisplay
) : MouseAdapter() {
private val floatingToolbarEnable get() = Database.getDatabase().terminal.floatingToolbar
override fun mouseMoved(e: MouseEvent) {
if (!floatingToolbarEnable) {
return
}
val floatingToolbar = terminalPanel.getData(FloatingToolbarPanel.FloatingToolbar) ?: return
val width = terminalPanel.width
val height = terminalPanel.height
val widthDiff = (width * 0.25).toInt()
val heightDiff = (height * 0.25).toInt()
if (e.x in width - widthDiff..width && e.y in 0..heightDiff) {
floatingToolbar.triggerShow()
} else {
floatingToolbar.triggerHide()
}
}
override fun mouseExited(e: MouseEvent) {
val floatingToolbar = terminalPanel.getData(FloatingToolbarPanel.FloatingToolbar) ?: return
if (terminalDisplay.isShowing) {
val rectangle = Rectangle(terminalDisplay.locationOnScreen, terminalDisplay.size)
// 如果鼠标指针还在 terminalDisplay 中,那么就不需要隐藏
if (rectangle.contains(e.locationOnScreen)) {
return
}
}
floatingToolbar.triggerHide()
}
}

View File

@@ -70,6 +70,7 @@ termora.settings.terminal.beep=Beep
termora.settings.terminal.select-copy=Select copy
termora.settings.terminal.cursor-style=Cursor type
termora.settings.terminal.local-shell=Local shell
termora.settings.terminal.floating-toolbar=Floating Toolbar
termora.settings.terminal.auto-close-tab=Auto Close Tab
termora.settings.terminal.auto-close-tab-description=Automatically close the tab when the terminal is disconnected normally

View File

@@ -77,6 +77,7 @@ termora.settings.terminal.beep=蜂鸣声
termora.settings.terminal.select-copy=选中复制
termora.settings.terminal.cursor-style=光标样式
termora.settings.terminal.local-shell=本地终端
termora.settings.terminal.floating-toolbar=悬浮工具栏
termora.settings.terminal.auto-close-tab=自动关闭标签
termora.settings.terminal.auto-close-tab-description=当终端正常断开连接时自动关闭标签页

View File

@@ -86,6 +86,7 @@ termora.settings.terminal.beep=蜂鳴聲
termora.settings.terminal.select-copy=選取複製
termora.settings.terminal.cursor-style=遊標風格
termora.settings.terminal.local-shell=本地端
termora.settings.terminal.floating-toolbar=懸浮工具列
termora.settings.terminal.auto-close-tab=自動關閉標籤
termora.settings.terminal.auto-close-tab-description=當終端正常斷開連線時自動關閉標籤頁

View File

@@ -0,0 +1,6 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M11.4939 4.48784C11.3002 4.28007 10.9724 4.27548 10.7729 4.47775L8.00074 7.28849L5.22871 4.47788C5.02922 4.27561 4.70143 4.2802 4.50768 4.48797C4.32506 4.68382 4.32933 4.98882 4.51736 5.17947L7.29908 7.99991L4.51756 10.8201C4.32953 11.0108 4.32526 11.3158 4.50788 11.5116C4.70163 11.7194 5.02942 11.724 5.22892 11.5217L8.00074 8.71133L10.7727 11.5219C10.9722 11.7241 11.3 11.7196 11.4937 11.5118C11.6764 11.3159 11.6721 11.0109 11.484 10.8203L8.7024 7.99991L11.4843 5.17934C11.6723 4.98869 11.6766 4.68368 11.4939 4.48784Z"
fill="#A8ADBD"/>
</svg>

After

Width:  |  Height:  |  Size: 844 B

View File

@@ -0,0 +1,7 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle opacity="0.1" cx="8" cy="8" r="8" fill="#313547"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M11.4939 4.48784C11.3002 4.28007 10.9724 4.27548 10.7729 4.47775L8.00074 7.28849L5.22871 4.47788C5.02922 4.27561 4.70143 4.2802 4.50768 4.48797C4.32506 4.68382 4.32933 4.98882 4.51736 5.17947L7.29908 7.99991L4.51756 10.8201C4.32953 11.0108 4.32526 11.3158 4.50788 11.5116C4.70163 11.7194 5.02942 11.724 5.22892 11.5217L8.00074 8.71133L10.7727 11.5219C10.9722 11.7241 11.3 11.7196 11.4937 11.5118C11.6764 11.3159 11.6721 11.0109 11.484 10.8203L8.7024 7.99991L11.4843 5.17934C11.6723 4.98869 11.6766 4.68368 11.4939 4.48784Z"
fill="#818594"/>
</svg>

After

Width:  |  Height:  |  Size: 907 B

View File

@@ -0,0 +1,7 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle opacity="0.13" cx="8" cy="8" r="8" fill="#F0F1F2"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M11.4939 4.48784C11.3002 4.28007 10.9724 4.27548 10.7729 4.47775L8.00074 7.28849L5.22871 4.47788C5.02922 4.27561 4.70143 4.2802 4.50768 4.48797C4.32506 4.68382 4.32933 4.98882 4.51736 5.17947L7.29908 7.99991L4.51756 10.8201C4.32953 11.0108 4.32526 11.3158 4.50788 11.5116C4.70163 11.7194 5.02942 11.724 5.22892 11.5217L8.00074 8.71133L10.7727 11.5219C10.9722 11.7241 11.3 11.7196 11.4937 11.5118C11.6764 11.3159 11.6721 11.0109 11.484 10.8203L8.7024 7.99991L11.4843 5.17934C11.6723 4.98869 11.6766 4.68368 11.4939 4.48784Z"
fill="#868A91"/>
</svg>

After

Width:  |  Height:  |  Size: 908 B

View File

@@ -0,0 +1,6 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M11.4939 4.48784C11.3002 4.28007 10.9724 4.27548 10.7729 4.47775L8.00074 7.28849L5.22871 4.47788C5.02922 4.27561 4.70143 4.2802 4.50768 4.48797C4.32506 4.68382 4.32933 4.98882 4.51736 5.17947L7.29908 7.99991L4.51756 10.8201C4.32953 11.0108 4.32526 11.3158 4.50788 11.5116C4.70163 11.7194 5.02942 11.724 5.22892 11.5217L8.00074 8.71133L10.7727 11.5219C10.9722 11.7241 11.3 11.7196 11.4937 11.5118C11.6764 11.3159 11.6721 11.0109 11.484 10.8203L8.7024 7.99991L11.4843 5.17934C11.6723 4.98869 11.6766 4.68368 11.4939 4.48784Z"
fill="#6F737A"/>
</svg>

After

Width:  |  Height:  |  Size: 844 B