mirror of
https://github.com/TermoraDev/termora.git
synced 2026-01-16 02:12:58 +08:00
feat: support fallback font
This commit is contained in:
71
src/main/kotlin/app/termora/FontComboBox.kt
Normal file
71
src/main/kotlin/app/termora/FontComboBox.kt
Normal file
@@ -0,0 +1,71 @@
|
||||
package app.termora
|
||||
|
||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||
import com.formdev.flatlaf.util.FontUtils
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import javax.swing.DefaultListCellRenderer
|
||||
import javax.swing.JList
|
||||
import javax.swing.event.PopupMenuEvent
|
||||
import javax.swing.event.PopupMenuListener
|
||||
|
||||
class FontComboBox : FlatComboBox<String>() {
|
||||
private var fontsLoaded = false
|
||||
|
||||
init {
|
||||
val fontComboBox = this
|
||||
fontComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
init {
|
||||
preferredSize = Dimension(preferredSize.width, fontComboBox.preferredSize.height - 2)
|
||||
maximumSize = Dimension(preferredSize.width, preferredSize.height)
|
||||
}
|
||||
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
var text = value
|
||||
if (text is String) {
|
||||
if (text.isBlank()) {
|
||||
text = "<None>"
|
||||
}
|
||||
return super.getListCellRendererComponent(
|
||||
list,
|
||||
"<html><font face='$text'>$text</font></html>",
|
||||
index,
|
||||
isSelected,
|
||||
cellHasFocus
|
||||
)
|
||||
}
|
||||
return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus)
|
||||
}
|
||||
}
|
||||
fontComboBox.maximumSize = fontComboBox.preferredSize
|
||||
|
||||
fontComboBox.addPopupMenuListener(object : PopupMenuListener {
|
||||
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
|
||||
if (fontsLoaded) return
|
||||
val selectedItem = fontComboBox.selectedItem
|
||||
val families = getItems()
|
||||
for (family in FontUtils.getAvailableFontFamilyNames()) {
|
||||
if (families.contains(family).not()) fontComboBox.addItem(family)
|
||||
}
|
||||
fontComboBox.selectedItem = selectedItem
|
||||
fontsLoaded = true
|
||||
}
|
||||
|
||||
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {}
|
||||
override fun popupMenuCanceled(e: PopupMenuEvent) {}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
fun getItems(): Set<String> {
|
||||
val families = mutableSetOf<String>()
|
||||
for (i in 0 until itemCount) families.add(getItemAt(i))
|
||||
return families
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import com.formdev.flatlaf.FlatClientProperties
|
||||
import com.formdev.flatlaf.extras.components.FlatComboBox
|
||||
import com.formdev.flatlaf.extras.components.FlatPopupMenu
|
||||
import com.formdev.flatlaf.extras.components.FlatToolBar
|
||||
import com.formdev.flatlaf.util.FontUtils
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import com.jgoodies.forms.builder.FormBuilder
|
||||
import com.jgoodies.forms.layout.FormLayout
|
||||
@@ -29,7 +28,6 @@ import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.SystemUtils
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Component
|
||||
import java.awt.Dimension
|
||||
import java.awt.Toolkit
|
||||
import java.awt.event.ActionEvent
|
||||
import java.awt.event.ItemEvent
|
||||
@@ -399,7 +397,8 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private val debugComboBox = YesOrNoComboBox()
|
||||
private val beepComboBox = YesOrNoComboBox()
|
||||
private val cursorBlinkComboBox = YesOrNoComboBox()
|
||||
private val fontComboBox = FlatComboBox<String>()
|
||||
private val fontComboBox = FontComboBox()
|
||||
private val fallbackFontComboBox = FontComboBox()
|
||||
private val shellComboBox = FlatComboBox<String>()
|
||||
private val maxRowsTextField = IntSpinner(0, 0)
|
||||
private val fontSizeTextField = IntSpinner(0, 9, 99)
|
||||
@@ -418,6 +417,13 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
}
|
||||
|
||||
fallbackFontComboBox.addItemListener {
|
||||
if (it.stateChange == ItemEvent.SELECTED) {
|
||||
terminalSetting.fallbackFont = fallbackFontComboBox.selectedItem as String
|
||||
fireFontChanged()
|
||||
}
|
||||
}
|
||||
|
||||
autoCloseTabComboBox.addItemListener { e ->
|
||||
if (e.stateChange == ItemEvent.SELECTED) {
|
||||
terminalSetting.autoCloseTabWhenDisconnected = autoCloseTabComboBox.selectedItem as Boolean
|
||||
@@ -526,33 +532,6 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
}
|
||||
}
|
||||
|
||||
fontComboBox.renderer = object : DefaultListCellRenderer() {
|
||||
init {
|
||||
preferredSize = Dimension(preferredSize.width, fontComboBox.preferredSize.height - 2)
|
||||
maximumSize = Dimension(preferredSize.width, preferredSize.height)
|
||||
}
|
||||
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<*>?,
|
||||
value: Any?,
|
||||
index: Int,
|
||||
isSelected: Boolean,
|
||||
cellHasFocus: Boolean
|
||||
): Component {
|
||||
if (value is String) {
|
||||
return super.getListCellRendererComponent(
|
||||
list,
|
||||
"<html><font face='$value'>$value</font></html>",
|
||||
index,
|
||||
isSelected,
|
||||
cellHasFocus
|
||||
)
|
||||
}
|
||||
return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus)
|
||||
}
|
||||
}
|
||||
fontComboBox.maximumSize = fontComboBox.preferredSize
|
||||
|
||||
cursorStyleComboBox.addItem(CursorStyle.Block)
|
||||
cursorStyleComboBox.addItem(CursorStyle.Bar)
|
||||
cursorStyleComboBox.addItem(CursorStyle.Underline)
|
||||
@@ -566,29 +545,18 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
shellComboBox.selectedItem = terminalSetting.localShell
|
||||
|
||||
fontComboBox.addItem(terminalSetting.font)
|
||||
var fontsLoaded = false
|
||||
val items = fontComboBox.getItems()
|
||||
for (family in listOf("JetBrains Mono", "Source Code Pro", "Monospaced")) {
|
||||
if (items.contains(family).not()) fontComboBox.addItem(family)
|
||||
}
|
||||
|
||||
fontComboBox.addPopupMenuListener(object : PopupMenuListener {
|
||||
override fun popupMenuWillBecomeVisible(e: PopupMenuEvent) {
|
||||
if (!fontsLoaded) {
|
||||
val selectedItem = fontComboBox.selectedItem
|
||||
fontComboBox.removeAllItems();
|
||||
fontComboBox.addItem("JetBrains Mono")
|
||||
fontComboBox.addItem("Source Code Pro")
|
||||
fontComboBox.addItem("Monospaced")
|
||||
FontUtils.getAvailableFontFamilyNames().forEach {
|
||||
fontComboBox.addItem(it)
|
||||
}
|
||||
fontComboBox.selectedItem = selectedItem
|
||||
fontsLoaded = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun popupMenuWillBecomeInvisible(e: PopupMenuEvent) {}
|
||||
override fun popupMenuCanceled(e: PopupMenuEvent) {}
|
||||
})
|
||||
if (terminalSetting.fallbackFont.isNotBlank()) {
|
||||
fallbackFontComboBox.addItem(StringUtils.EMPTY)
|
||||
}
|
||||
fallbackFontComboBox.addItem(terminalSetting.fallbackFont)
|
||||
|
||||
fontComboBox.selectedItem = terminalSetting.font
|
||||
fallbackFontComboBox.selectedItem = terminalSetting.fallbackFont
|
||||
debugComboBox.selectedItem = terminalSetting.debug
|
||||
beepComboBox.selectedItem = terminalSetting.beep
|
||||
hyperlinkComboBox.selectedItem = terminalSetting.hyperlink
|
||||
@@ -627,7 +595,7 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
private fun getCenterComponent(): JComponent {
|
||||
val layout = FormLayout(
|
||||
"left:pref, $FORM_MARGIN, default:grow, $FORM_MARGIN, left:pref, $FORM_MARGIN, pref, default:grow",
|
||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||
"pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref, $FORM_MARGIN, pref"
|
||||
)
|
||||
|
||||
val beepBtn = JButton(Icons.run)
|
||||
@@ -643,6 +611,8 @@ class SettingsOptionsPane : OptionsPane() {
|
||||
.add(fontComboBox).xy(3, rows)
|
||||
.add("${I18n.getString("termora.settings.terminal.size")}:").xy(5, rows)
|
||||
.add(fontSizeTextField).xy(7, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.terminal.fallback-font")}:").xy(1, rows)
|
||||
.add(fallbackFontComboBox).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.terminal.max-rows")}:").xy(1, rows)
|
||||
.add(maxRowsTextField).xy(3, rows).apply { rows += step }
|
||||
.add("${I18n.getString("termora.settings.terminal.debug")}:").xy(1, rows)
|
||||
|
||||
@@ -619,6 +619,11 @@ class DatabaseManager private constructor() : Disposable {
|
||||
*/
|
||||
var font by StringPropertyDelegate("JetBrains Mono")
|
||||
|
||||
/**
|
||||
* 回退字体
|
||||
*/
|
||||
var fallbackFont by StringPropertyDelegate(StringUtils.EMPTY)
|
||||
|
||||
/**
|
||||
* 默认终端
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,7 @@ import app.termora.assertEventDispatchThread
|
||||
import app.termora.database.DatabaseManager
|
||||
import app.termora.swingCoroutineScope
|
||||
import app.termora.terminal.*
|
||||
import com.formdev.flatlaf.util.SystemInfo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -23,9 +24,15 @@ class TerminalDisplay(
|
||||
private val terminalBlink: TerminalBlink
|
||||
) : JComponent() {
|
||||
|
||||
enum class RendererFont {
|
||||
Base,
|
||||
Monospaced,
|
||||
Fallback,
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val lru = object : LinkedHashMap<String, Boolean>() {
|
||||
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, Boolean>?): Boolean {
|
||||
private val lru = object : LinkedHashMap<String, RendererFont>() {
|
||||
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, RendererFont>?): Boolean {
|
||||
return size > 2048
|
||||
}
|
||||
}
|
||||
@@ -40,6 +47,7 @@ class TerminalDisplay(
|
||||
private var boldFont = font.deriveFont(Font.BOLD)
|
||||
private var italicFont = font.deriveFont(Font.ITALIC)
|
||||
private var boldItalicFont = font.deriveFont(Font.ITALIC or Font.BOLD)
|
||||
private var fallbackFont = getFallbackTerminalFont()
|
||||
|
||||
/**
|
||||
* 正在输入的内容
|
||||
@@ -179,15 +187,22 @@ class TerminalDisplay(
|
||||
}
|
||||
|
||||
private fun checkFont() {
|
||||
// 如果字体已经改变,那么这里刷新字体
|
||||
if (font.family != DatabaseManager.getInstance().terminal.font
|
||||
|| font.size != DatabaseManager.getInstance().terminal.fontSize
|
||||
val terminal = DatabaseManager.getInstance().terminal
|
||||
|
||||
if ((terminal.fallbackFont.isNotBlank() && fallbackFont == null) ||
|
||||
(terminal.fallbackFont.isBlank() && fallbackFont != null) ||
|
||||
(terminal.fallbackFont != fallbackFont?.family) ||
|
||||
(font.size != terminal.fontSize)
|
||||
) {
|
||||
fallbackFont = getFallbackTerminalFont()
|
||||
}
|
||||
|
||||
if (font.family != terminal.font || font.size != terminal.fontSize) {
|
||||
font = getTerminalFont()
|
||||
monospacedFont = Font(Font.MONOSPACED, font.style, font.size)
|
||||
boldFont = font.deriveFont(Font.BOLD)
|
||||
italicFont = font.deriveFont(Font.ITALIC)
|
||||
boldItalicFont = font.deriveFont(Font.ITALIC or Font.BOLD)
|
||||
monospacedFont = Font(Font.MONOSPACED, font.style, font.size)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,7 +410,7 @@ class TerminalDisplay(
|
||||
fun getDisplayFont(text: String, style: TextStyle): Font {
|
||||
assertEventDispatchThread()
|
||||
|
||||
var font = if (style.bold && style.italic) {
|
||||
val displayFont = if (style.bold && style.italic) {
|
||||
boldItalicFont
|
||||
} else if (style.italic) {
|
||||
italicFont
|
||||
@@ -405,17 +420,38 @@ class TerminalDisplay(
|
||||
font
|
||||
}
|
||||
|
||||
var font = displayFont
|
||||
|
||||
val key = "${font.fontName}:${font.style}:${font.size}:${text}"
|
||||
if (lru.containsKey(key)) {
|
||||
if (!lru.getValue(key)) {
|
||||
font = monospacedFont
|
||||
val c = lru.getValue(key)
|
||||
font = when (c) {
|
||||
RendererFont.Base -> font
|
||||
RendererFont.Fallback -> fallbackFont ?: monospacedFont
|
||||
else -> monospacedFont
|
||||
}
|
||||
} else {
|
||||
if ((font.canDisplayUpTo(text) != -1).also { lru[key] = !it }) {
|
||||
font = monospacedFont
|
||||
// >=0 表示不支持
|
||||
if (FontCanDisplay.canDisplayUpTo(font, text) != -1) {
|
||||
val fallbackTerminalFont = fallbackFont ?: monospacedFont
|
||||
font = if (fallbackTerminalFont.fontName == monospacedFont.fontName) {
|
||||
monospacedFont
|
||||
} else if (FontCanDisplay.canDisplayUpTo(fallbackTerminalFont, text) != -1) {
|
||||
monospacedFont
|
||||
} else {
|
||||
fallbackTerminalFont
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// macOS 比较特殊,因为它可以自动选择 PingFang,而 PingFang 在 macOS 效果最好(前提是回退字体可用的情况下)
|
||||
if (SystemInfo.isMacOS) {
|
||||
if (font == monospacedFont) {
|
||||
font = displayFont
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return font
|
||||
}
|
||||
@@ -438,11 +474,17 @@ class TerminalDisplay(
|
||||
|
||||
|
||||
private fun getTerminalFont(): Font {
|
||||
return Font(
|
||||
DatabaseManager.getInstance().terminal.font,
|
||||
Font.PLAIN,
|
||||
DatabaseManager.getInstance().terminal.fontSize
|
||||
)
|
||||
val terminal = DatabaseManager.getInstance().terminal
|
||||
return Font(terminal.font, Font.PLAIN, terminal.fontSize)
|
||||
}
|
||||
|
||||
private fun getFallbackTerminalFont(): Font? {
|
||||
val terminal = DatabaseManager.getInstance().terminal
|
||||
return if (terminal.fallbackFont.isBlank()) {
|
||||
null
|
||||
} else {
|
||||
Font(terminal.fallbackFont, Font.PLAIN, terminal.fontSize)
|
||||
}
|
||||
}
|
||||
|
||||
fun toast(text: String, duration: Duration) {
|
||||
@@ -509,4 +551,39 @@ class TerminalDisplay(
|
||||
}
|
||||
|
||||
|
||||
private object FontCanDisplay {
|
||||
fun canDisplayUpTo(font: Font, str: String): Int {
|
||||
|
||||
if (SystemInfo.isWindows || SystemInfo.isLinux) {
|
||||
return font.canDisplayUpTo(str)
|
||||
}
|
||||
|
||||
val getFontMethod = Font::class.java.getDeclaredMethod("getFont2D")
|
||||
getFontMethod.isAccessible = true
|
||||
val font2d = getFontMethod.invoke(font)
|
||||
val getMapperMethod = font2d.javaClass.getDeclaredMethod("getMapper")
|
||||
getMapperMethod.isAccessible = true
|
||||
val mapper = getMapperMethod.invoke(font2d)
|
||||
val charToGlyphMethod = mapper.javaClass.getDeclaredMethod("charToGlyph", Char::class.java)
|
||||
|
||||
val len = str.length
|
||||
var i = 0
|
||||
while (i < len) {
|
||||
val c = str[i]
|
||||
val glyph = charToGlyphMethod.invoke(mapper, c) as Int
|
||||
if (glyph >= 0) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (!Character.isHighSurrogate(c)
|
||||
|| (charToGlyphMethod.invoke(mapper, str.codePointAt(i)) as Int) < 0
|
||||
) {
|
||||
return i
|
||||
}
|
||||
i += 2
|
||||
}
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -63,6 +63,7 @@ termora.settings.appearance.confirm-tab-close=Confirm tab close
|
||||
|
||||
termora.settings.terminal=Terminal
|
||||
termora.settings.terminal.font=Font
|
||||
termora.settings.terminal.fallback-font=Fallback Font
|
||||
termora.settings.terminal.size=Size
|
||||
termora.settings.terminal.max-rows=Max rows
|
||||
termora.settings.terminal.debug=Debug mode
|
||||
|
||||
@@ -76,6 +76,7 @@ termora.find-everywhere.quick-command.local-terminal=本地终端
|
||||
|
||||
termora.settings.terminal=终端
|
||||
termora.settings.terminal.font=字体
|
||||
termora.settings.terminal.fallback-font=回退字体
|
||||
termora.settings.terminal.size=大小
|
||||
termora.settings.terminal.max-rows=最大行数
|
||||
termora.settings.terminal.debug=调试模式
|
||||
|
||||
@@ -86,6 +86,7 @@ termora.find-everywhere.quick-command.local-terminal=本地端
|
||||
|
||||
termora.settings.terminal=終端
|
||||
termora.settings.terminal.font=字體
|
||||
termora.settings.terminal.fallback-font=回退字體
|
||||
termora.settings.terminal.size=大小
|
||||
termora.settings.terminal.max-rows=最大行數
|
||||
termora.settings.terminal.debug=偵錯模式
|
||||
|
||||
Reference in New Issue
Block a user