mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 10:22:58 +08:00
feat: Floating Toolbar (#231)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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") }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user